diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index c8d8d1ee8..2fccfb0a9 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -166,9 +166,10 @@ const overrides = new Map([ // setup-mode requirements. The Windows-only requirement and serialization // test add eight lines; split remains queued with the existing file debt. // Windows Doctor install fix: cli_install_commands_windows field added to test stubs. - // team-instructions-first-class: ManagedAgentRecord fixture gains the new - // team_id field (+1 line). - ["src-tauri/src/managed_agents/readiness.rs", 1765], + // `team-instructions-first-class` added `team_id`; MCP configuration added + // the required `mcp_servers` minimal-fixture field. The gate counts the + // trailing newline, so 1,765 physical lines are 1,766 counted lines. + ["src-tauri/src/managed_agents/readiness.rs", 1766], // applyWorkspace reposDir parameter plus the validateReposDir binding, // threaded through Tauri invokes for configurable repos_dir, plus the // harness-persona-sync `harnessOverride` create-input bit — load-bearing @@ -429,7 +430,11 @@ const overrides = new Map([ // is_safe_to_reveal allowlist + baked_env_thinking_effort_is_unmasked test. // +1: doctor-install-reliability: login_hint: None added to goose_runtime test stub. // +1: doctor-install-reliability review fixes: auth_probe_args: None added to stub. - ["src-tauri/src/commands/agent_config.rs", 1021], + // MCP config surface: `resolve_config_surface` receives the effective command + // and exposes the effective `buzz_agent_mcp_servers` merge; coverage pins the + // merge, disabled-mask, and non-buzz-agent cases. The gate counts trailing + // newlines, so 1,139 physical lines are 1,140 counted lines. + ["src-tauri/src/commands/agent_config.rs", 1140], // codex-install-auto-restart review-fixes: should_restart_after_install // takes pid_alive:bool (pure predicate, no OS-dependent call); 3 racy // cache tests replaced with 6 pure availability_drift predicate tests; diff --git a/desktop/src-tauri/src/commands/agent_config.rs b/desktop/src-tauri/src/commands/agent_config.rs index 7b9577ba2..5a250491c 100644 --- a/desktop/src-tauri/src/commands/agent_config.rs +++ b/desktop/src-tauri/src/commands/agent_config.rs @@ -55,6 +55,7 @@ fn resolve_config_surface( mut record: ManagedAgentRecord, personas: &[AgentDefinition], runtime_meta: Option<&KnownAcpRuntime>, + effective_command: &str, session_cache: Option<&SessionConfigCache>, global: &GlobalAgentConfig, ) -> RuntimeConfigSurface { @@ -152,6 +153,19 @@ fn resolve_config_surface( baseline.as_ref().map(|(m, o)| (m.as_str(), o.clone())), ); + if runtime_meta.is_some_and(|m| m.id == "buzz-agent") { + surface.buzz_agent_mcp_servers = crate::managed_agents::effective_buzz_agent_mcp_servers( + &record, + personas, + &global.mcp_servers, + effective_command, + ) + .unwrap_or_else(|error| { + eprintln!("buzz-desktop: invalid persisted MCP server configuration: {error}"); + Vec::new() + }); + } + // Re-tag persona-sourced fields from BuzzExplicit to PersonaDefault. if !had_prompt { retag_persona_default(&mut surface.normalized.system_prompt); @@ -375,6 +389,7 @@ pub async fn get_agent_config_surface( record, &personas, runtime_meta, + &effective_cmd, session_cache.as_ref(), &global, )) @@ -713,6 +728,7 @@ mod tests { record, &personas, Some(goose_runtime()), + "goose", None, &Default::default(), ); @@ -740,6 +756,7 @@ mod tests { record, &personas, Some(goose_runtime()), + "goose", Some(&cache), &Default::default(), ); @@ -768,6 +785,7 @@ mod tests { record, &personas, Some(goose_runtime()), + "goose", Some(&cache), &Default::default(), ); @@ -795,6 +813,7 @@ mod tests { record, &personas, Some(goose_runtime()), + "goose", Some(&cache), &Default::default(), ); @@ -819,6 +838,7 @@ mod tests { record, &personas, Some(goose_runtime()), + "goose", Some(&cache), &Default::default(), ); @@ -855,6 +875,7 @@ mod tests { record, &personas, Some(goose_runtime()), + "goose", Some(&cache), &global, ); @@ -876,6 +897,102 @@ mod tests { ); } + // ── buzz_agent_mcp_servers surface tests ──────────────────────────────── + + fn mcp_server( + name: &str, + command: &str, + enabled: bool, + ) -> crate::managed_agents::McpServerConfig { + crate::managed_agents::McpServerConfig { + name: name.to_string(), + command: command.to_string(), + args: vec![], + env: vec![], + enabled, + } + } + + /// The buzz-agent runtime surfaces its effective-merged MCP servers onto + /// `RuntimeConfigSurface.buzz_agent_mcp_servers`, matching what + /// `effective_buzz_agent_mcp_servers` (the spawn-time source of truth) + /// would compute for the same record/persona/global layers. + #[test] + fn buzz_agent_runtime_surfaces_effective_merged_mcp_servers() { + let mut record = agent_record(); + record.persona_id = None; + record.mcp_servers = vec![mcp_server("agent-server", "agent-cmd", true)]; + let personas: Vec = vec![]; + let global = crate::managed_agents::GlobalAgentConfig { + mcp_servers: vec![mcp_server("global-server", "global-cmd", true)], + ..Default::default() + }; + + let surface = resolve_config_surface( + record, + &personas, + known_acp_runtime("buzz-agent"), + "buzz-agent", + None, + &global, + ); + + let mut names: Vec<&str> = surface + .buzz_agent_mcp_servers + .iter() + .map(|s| s.name.as_str()) + .collect(); + names.sort_unstable(); + assert_eq!(names, vec!["agent-server", "global-server"]); + } + + /// A disabled agent-layer entry masks a same-named global entry — the + /// merged effective list must NOT include it. Fails against a variant + /// that surfaces the raw record layer instead of the merged result. + #[test] + fn buzz_agent_disabled_override_masks_inherited_server_in_surface() { + let mut record = agent_record(); + record.persona_id = None; + record.mcp_servers = vec![mcp_server("shared", "", false)]; + let personas: Vec = vec![]; + let global = crate::managed_agents::GlobalAgentConfig { + mcp_servers: vec![mcp_server("shared", "global-cmd", true)], + ..Default::default() + }; + + let surface = resolve_config_surface( + record, + &personas, + known_acp_runtime("buzz-agent"), + "buzz-agent", + None, + &global, + ); + + assert!(surface.buzz_agent_mcp_servers.is_empty()); + } + + /// Non-buzz-agent runtimes (e.g. goose) must never populate + /// `buzz_agent_mcp_servers` — they surface their servers via `extensions` + /// instead. Fails against a variant that populates the field unconditionally. + #[test] + fn non_buzz_agent_runtime_leaves_mcp_surface_empty() { + let mut record = agent_record(); + record.mcp_servers = vec![mcp_server("agent-server", "agent-cmd", true)]; + let personas = vec![persona_with_model("persona-model")]; + + let surface = resolve_config_surface( + record, + &personas, + Some(goose_runtime()), + "goose", + None, + &Default::default(), + ); + + assert!(surface.buzz_agent_mcp_servers.is_empty()); + } + // ── get_baked_build_env / is_secret_key tests ────────────────────────── /// Build a `BakedEnvEntry` vec from a synthetic map, mirroring what diff --git a/desktop/src-tauri/src/managed_agents/config_bridge/reader.rs b/desktop/src-tauri/src/managed_agents/config_bridge/reader.rs index 372d2cfde..be6393e40 100644 --- a/desktop/src-tauri/src/managed_agents/config_bridge/reader.rs +++ b/desktop/src-tauri/src/managed_agents/config_bridge/reader.rs @@ -209,6 +209,10 @@ pub(crate) fn read_config_surface( normalized, advanced, extensions, + // Populated by `resolve_config_surface` for the buzz-agent runtime only; + // this reader has no access to the global/persona MCP layers needed to + // compute the effective merge. + buzz_agent_mcp_servers: Vec::new(), sources, } } diff --git a/desktop/src-tauri/src/managed_agents/config_bridge/types.rs b/desktop/src-tauri/src/managed_agents/config_bridge/types.rs index 15ccb718e..a2a6ee7d9 100644 --- a/desktop/src-tauri/src/managed_agents/config_bridge/types.rs +++ b/desktop/src-tauri/src/managed_agents/config_bridge/types.rs @@ -2,6 +2,8 @@ use std::collections::BTreeMap; use serde::{Deserialize, Serialize}; +use crate::managed_agents::types::McpServerConfig; + /// Where a config value came from — determines precedence and UI annotations. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -136,6 +138,12 @@ pub struct RuntimeConfigSurface { pub normalized: NormalizedConfig, pub advanced: Vec, pub extensions: Vec, + /// Effective (global < definition < agent merged, enabled-only) buzz-agent + /// MCP servers — "what runs." Populated only for the `buzz-agent` runtime; + /// empty for every other runtime, which surface their servers via + /// `extensions` instead. + #[serde(default)] + pub buzz_agent_mcp_servers: Vec, pub sources: ConfigSourceReport, }