diff --git a/Cargo.lock b/Cargo.lock index 5e9c5aade3d..e35e3f833f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -846,6 +846,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.11.0", + "shlex", "thiserror 2.0.18", "tokio", "tokio-tungstenite 0.29.0", diff --git a/crates/buzz-acp/Cargo.toml b/crates/buzz-acp/Cargo.toml index d047849806f..e828b40bda6 100644 --- a/crates/buzz-acp/Cargo.toml +++ b/crates/buzz-acp/Cargo.toml @@ -68,6 +68,9 @@ clap = { version = "4", features = ["derive", "env"] } # Config file toml = "1.0" +# Shell-style argv splitting for BUZZ_ACP_AGENT_ARGS and extra MCP commands +shlex = "1.3" + # Filter expressions evalexpr = { workspace = true } diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index e6164b02dd3..2d1774469d4 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -111,6 +111,7 @@ All configuration is via environment variables (or CLI flags — every env var h | `BUZZ_ACP_AGENT_COMMAND` | no | `goose` | Agent binary to spawn. | | `BUZZ_ACP_AGENT_ARGS` | no | `acp` | Agent arguments (comma-separated). | | `BUZZ_ACP_MCP_COMMAND` | no | `""` (empty) | Path to an optional MCP server binary to provide to the agent subprocess. | +| `BUZZ_ACP_EXTRA_MCP_COMMANDS` | no | — | Comma-separated additional MCP server commands. Each entry is shell-split with POSIX quoting (e.g. `npx -y my-mcp-server`). Extra servers do **not** receive `BUZZ_PRIVATE_KEY`, `BUZZ_RELAY_URL`, or `BUZZ_AUTH_TAG` — they are third-party tools, not Buzz-native MCP. Names are derived from the executable stem, sanitized to ASCII alphanumeric/hyphen, and disambiguated with numeric suffixes. Malformed quoting fails startup with the entry index (the raw command is not logged). | | `BUZZ_ACP_IDLE_TIMEOUT` | no | `620` | Idle timeout: max seconds of silence before cancelling a turn. Resets on any agent stdout activity. | | `BUZZ_ACP_MAX_TURN_DURATION` | no | `7200` | Absolute wall-clock cap per turn (safety valve). | | `BUZZ_API_TOKEN` | no | — | API token (required if relay enforces token auth). | diff --git a/crates/buzz-acp/src/acp.rs b/crates/buzz-acp/src/acp.rs index 0d87bca028c..994f6486aa2 100644 --- a/crates/buzz-acp/src/acp.rs +++ b/crates/buzz-acp/src/acp.rs @@ -26,12 +26,23 @@ const MAX_LINE_SIZE: usize = 10_000_000; // 10 MB /// /// Corresponds to the `McpServerStdio` variant in the ACP schema. /// All four fields are **required** by the schema (`args` and `env` may be empty arrays). +/// `trusted` controls whether the agent runtime passes Buzz identity credentials +/// (`BUZZ_PRIVATE_KEY`, `BUZZ_RELAY_URL`, `BUZZ_AUTH_TAG`) into the child process. +/// Only the built-in `buzz-dev-mcp` server is trusted; extra MCP servers +/// configured via `BUZZ_ACP_EXTRA_MCP_COMMANDS` are untrusted and receive no +/// Buzz credentials. #[derive(Debug, Clone, serde::Serialize)] pub struct McpServer { pub name: String, pub command: String, pub args: Vec, pub env: Vec, + #[serde(default, skip_serializing_if = "is_false")] + pub trusted: bool, +} + +fn is_false(b: &bool) -> bool { + !b } /// A single environment variable for an MCP server. @@ -2538,6 +2549,7 @@ mod tests { value: "nsec1abc".into(), }, ], + trusted: true, }; let serialized = serde_json::to_value(&server).unwrap(); assert_eq!(serialized["name"].as_str(), Some("test-mcp")); diff --git a/crates/buzz-acp/src/config.rs b/crates/buzz-acp/src/config.rs index 4a82cf6306d..f0063ea7d9c 100644 --- a/crates/buzz-acp/src/config.rs +++ b/crates/buzz-acp/src/config.rs @@ -267,6 +267,17 @@ pub struct CliArgs { #[arg(long, env = "BUZZ_ACP_MCP_COMMAND", default_value = "")] pub mcp_command: String, + /// Additional MCP server commands to pass to the agent session alongside + /// the primary MCP server. Entries are comma-separated; each entry is + /// shell-split (shlex) into a command and its args, so quoted paths and + /// arguments with spaces are preserved. Server names are derived from the + /// executable stem and disambiguated with a numeric suffix if duplicates + /// occur (e.g. two `npx` wrappers become `npx` and `npx-2`). Entries with + /// malformed quoting are skipped with a warning. Example: + /// `npx -y mcp-remote https://mcp.tavily.com/mcp/?tavilyApiKey=...,other-server` + #[arg(long, env = "BUZZ_ACP_EXTRA_MCP_COMMANDS", value_delimiter = ',')] + pub extra_mcp_commands: Vec, + /// Idle timeout: max seconds of silence before killing a turn. /// Resets on any agent stdout activity. #[arg(long, env = "BUZZ_ACP_IDLE_TIMEOUT")] @@ -521,6 +532,7 @@ pub struct Config { pub agent_command: String, pub agent_args: Vec, pub mcp_command: String, + pub extra_mcp_commands: Vec, pub idle_timeout_secs: u64, pub max_turn_duration_secs: u64, pub agents: u32, @@ -1097,6 +1109,7 @@ impl Config { agent_command, agent_args, mcp_command: args.mcp_command, + extra_mcp_commands: args.extra_mcp_commands, idle_timeout_secs, max_turn_duration_secs, agents: args.agents, @@ -1478,6 +1491,7 @@ mod tests { agent_command: "goose".into(), agent_args: vec!["acp".into()], mcp_command: "".into(), + extra_mcp_commands: vec![], idle_timeout_secs: DEFAULT_IDLE_TIMEOUT_SECS, max_turn_duration_secs: DEFAULT_MAX_TURN_DURATION_SECS, agents: 1, diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index 146214197a8..d1c87960db1 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -30,7 +30,7 @@ use buzz_core::observer::{ }; use clap::Parser; use config::{ - AuthAgentArgs, AuthMethodsArgs, AuthenticateArgs, Config, DedupMode, ModelsArgs, + AuthAgentArgs, AuthMethodsArgs, AuthenticateArgs, Config, ConfigError, DedupMode, ModelsArgs, MultipleEventHandling, RespondTo, SubscribeMode, }; use filter::SubscriptionRule; @@ -2191,7 +2191,7 @@ async fn tokio_main() -> Result<()> { let base_prompt_content = config.base_prompt_content.take(); let cwd = current_working_directory()?; let ctx = Arc::new(PromptContext { - mcp_servers: build_mcp_servers(&config), + mcp_servers: build_mcp_servers(&config)?, initial_message: config.initial_message.clone(), idle_timeout: Duration::from_secs(config.idle_timeout_secs), max_turn_duration: Duration::from_secs(config.max_turn_duration_secs), @@ -5030,11 +5030,11 @@ async fn run_models(args: ModelsArgs) -> Result<()> { Ok(()) } -fn build_mcp_servers(config: &Config) -> Vec { +fn build_mcp_servers(config: &Config) -> Result, ConfigError> { if config.mcp_command.is_empty() { - return vec![]; + return Ok(vec![]); } - vec![McpServer { + let mut servers = vec![McpServer { name: std::path::Path::new(&config.mcp_command) .file_stem() .and_then(|s| s.to_str()) @@ -5084,7 +5084,97 @@ fn build_mcp_servers(config: &Config) -> Vec { } env }, - }] + trusted: true, + }]; + + // Append extra MCP servers from BUZZ_ACP_EXTRA_MCP_COMMANDS. + // Each entry is shell-split into command + args using shlex, so quoted + // paths and arguments with spaces are preserved. Malformed entries cause + // startup to fail closed — the error identifies the entry index without + // echoing the command, which may contain an embedded API key. + // Extra servers do not receive Buzz relay credentials or auth tags — + // they are third-party tools, not Buzz-native MCP servers. + let mut seen_names: std::collections::HashSet = + std::collections::HashSet::from_iter([servers[0].name.clone()]); + for (idx, extra) in config.extra_mcp_commands.iter().enumerate() { + let trimmed = extra.trim(); + if trimmed.is_empty() { + continue; + } + let parts = match shlex::split(trimmed) { + Some(p) if !p.is_empty() => p, + Some(_) => continue, + None => { + return Err(ConfigError::ConfigFile(format!( + "BUZZ_ACP_EXTRA_MCP_COMMANDS entry {} has malformed shell quoting; \ + fix the quoting or remove the entry and restart", + idx + 1 + ))); + } + }; + let command = parts[0].clone(); + let args: Vec = parts[1..].to_vec(); + // Derive a name from the executable stem, then disambiguate so + // two wrappers like `npx -y first-mcp` and `npx -y second-mcp` + // don't both become `npx` and trip McpRegistry's duplicate check. + let raw_stem = std::path::Path::new(&command) + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("extra-mcp"); + let base_name = sanitize_mcp_name(raw_stem); + let name = if seen_names.contains(&base_name) { + // Append a numeric suffix until we find a unique name. + let mut i = 2; + loop { + let candidate = format!("{base_name}-{i}"); + if !seen_names.contains(&candidate) { + break candidate; + } + i += 1; + } + } else { + base_name + }; + seen_names.insert(name.clone()); + servers.push(McpServer { + name, + command, + args, + env: vec![], + trusted: false, + }); + } + + Ok(servers) +} + +/// Sanitize a raw executable stem into a name that satisfies the downstream +/// `McpRegistry` validator: ASCII alphanumeric and hyphens only, ≤128 bytes. +/// Non-conforming characters are replaced with hyphens; leading/trailing +/// hyphens are stripped. An empty result falls back to `"extra-mcp"`. +fn sanitize_mcp_name(raw: &str) -> String { + let sanitized: String = raw + .chars() + .map(|c| { + if c.is_ascii_alphanumeric() { + c + } else { + '-' + } + }) + .collect::() + .trim_matches('-') + .to_string(); + let truncated = if sanitized.len() > 128 { + sanitized[..=128].to_string() + } else { + sanitized + }; + if truncated.is_empty() { + "extra-mcp".to_string() + } else { + truncated + } } #[cfg(test)] @@ -6760,6 +6850,7 @@ mod build_mcp_servers_tests { agent_command: "goose".into(), agent_args: vec!["acp".into()], mcp_command: "test-mcp-server".into(), + extra_mcp_commands: vec![], idle_timeout_secs: config::DEFAULT_IDLE_TIMEOUT_SECS, max_turn_duration_secs: config::DEFAULT_MAX_TURN_DURATION_SECS, agents: 1, @@ -6804,7 +6895,7 @@ mod build_mcp_servers_tests { #[test] fn session_new_mcp_server_has_required_fields() { let config = test_config(); - let servers = build_mcp_servers(&config); + let servers = build_mcp_servers(&config).unwrap(); assert_eq!(servers.len(), 1); let server = &servers[0]; assert_eq!(server.name, "test-mcp-server"); @@ -6825,7 +6916,7 @@ mod build_mcp_servers_tests { let _guard = ENV_LOCK.lock().unwrap(); std::env::set_var("BUZZ_AUTH_TAG", "test-attestation-tag"); let config = test_config(); - let servers = build_mcp_servers(&config); + let servers = build_mcp_servers(&config).unwrap(); std::env::remove_var("BUZZ_AUTH_TAG"); let server = &servers[0]; @@ -6842,7 +6933,7 @@ mod build_mcp_servers_tests { let _guard = ENV_LOCK.lock().unwrap(); std::env::set_var("BUZZ_AUTH_TAG", ""); let config = test_config(); - let servers = build_mcp_servers(&config); + let servers = build_mcp_servers(&config).unwrap(); std::env::remove_var("BUZZ_AUTH_TAG"); let server = &servers[0]; @@ -6855,7 +6946,7 @@ mod build_mcp_servers_tests { let _guard = ENV_LOCK.lock().unwrap(); std::env::set_var("BUZZ_ACP_DISPLAY_NAME", "Duncan"); let config = test_config(); - let servers = build_mcp_servers(&config); + let servers = build_mcp_servers(&config).unwrap(); std::env::remove_var("BUZZ_ACP_DISPLAY_NAME"); let entry = servers[0] @@ -6874,7 +6965,7 @@ mod build_mcp_servers_tests { let _guard = ENV_LOCK.lock().unwrap(); std::env::remove_var("BUZZ_ACP_DISPLAY_NAME"); let config = test_config(); - let servers = build_mcp_servers(&config); + let servers = build_mcp_servers(&config).unwrap(); // Absent, not empty-valued: dev-mcp distinguishes the two and only // falls back to the npub when the key is missing or blank. @@ -6892,7 +6983,7 @@ mod build_mcp_servers_tests { let _guard = ENV_LOCK.lock().unwrap(); std::env::set_var("BUZZ_ACP_DISPLAY_NAME", ""); let config = test_config(); - let servers = build_mcp_servers(&config); + let servers = build_mcp_servers(&config).unwrap(); std::env::remove_var("BUZZ_ACP_DISPLAY_NAME"); assert!( @@ -6908,7 +6999,7 @@ mod build_mcp_servers_tests { fn empty_mcp_command_returns_no_servers() { let mut config = test_config(); config.mcp_command = "".into(); - let servers = build_mcp_servers(&config); + let servers = build_mcp_servers(&config).unwrap(); assert!( servers.is_empty(), "empty mcp_command should produce no MCP servers" @@ -6919,7 +7010,7 @@ mod build_mcp_servers_tests { fn absolute_path_mcp_command_uses_file_stem_as_name() { let mut config = test_config(); config.mcp_command = "/opt/bin/my-mcp-server".into(); - let servers = build_mcp_servers(&config); + let servers = build_mcp_servers(&config).unwrap(); assert_eq!(servers.len(), 1); assert_eq!(servers[0].name, "my-mcp-server"); } @@ -6940,13 +7031,159 @@ mod build_mcp_servers_tests { // Confirm a non-empty command with no stem (e.g. just a dot) also falls back. config.mcp_command = ".".into(); - let servers = build_mcp_servers(&config); + let servers = build_mcp_servers(&config).unwrap(); assert_eq!(servers.len(), 1); assert_eq!( servers[0].name, "mcp", "Path::new(\".\").file_stem() is None — should fall back to \"mcp\"" ); } + + #[test] + fn extra_mcp_commands_append_additional_servers() { + let mut config = test_config(); + config.extra_mcp_commands = + vec!["npx -y mcp-remote https://mcp.tavily.com/mcp/?tavilyApiKey=test-key".into()]; + let servers = build_mcp_servers(&config).unwrap(); + assert_eq!(servers.len(), 2, "primary + 1 extra = 2 servers"); + assert_eq!(servers[0].name, "test-mcp-server"); + assert_eq!(servers[1].name, "npx"); + assert_eq!(servers[1].command, "npx"); + assert_eq!( + servers[1].args, + vec![ + "-y", + "mcp-remote", + "https://mcp.tavily.com/mcp/?tavilyApiKey=test-key" + ] + ); + // Extra servers should not receive Buzz relay credentials. + let env_names: Vec<&str> = servers[1].env.iter().map(|e| e.name.as_str()).collect(); + assert!( + !env_names.contains(&"BUZZ_RELAY_URL"), + "extra MCP servers should not get BUZZ_RELAY_URL" + ); + assert!( + !env_names.contains(&"BUZZ_PRIVATE_KEY"), + "extra MCP servers should not get BUZZ_PRIVATE_KEY" + ); + } + + #[test] + fn multiple_extra_mcp_commands_append_in_order() { + let mut config = test_config(); + config.extra_mcp_commands = vec![ + "brave-search-mcp".into(), + "npx -y mcp-remote https://mcp.tavily.com/mcp/".into(), + ]; + let servers = build_mcp_servers(&config).unwrap(); + assert_eq!(servers.len(), 3, "primary + 2 extra = 3 servers"); + assert_eq!(servers[0].name, "test-mcp-server"); + assert_eq!(servers[1].name, "brave-search-mcp"); + assert_eq!(servers[2].name, "npx"); + } + + #[test] + fn empty_extra_mcp_commands_are_skipped() { + let mut config = test_config(); + config.extra_mcp_commands = vec![ + "valid-server".into(), + "".into(), + " ".into(), + "another-server arg1".into(), + ]; + let servers = build_mcp_servers(&config).unwrap(); + assert_eq!( + servers.len(), + 3, + "primary + 2 valid extras (empty and whitespace-only skipped)" + ); + assert_eq!(servers[0].name, "test-mcp-server"); + assert_eq!(servers[1].name, "valid-server"); + assert_eq!(servers[1].args.len(), 0); + assert_eq!(servers[2].name, "another-server"); + assert_eq!(servers[2].args, vec!["arg1"]); + } + + #[test] + fn extra_mcp_commands_with_empty_mcp_command_returns_no_servers() { + let mut config = test_config(); + config.mcp_command = "".into(); + config.extra_mcp_commands = vec!["some-extra-server".into()]; + let servers = build_mcp_servers(&config).unwrap(); + assert!( + servers.is_empty(), + "empty primary mcp_command should still short-circuit even with extras" + ); + } + + #[test] + fn extra_mcp_commands_disambiguate_duplicate_names() { + // Two npx-based wrappers must not both become "npx" — that would + // trip McpRegistry's duplicate-name check at spawn. + let mut config = test_config(); + config.extra_mcp_commands = vec!["npx -y first-mcp".into(), "npx -y second-mcp".into()]; + let servers = build_mcp_servers(&config).unwrap(); + assert_eq!(servers.len(), 3, "primary + 2 extra = 3 servers"); + assert_eq!(servers[1].name, "npx"); + assert_eq!(servers[2].name, "npx-2"); + } + + #[test] + fn extra_mcp_commands_shell_split_quoted_paths() { + // Quoted paths with spaces must be preserved as a single argv element. + let mut config = test_config(); + config.extra_mcp_commands = vec![r#""my server" --port 8080"#.into()]; + let servers = build_mcp_servers(&config).unwrap(); + assert_eq!(servers.len(), 2); + assert_eq!(servers[1].command, "my server"); + assert_eq!(servers[1].args, vec!["--port", "8080"]); + } + + #[test] + fn extra_mcp_commands_fail_closed_on_malformed_quoting() { + // Malformed quoting must fail startup, not silently skip the entry. + // The error must not echo the command (it may contain an API key). + let mut config = test_config(); + config.extra_mcp_commands = vec![ + "valid-server".into(), + "'unmatched-quote".into(), + "another-server".into(), + ]; + let result = build_mcp_servers(&config); + assert!(result.is_err(), "malformed quoting must fail closed"); + let err_msg = format!("{}", result.unwrap_err()); + assert!( + err_msg.contains("entry 2"), + "error should identify the entry index" + ); + assert!( + !err_msg.contains("unmatched-quote"), + "error must not echo the raw command" + ); + } + + #[test] + fn extra_mcp_commands_sanitized_names() { + // Names with underscores, spaces, or punctuation must be sanitized + // to the McpRegistry ASCII alphanumeric/hyphen contract. + let mut config = test_config(); + config.extra_mcp_commands = vec!["my_server --port 8080".into()]; + let servers = build_mcp_servers(&config).unwrap(); + assert_eq!(servers.len(), 2); + assert_eq!(servers[1].name, "my-server"); + } + + #[test] + fn extra_mcp_commands_trusted_flag() { + // The primary server must be trusted; extras must not be. + let mut config = test_config(); + config.extra_mcp_commands = vec!["some-extra-server".into()]; + let servers = build_mcp_servers(&config).unwrap(); + assert_eq!(servers.len(), 2); + assert!(servers[0].trusted, "primary MCP server must be trusted"); + assert!(!servers[1].trusted, "extra MCP servers must not be trusted"); + } } #[cfg(test)] @@ -6984,6 +7221,7 @@ mod error_outcome_emission_tests { agent_command: "true".into(), agent_args: vec![], mcp_command: "test-mcp-server".into(), + extra_mcp_commands: vec![], idle_timeout_secs: config::DEFAULT_IDLE_TIMEOUT_SECS, max_turn_duration_secs: config::DEFAULT_MAX_TURN_DURATION_SECS, agents: 1, diff --git a/crates/buzz-acp/src/pool.rs b/crates/buzz-acp/src/pool.rs index 38749577398..33dd8793331 100644 --- a/crates/buzz-acp/src/pool.rs +++ b/crates/buzz-acp/src/pool.rs @@ -4711,6 +4711,7 @@ mod tests { command: "buzz-dev-mcp".into(), args: vec![], env: vec![], + trusted: true, } } diff --git a/crates/buzz-agent/src/mcp.rs b/crates/buzz-agent/src/mcp.rs index 42c9cc48780..3c5f2617106 100644 --- a/crates/buzz-agent/src/mcp.rs +++ b/crates/buzz-agent/src/mcp.rs @@ -119,6 +119,7 @@ struct ServerSpec { args: Vec, env: Vec<(String, String)>, cwd: String, + trusted: bool, } enum ClientState { @@ -243,6 +244,7 @@ impl McpRegistry { .map(|e| (e.name.clone(), e.value.clone())) .collect(), cwd: cwd.to_owned(), + trusted: s.trusted, }; let (client, pgid, tool_names, raw_tools) = spawn_one(&spec, reg.init_timeout).await?; let server_idx = reg.servers.len(); @@ -735,6 +737,13 @@ async fn spawn_one( cmd.args(&spec.args); cmd.env_clear(); for k in PASSTHROUGH_ENV { + // Withhold Buzz identity credentials from untrusted MCP servers so + // third-party tooling cannot exfiltrate the agent's signing key, + // relay URL, or owner attestation. Only the built-in buzz-dev-mcp + // server (marked `trusted`) receives these. + if !spec.trusted && is_buzz_identity_env(k) { + continue; + } if let Ok(v) = std::env::var(k) { cmd.env(k, v); } @@ -908,6 +917,15 @@ fn valid_name(s: &str) -> bool { .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') } +/// Returns `true` for env vars that carry Buzz identity credentials. +/// These are withheld from untrusted (third-party) MCP server children. +fn is_buzz_identity_env(key: &str) -> bool { + matches!( + key, + "BUZZ_PRIVATE_KEY" | "NOSTR_PRIVATE_KEY" | "BUZZ_RELAY_URL" | "BUZZ_AUTH_TAG" + ) +} + pub(crate) fn truncate_at_boundary(s: &str, max: usize) -> &str { if s.len() <= max { return s; diff --git a/crates/buzz-agent/src/types.rs b/crates/buzz-agent/src/types.rs index 10ac65b46ef..d2edb6bf11c 100644 --- a/crates/buzz-agent/src/types.rs +++ b/crates/buzz-agent/src/types.rs @@ -541,6 +541,13 @@ pub struct McpServerStdio { pub args: Vec, #[serde(default)] pub env: Vec, + /// When `false`, the spawn boundary withholds Buzz identity credentials + /// (`BUZZ_PRIVATE_KEY`, `BUZZ_RELAY_URL`, `BUZZ_AUTH_TAG`) from the child + /// process so third-party MCP servers cannot exfiltrate the agent's + /// signing key or owner attestation. Only the built-in `buzz-dev-mcp` + /// server sets this to `true`. + #[serde(default)] + pub trusted: bool, } #[derive(Debug, Deserialize, Clone)] diff --git a/desktop/src-tauri/src/managed_agents/env_vars/tests.rs b/desktop/src-tauri/src/managed_agents/env_vars/tests.rs index f3de11ad242..c124b6eb841 100644 --- a/desktop/src-tauri/src/managed_agents/env_vars/tests.rs +++ b/desktop/src-tauri/src/managed_agents/env_vars/tests.rs @@ -183,6 +183,7 @@ fn reserved_keys_include_code_execution_surface() { "BUZZ_ACP_AGENT_COMMAND", "BUZZ_ACP_AGENT_ARGS", "BUZZ_ACP_MCP_COMMAND", + "BUZZ_ACP_EXTRA_MCP_COMMANDS", ] { assert!(is_reserved_env_key(key), "{key} should be reserved"); } diff --git a/desktop/src-tauri/src/managed_agents/reserved_env_keys.rs b/desktop/src-tauri/src/managed_agents/reserved_env_keys.rs index afaaa2b4eb3..7235cf8c206 100644 --- a/desktop/src-tauri/src/managed_agents/reserved_env_keys.rs +++ b/desktop/src-tauri/src/managed_agents/reserved_env_keys.rs @@ -41,6 +41,7 @@ pub(crate) const RESERVED_ENV_KEYS: &[&str] = &[ "BUZZ_ACP_AGENT_COMMAND", "BUZZ_ACP_AGENT_ARGS", "BUZZ_ACP_MCP_COMMAND", + "BUZZ_ACP_EXTRA_MCP_COMMANDS", // Control-plane parallelism: the Desktop resolves the effective // worker-pool size (applying any per-harness cap) and writes it into // launch.policy_env. A user-supplied BUZZ_ACP_AGENTS would bypass the