diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index 55bf334f2..b48f1be83 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -333,7 +333,13 @@ const overrides = new Map([ // absent, so AdapterMissing replaces the misleading NotInstalled. Includes // the deliberate-divergence doc comments; net after the inline preset // entries.push block collapsed into the helper. - ["src-tauri/src/managed_agents/discovery.rs", 1835], + // +101: preset auto-install — PresetHarness.adapter_install_commands (per-entry, + // populated for Amp only) drives can_auto_install and node_required, the + // node_required predicate is extracted and shared with the builtin path, and + // AcpInstallPlan / acp_install_plan project the installer's slice of either + // tier behind one lookup so presets resolve without widening KnownAcpRuntime. + // Includes the tier-boundary doc comments. + ["src-tauri/src/managed_agents/discovery.rs", 1936], // BYOH — save_custom_harness_to_dir (backup-swap atomic write) + save_and_warm / // delete_and_warm (persist-mutex serialization for concurrent-safe registry // refresh, B-6). Also: id/collision/load/registry tests (from the file base) + @@ -378,7 +384,12 @@ const overrides = new Map([ // Available both-present AND adapter-present/CLI-absent — the selectability // regression guard), bound to an injectable resolver so the tests stay // PATH-independent. - ["src-tauri/src/managed_agents/discovery/tests.rs", 1871], + // +155: preset auto-install coverage — can_auto_install derivation (both + // arms), node_required on managed-node-supported AND unsupported platforms, + // the available-adapter and docs-only negatives, acp_install_plan resolution + // for preset/builtin/custom-id, and a scope guard asserting Amp is the only + // preset carrying install commands. + ["src-tauri/src/managed_agents/discovery/tests.rs", 2026], // identity-import-keyring: the identity resolution state machine's behavioral // matrix (46 tests over FakeIdentityStore — probe × marker × file cells, // adoption / read-back-corruption / marker-failure arms, recovery-mode @@ -616,7 +627,11 @@ const overrides = new Map([ // return value so the frontend immediately has the updated env. // +1: rebase over main (#2680) — requires_external_cli: false added to // save_custom_harness catalog entry construction (new required field). - ["src-tauri/src/commands/agent_discovery.rs", 2167], + // +52: preset auto-install — install_acp_runtime_blocking resolves through + // acp_install_plan (builtins then presets) instead of known_acp_runtime_exact, + // plus tests pinning that Amp takes the plain adapter-missing arm and that + // Phase 1 no-ops for a preset whose vendor CLI is absent. + ["src-tauri/src/commands/agent_discovery.rs", 2219], // draft-persistence predicate: submit-time `loadDraft` check + inline comment // + deps-array entry in submitMessage closes the never-persisted-boundary // defect (Thufir Pass-3 finding). Load-bearing correctness fix; queued to diff --git a/desktop/src-tauri/src/commands/agent_discovery.rs b/desktop/src-tauri/src/commands/agent_discovery.rs index 84a00433c..e0df900ef 100644 --- a/desktop/src-tauri/src/commands/agent_discovery.rs +++ b/desktop/src-tauri/src/commands/agent_discovery.rs @@ -294,7 +294,7 @@ fn install_acp_runtime_blocking(runtime_id: &str) -> Result Result Result Option { resolve_command(command) } -/// Returns true when the runtime has at least one adapter install step that -/// is an npm global install. Used to determine whether Node.js is required. -fn runtime_needs_npm(runtime: &KnownAcpRuntime) -> bool { - runtime - .adapter_install_commands +/// True when no npm is reachable for an npm-backed install: Buzz cannot +/// provide its private Node runtime on this platform AND the machine has no +/// system `npm`/`node`. +fn npm_unavailable() -> bool { + buzz_managed_node_bin_dir().is_none() + && resolve_command("npm").is_none() + && resolve_command("node").is_none() +} + +/// `node_required` means Buzz cannot provide npm for this platform, so an +/// npm-backed install cannot possibly succeed and Doctor must send the user +/// to nodejs.org instead of offering a toggle. +/// +/// Only adapter commands are considered: CLI install steps are curl-pipe +/// installers, never npm. `npm_unavailable` is passed in rather than probed +/// so the platform branch is testable on any machine. +fn node_required( + availability: &AcpAvailabilityStatus, + adapter_install_commands: &[&str], + npm_unavailable: bool, +) -> bool { + matches!( + availability, + AcpAvailabilityStatus::AdapterMissing | AcpAvailabilityStatus::NotInstalled + ) && adapter_install_commands .iter() .any(|cmd| is_npm_global_install(cmd)) + && npm_unavailable } /// Returns `true` when `cmd` is an npm global install/uninstall invocation. @@ -1363,13 +1384,11 @@ fn discover_acp_runtime_phase1(runtime: &'static KnownAcpRuntime) -> PartialEntr // node_required now means Buzz cannot provide npm for this platform. // On supported desktop platforms, Buzz downloads a private Node/npm // runtime into app data before running npm-backed adapter installs. - let node_required = matches!( - availability, - AcpAvailabilityStatus::AdapterMissing | AcpAvailabilityStatus::NotInstalled - ) && runtime_needs_npm(runtime) - && buzz_managed_node_bin_dir().is_none() - && resolve_command("npm").is_none() - && resolve_command("node").is_none(); + let node_required = node_required( + &availability, + runtime.adapter_install_commands, + npm_unavailable(), + ); PartialEntry { runtime, @@ -1405,10 +1424,18 @@ fn discover_acp_runtime_phase1(runtime: &'static KnownAcpRuntime) -> PartialEntr /// /// Post-install verification only needs to know whether the requested runtime /// resolves, so it should not pay the cost of authenticating every catalog entry. +/// +/// Covers presets as well as builtins: any runtime the installer can act on +/// must also be verifiable, or a successful preset install reports as failed. +/// The preset arm builds the full catalog entry and keeps only the +/// availability — deliberately, so verification can never disagree with what +/// the catalog will show the user a moment later. pub(crate) fn discover_acp_runtime_availability(runtime_id: &str) -> Option { - known_acp_runtime_exact(runtime_id) - .map(discover_acp_runtime_phase1) - .map(|partial| partial.entry.availability) + if let Some(runtime) = known_acp_runtime_exact(runtime_id) { + return Some(discover_acp_runtime_phase1(runtime).entry.availability); + } + preset_harness_exact(runtime_id) + .map(|def| preset_catalog_entry(def, find_command, npm_unavailable()).availability) } // ── Tier-2 preset harnesses ──────────────────────────────────────────────── @@ -1436,6 +1463,15 @@ struct PresetHarness { /// the `CliMissing` copy would tell the user to install the adapter /// they already have. `None` when the command IS the vendor CLI. underlying_cli: Option<&'static str>, + /// Shell commands that install this preset's ACP adapter, run in order by + /// `install_acp_runtime_blocking`. Empty — the default — means no + /// auto-install: the Doctor toggle stays disabled and the card is + /// docs-link only. npm-global shapes are rewritten to Buzz's private npm + /// prefix before execution, exactly as for the builtins. + /// + /// There is deliberately no CLI-install counterpart: a preset's + /// `underlying_cli` is a status probe, never an install target. + adapter_install_commands: &'static [&'static str], } /// Build the catalog entry for one preset harness through an injectable @@ -1448,9 +1484,14 @@ struct PresetHarness { /// distinguish `AdapterMissing` (vendor CLI present) from `NotInstalled` /// (neither found). See the `underlying_cli` field doc for why the full /// `classify_runtime` predicate is deliberately not used here. +/// +/// `npm_unavailable` is injected for the same reason as `resolve`: it is a +/// platform probe, and the `node_required` branch it drives has to be +/// exercised on both a managed-node-supported and an unsupported platform. fn preset_catalog_entry( def: &PresetHarness, resolve: impl Fn(&str) -> Option, + npm_unavailable: bool, ) -> AcpRuntimeCatalogEntry { let (availability, command, binary_path) = match resolve(def.command) { Some(path) => ( @@ -1480,6 +1521,8 @@ fn preset_catalog_entry( def.args.iter().map(|s| s.to_string()).collect(), ); + let node_required = node_required(&availability, def.adapter_install_commands, npm_unavailable); + AcpRuntimeCatalogEntry { id: def.id.to_string(), label: def.label.to_string(), @@ -1495,7 +1538,7 @@ fn preset_catalog_entry( thinking_env_var: None, install_hint: def.install_hint.to_string(), install_instructions_url: def.install_instructions_url.to_string(), - can_auto_install: false, + can_auto_install: !def.adapter_install_commands.is_empty(), // Kept false even for adapter presets: presets carry one flat // install_hint (the adapter's), so the requiresExternalCli // "CLI is missing" wording would pair the wrong noun with it. @@ -1503,7 +1546,7 @@ fn preset_catalog_entry( // consumer of the true case. requires_external_cli: false, underlying_cli_path, - node_required: false, + node_required, auth_status: AuthStatus::NotApplicable, login_hint: None, source: HarnessSource::Preset, @@ -1521,6 +1564,7 @@ const PRESET_HARNESSES: &[PresetHarness] = &[ install_instructions_url: "https://cursor.com/downloads", install_hint: "Install Cursor from cursor.com/downloads.", underlying_cli: None, + adapter_install_commands: &[], }, PresetHarness { id: "omp", @@ -1530,6 +1574,7 @@ const PRESET_HARNESSES: &[PresetHarness] = &[ install_instructions_url: "https://github.com/can1357/oh-my-pi", install_hint: "Install Oh My Pi from github.com/can1357/oh-my-pi.", underlying_cli: None, + adapter_install_commands: &[], }, PresetHarness { id: "grok", @@ -1539,6 +1584,7 @@ const PRESET_HARNESSES: &[PresetHarness] = &[ install_instructions_url: "https://build.x.ai/docs", install_hint: "Install Grok Build from build.x.ai.", underlying_cli: None, + adapter_install_commands: &[], }, PresetHarness { id: "opencode", @@ -1548,6 +1594,7 @@ const PRESET_HARNESSES: &[PresetHarness] = &[ install_instructions_url: "https://opencode.ai/docs", install_hint: "Install OpenCode from opencode.ai/docs.", underlying_cli: None, + adapter_install_commands: &[], }, PresetHarness { id: "kimi", @@ -1557,6 +1604,7 @@ const PRESET_HARNESSES: &[PresetHarness] = &[ install_instructions_url: "https://kimi.ai/download", install_hint: "Install Kimi Code from kimi.ai/download.", underlying_cli: None, + adapter_install_commands: &[], }, PresetHarness { id: "amp", @@ -1566,6 +1614,11 @@ const PRESET_HARNESSES: &[PresetHarness] = &[ install_instructions_url: "https://github.com/tao12345666333/amp-acp", install_hint: "Install the amp-acp npm adapter: npm install -g amp-acp.", underlying_cli: Some("amp"), + // The only preset with an auto-install today. `amp-acp` is the npm + // package's own bin name, so the installed global shim is exactly + // the `command` probed above. The vendor `amp` CLI is NOT installed + // here — see the field doc. + adapter_install_commands: &["npm install -g amp-acp"], }, PresetHarness { id: "hermes", @@ -1575,6 +1628,7 @@ const PRESET_HARNESSES: &[PresetHarness] = &[ install_instructions_url: "https://hermes-agent.nousresearch.com", install_hint: "Install Hermes Agent from hermes-agent.nousresearch.com.", underlying_cli: None, + adapter_install_commands: &[], }, PresetHarness { id: "openclaw", @@ -1591,6 +1645,7 @@ const PRESET_HARNESSES: &[PresetHarness] = &[ needs BUZZ_* credentials at execution time, set them on the \ Gateway's own environment separately.", underlying_cli: None, + adapter_install_commands: &[], }, ]; @@ -1616,6 +1671,50 @@ pub(crate) fn preset_harness_definitions( .collect() } +fn preset_harness_exact(id: &str) -> Option<&'static PresetHarness> { + PRESET_HARNESSES.iter().find(|p| p.id == id) +} + +/// The install-time slice of a runtime, projected from either tier. +/// +/// `install_acp_runtime_blocking` consumes exactly these four things, so one +/// lookup can serve builtins and presets without either static type growing +/// the other's fields — they differ deliberately (see +/// `PresetHarness::underlying_cli`). This is the narrow intersection. +pub(crate) struct AcpInstallPlan { + /// Vendor CLI to probe before the adapter phase. + pub underlying_cli: Option<&'static str>, + /// CLI install steps, run only when `underlying_cli` is absent. Always + /// empty for presets, whose vendor CLI is installed out of band. + pub cli_install_commands: &'static [&'static str], + /// Adapter install steps. Empty means no auto-install. + pub adapter_install_commands: &'static [&'static str], + /// Commands to probe for an already-installed adapter. + pub adapter_commands: &'static [&'static str], +} + +/// Resolve the install plan for `runtime_id`, checking builtins then presets. +/// +/// `None` for an unknown id — including custom (Tier-3) harnesses, whose +/// install hint is user-typed text and never an executable plan. +pub(crate) fn acp_install_plan(runtime_id: &str) -> Option { + if let Some(runtime) = known_acp_runtime_exact(runtime_id) { + return Some(AcpInstallPlan { + underlying_cli: runtime.underlying_cli, + cli_install_commands: runtime.cli_install_commands_for_os(), + adapter_install_commands: runtime.adapter_install_commands, + adapter_commands: runtime.commands, + }); + } + let preset = preset_harness_exact(runtime_id)?; + Some(AcpInstallPlan { + underlying_cli: preset.underlying_cli, + cli_install_commands: &[], + adapter_install_commands: preset.adapter_install_commands, + adapter_commands: std::slice::from_ref(&preset.command), + }) +} + /// Return the static slice of preset harness IDs. /// /// Used by `check_id_collision` in `custom_harnesses` to derive the reserved-ID @@ -1712,6 +1811,8 @@ pub fn discover_acp_runtimes_from( entries.iter().map(|e| e.id.clone()).collect(); // Phase 2.5: insert static preset entries (PATH-probed, not editable/deletable). + // The npm probe is platform state, identical for every preset — resolve once. + let npm_unavailable = npm_unavailable(); for def in PRESET_HARNESSES { if seen_ids.contains(def.id) { // Builtin or earlier preset shadowed this id — skip silently. @@ -1719,7 +1820,7 @@ pub fn discover_acp_runtimes_from( } seen_ids.insert(def.id.to_string()); - entries.push(preset_catalog_entry(def, find_command)); + entries.push(preset_catalog_entry(def, find_command, npm_unavailable)); } // Phase 3: load and append custom harness definitions. diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 48e8d5479..825f8d23b 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -2,13 +2,14 @@ use std::path::PathBuf; use super::overrides::{divergent_agent_command_override, update_time_agent_command_override}; use super::{ - apply_agent_command_update, classify_runtime, codex_adapter_availability, + acp_install_plan, apply_agent_command_update, classify_runtime, codex_adapter_availability, codex_adapter_is_outdated, create_time_agent_command_override, default_agent_command, effective_agent_command, find_nvm_default_bin, find_via_login_shell, - is_login_shell_path_uninit, is_safe_nvm_tag, managed_agent_avatar_url, normalize_agent_args, - parse_semver_tag, preset_catalog_entry, probe_codex_acp_major_version, record_agent_command, - refresh_login_shell_path, try_record_agent_command, PresetHarness, BUZZ_AGENT_AVATAR_URL, - CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL, + is_login_shell_path_uninit, is_safe_nvm_tag, managed_agent_avatar_url, node_required, + normalize_agent_args, parse_semver_tag, preset_catalog_entry, preset_harness_ids, + probe_codex_acp_major_version, record_agent_command, refresh_login_shell_path, + try_record_agent_command, PresetHarness, BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL, + CODEX_AVATAR_URL, GOOSE_AVATAR_URL, }; use crate::managed_agents::AcpAvailabilityStatus; @@ -199,13 +200,37 @@ const ADAPTER_PRESET: PresetHarness = PresetHarness { install_instructions_url: "https://example.com/install", install_hint: "Install the amp-acp npm adapter.", underlying_cli: Some("amp"), + adapter_install_commands: &["npm install -g amp-acp"], +}; + +/// Build a preset entry on a platform where npm IS reachable — the case every +/// availability/wording test below cares about. The `node_required` platform +/// branch has its own dedicated tests. +fn preset_entry( + def: &PresetHarness, + resolve: impl Fn(&str) -> Option, +) -> crate::managed_agents::AcpRuntimeCatalogEntry { + preset_catalog_entry(def, resolve, false) +} + +/// Docs-link-only preset: the command IS the vendor CLI and Buzz installs +/// nothing. The shape of every preset except Amp. +const DOCS_ONLY_PRESET: PresetHarness = PresetHarness { + id: "docs-only-test", + label: "Docs Only Test", + command: "vendor-cli", + args: &["acp"], + install_instructions_url: "https://example.com/install", + install_hint: "Install Vendor CLI from example.com.", + underlying_cli: None, + adapter_install_commands: &[], }; #[test] fn preset_entry_adapter_missing_when_underlying_cli_present() { // Vendor CLI resolves, adapter does not — the state Tyler's Amp // hand-test hit. Must NOT degrade to the misleading NotInstalled. - let entry = preset_catalog_entry(&ADAPTER_PRESET, |cmd| { + let entry = preset_entry(&ADAPTER_PRESET, |cmd| { (cmd == "amp").then(|| PathBuf::from("/usr/local/bin/amp")) }); assert_eq!(entry.availability, AcpAvailabilityStatus::AdapterMissing); @@ -221,7 +246,7 @@ fn preset_entry_adapter_missing_when_underlying_cli_present() { #[test] fn preset_entry_not_installed_when_both_missing() { - let entry = preset_catalog_entry(&ADAPTER_PRESET, |_| None); + let entry = preset_entry(&ADAPTER_PRESET, |_| None); assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled); assert!(entry.underlying_cli_path.is_none()); assert!(!entry.requires_external_cli); @@ -229,7 +254,7 @@ fn preset_entry_not_installed_when_both_missing() { #[test] fn preset_entry_available_when_adapter_and_cli_present() { - let entry = preset_catalog_entry(&ADAPTER_PRESET, |cmd| match cmd { + let entry = preset_entry(&ADAPTER_PRESET, |cmd| match cmd { "amp-acp" => Some(PathBuf::from("/usr/local/bin/amp-acp")), "amp" => Some(PathBuf::from("/usr/local/bin/amp")), _ => None, @@ -250,7 +275,7 @@ fn preset_entry_stays_available_when_adapter_present_but_cli_absent() { // FULL classify_runtime predicate would flip this to CliMissing // (unselectable, with backwards install copy) — the adapter-missing // arm is the only one presets consume. - let entry = preset_catalog_entry(&ADAPTER_PRESET, |cmd| { + let entry = preset_entry(&ADAPTER_PRESET, |cmd| { (cmd == "amp-acp").then(|| PathBuf::from("/usr/local/bin/amp-acp")) }); assert_eq!(entry.availability, AcpAvailabilityStatus::Available); @@ -267,12 +292,142 @@ fn preset_entry_without_underlying_cli_stays_simple() { underlying_cli: None, ..ADAPTER_PRESET }; - let entry = preset_catalog_entry(&preset, |_| None); + let entry = preset_entry(&preset, |_| None); assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled); assert!(!entry.requires_external_cli); assert!(entry.underlying_cli_path.is_none()); } +// ── Preset auto-install (Tier-2) ──────────────────────────────────────────── + +#[test] +fn test_preset_with_adapter_install_commands_can_auto_install() { + let entry = preset_entry(&ADAPTER_PRESET, |_| None); + assert!( + entry.can_auto_install, + "a preset carrying install commands must offer the Doctor toggle" + ); +} + +#[test] +fn test_preset_without_adapter_install_commands_cannot_auto_install() { + // The docs-link-only shape: every preset except Amp. Nothing to run, so + // the toggle must stay disabled rather than invoking a no-op install. + let entry = preset_entry(&DOCS_ONLY_PRESET, |_| None); + assert!(!entry.can_auto_install); +} + +#[test] +fn test_installable_preset_node_required_when_npm_unavailable() { + // Buzz cannot provide npm on this platform and none is on PATH: Doctor + // must send the user to nodejs.org instead of offering a toggle that + // cannot possibly succeed. + let entry = preset_catalog_entry(&ADAPTER_PRESET, |_| None, true); + assert_eq!(entry.availability, AcpAvailabilityStatus::NotInstalled); + assert!(entry.node_required); +} + +#[test] +fn test_installable_preset_not_node_required_when_npm_available() { + let entry = preset_catalog_entry(&ADAPTER_PRESET, |_| None, false); + assert!(!entry.node_required); +} + +#[test] +fn test_docs_only_preset_never_node_required() { + // No npm-backed install to gate, so the nodejs.org nudge would be noise + // even on a platform with no npm at all. + let entry = preset_catalog_entry(&DOCS_ONLY_PRESET, |_| None, true); + assert!(!entry.node_required); +} + +#[test] +fn test_available_preset_not_node_required_despite_missing_npm() { + // The adapter already resolves; nothing will be installed, so the + // platform's npm situation is irrelevant. + let entry = preset_catalog_entry( + &ADAPTER_PRESET, + |cmd| (cmd == "amp-acp").then(|| PathBuf::from("/usr/local/bin/amp-acp")), + true, + ); + assert_eq!(entry.availability, AcpAvailabilityStatus::Available); + assert!(!entry.node_required); +} + +#[test] +fn test_amp_preset_carries_npm_adapter_install() { + // Pins the one populated entry in the shipped catalog. `amp-acp` is both + // the probed command and the npm package's bin name, so the installed + // global shim is exactly what discovery looks for. + let plan = acp_install_plan("amp").expect("amp must resolve as installable"); + assert_eq!(plan.adapter_install_commands, ["npm install -g amp-acp"]); + assert_eq!(plan.adapter_commands, ["amp-acp"]); + assert_eq!(plan.underlying_cli, Some("amp")); +} + +#[test] +fn test_every_other_preset_has_no_install_commands() { + // Scope guard: Will's ruling is adapters only, and Amp is the only ACP + // adapter on npm today. This reds if a vendor-CLI preset gains an + // install command without that ruling being revisited. + for id in preset_harness_ids() { + if *id == "amp" { + continue; + } + let plan = acp_install_plan(id).expect("every preset id must resolve"); + assert!( + plan.adapter_install_commands.is_empty(), + "preset {id} must stay docs-link only" + ); + } +} + +#[test] +fn test_preset_install_plan_carries_no_cli_install_commands() { + // A preset's underlying_cli is a status probe, never an install target: + // Phase 1 of the installer must find nothing to run and fall through to + // the adapter phase rather than erroring. + let plan = acp_install_plan("amp").expect("amp must resolve"); + assert!(plan.underlying_cli.is_some()); + assert!( + plan.cli_install_commands.is_empty(), + "presets never install the vendor CLI" + ); +} + +#[test] +fn test_builtin_install_plan_still_resolves_through_shared_lookup() { + // Builtins must keep their CLI install steps and full command list. + let plan = acp_install_plan("codex").expect("codex must resolve"); + assert_eq!(plan.underlying_cli, Some("codex")); + assert!(!plan.cli_install_commands.is_empty()); + assert!(plan + .adapter_install_commands + .iter() + .any(|cmd| cmd.contains("codex-acp"))); + assert!(plan.adapter_commands.contains(&"codex-acp")); +} + +#[test] +fn test_custom_harness_id_has_no_install_plan() { + // Tier-3 boundary: custom harnesses carry a user-typed install hint. + // Resolving one here would wire a one-click button to a user-supplied + // shell string. `None` is what keeps `can_auto_install` false for them. + assert!(acp_install_plan("my-custom-harness").is_none()); + assert!(acp_install_plan("").is_none()); +} + +#[test] +fn test_node_required_ignores_non_npm_install_commands() { + // CLI installers are curl-pipes; they need no npm, so a missing npm must + // not gate them. + assert!(!node_required( + &AcpAvailabilityStatus::NotInstalled, + &["curl -fsSL https://example.com/install.sh | bash"], + true, + )); +} + fn persona_with_runtime(id: &str, runtime: Option<&str>) -> crate::managed_agents::AgentDefinition { crate::managed_agents::AgentDefinition { id: id.to_string(),