diff --git a/desktop/src-tauri/src/managed_agents/global_config/mod.rs b/desktop/src-tauri/src/managed_agents/global_config/mod.rs index 445e89779..be3ec97b6 100644 --- a/desktop/src-tauri/src/managed_agents/global_config/mod.rs +++ b/desktop/src-tauri/src/managed_agents/global_config/mod.rs @@ -214,6 +214,8 @@ pub fn save_global_agent_config(app: &AppHandle, config: &GlobalAgentConfig) -> /// command is pinned on the record, and provider/model defaults belonging to a /// different preferred runtime must not cross the harness boundary. Explicitly /// configured definitions and standalone agents keep normal global inheritance. +/// Configs written before `preferred_runtime` existed implicitly belong to +/// Buzz Agent. pub(crate) fn global_model_provider_for_record<'a>( record: &ManagedAgentRecord, personas: &[AgentDefinition], @@ -235,17 +237,17 @@ pub(crate) fn global_model_provider_for_record<'a>( return global_values; } - let Some(preferred_runtime) = global + let preferred_runtime = global .preferred_runtime .as_deref() .and_then(crate::managed_agents::known_acp_runtime) - else { - return global_values; - }; + .or_else(|| crate::managed_agents::known_acp_runtime("buzz-agent")); let selected_command = crate::managed_agents::record_agent_command(record, personas); let selected_runtime = crate::managed_agents::known_acp_runtime(&selected_command); - if selected_runtime.is_some_and(|selected| preferred_runtime.id == selected.id) { + if selected_runtime.is_some_and(|selected| { + preferred_runtime.is_some_and(|preferred| preferred.id == selected.id) + }) { global_values } else { (None, None) diff --git a/desktop/src-tauri/src/managed_agents/global_config/tests.rs b/desktop/src-tauri/src/managed_agents/global_config/tests.rs index a5e6d35d9..8dc2296f4 100644 --- a/desktop/src-tauri/src/managed_agents/global_config/tests.rs +++ b/desktop/src-tauri/src/managed_agents/global_config/tests.rs @@ -491,6 +491,25 @@ fn runtime_fallback_does_not_inherit_defaults_from_a_different_preferred_runtime ); } +#[test] +fn legacy_runtime_fallback_does_not_inherit_implicit_buzz_agent_defaults() { + let mut record = bare_record(); + record.persona_id = Some("p1".to_string()); + record.agent_command_override = Some("goose".to_string()); + let personas = vec![persona("p1", None, None)]; + let global = GlobalAgentConfig { + model: Some("auto".to_string()), + provider: Some("relay-mesh".to_string()), + preferred_runtime: None, + ..Default::default() + }; + + assert_eq!( + resolve_effective_model_provider(&record, &personas, &global), + (None, None) + ); +} + #[test] fn default_command_fallback_does_not_inherit_defaults_from_another_preferred_runtime() { let mut record = bare_record(); diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index 2c41a4787..33b23a530 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -96,6 +96,9 @@ with a TypeScript lookup table or an id comparison in a component. editor persists its visible fallback on the next save. Its dependent provider/model defaults are also ignored for new implicit fallback agents, without changing the persisted configuration used by existing agents. + Legacy global defaults with no saved preferred runtime are treated as + Buzz Agent-owned for this masking boundary, so hiding Buzz Agent cannot + leak its provider/model into another implicit fallback. Create-mode dialogs use the implicit masked config; existing definition and instance edit dialogs use the raw persisted config. `resolvePersonaRuntime` is the shared visibility boundary for every diff --git a/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs b/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs index 9bf801493..16ec7ff4c 100644 --- a/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs +++ b/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs @@ -118,3 +118,19 @@ test("a disabled saved runtime and its dependent defaults are masked", () => { }); assert.equal(maskDisabledAcpRuntimePreference(config, ["claude"]), config); }); + +test("legacy defaults without a saved runtime are owned by buzz-agent", () => { + const config = { + env_vars: {}, + provider: "relay-mesh", + model: "auto", + preferred_runtime: null, + }; + + assert.deepEqual(maskDisabledAcpRuntimePreference(config, ["buzz-agent"]), { + ...config, + provider: null, + model: null, + }); + assert.equal(maskDisabledAcpRuntimePreference(config, ["goose"]), config); +}); diff --git a/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts b/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts index 040831c28..45e53e614 100644 --- a/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts +++ b/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts @@ -166,7 +166,8 @@ export function visibleAcpRuntimeSeedForCreate( * The persisted config is left untouched until the user next saves defaults; * existing agents keep their configuration while new consumers immediately * fall back through the normal runtime selection path without carrying a - * provider or model selected for the hidden harness. + * provider or model selected for the hidden harness. Configs written before + * preferred_runtime existed implicitly belong to Buzz Agent. */ export function maskDisabledAcpRuntimePreference< T extends { @@ -176,11 +177,11 @@ export function maskDisabledAcpRuntimePreference< }, >(config: T, disabledRuntimeIds: readonly string[]): T { const preferredRuntime = config.preferred_runtime; + const implicitLegacyRuntime = preferredRuntime?.trim() || "buzz-agent"; if ( - !preferredRuntime || !disabledRuntimeIds .map(normalizeRuntimeId) - .includes(normalizeRuntimeId(preferredRuntime)) + .includes(normalizeRuntimeId(implicitLegacyRuntime)) ) { return config; } diff --git a/desktop/src/features/onboarding/welcomeKickoff.test.mjs b/desktop/src/features/onboarding/welcomeKickoff.test.mjs index be09d82ad..c21641b12 100644 --- a/desktop/src/features/onboarding/welcomeKickoff.test.mjs +++ b/desktop/src/features/onboarding/welcomeKickoff.test.mjs @@ -57,6 +57,40 @@ test("welcome readiness ignores a hidden logged-in runtime", () => { ); }); +test("welcome readiness evaluates the runtime selected by the visible fallback", () => { + const runtimes = [ + { + id: "buzz-agent", + label: "Buzz Agent", + availability: "available", + authStatus: { status: "not_applicable" }, + }, + { + id: "goose", + label: "Goose", + availability: "available", + authStatus: { status: "not_applicable" }, + }, + { + id: "claude", + label: "Claude", + availability: "available", + authStatus: { status: "logged_in" }, + }, + ]; + const globalConfig = { + env_vars: {}, + provider: null, + model: null, + preferred_runtime: "buzz-agent", + }; + + assert.deepEqual( + resolveWelcomeAgentReadiness(runtimes, globalConfig, ["buzz-agent"]), + { ready: false }, + ); +}); + test("welcome provisioning requires an enabled available runtime", () => { const runtimes = [ { diff --git a/desktop/src/features/onboarding/welcomeKickoff.ts b/desktop/src/features/onboarding/welcomeKickoff.ts index bba48bf0d..fad049eeb 100644 --- a/desktop/src/features/onboarding/welcomeKickoff.ts +++ b/desktop/src/features/onboarding/welcomeKickoff.ts @@ -9,6 +9,7 @@ import { filterEnabledAcpRuntimes, useDisabledAcpRuntimeIds, } from "@/features/agents/lib/runtimeVisibilityPreference"; +import { getDefaultPersonaRuntime } from "@/features/agents/lib/resolvePersonaRuntime"; import { clearActiveTurnsForAgentOnStop } from "@/features/agents/managedAgentRuntimeHooks"; import { useImplicitGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig"; import { useCommunities } from "@/features/communities/useCommunities"; @@ -61,9 +62,23 @@ export function resolveWelcomeAgentReadiness( globalConfig: GlobalAgentConfig, disabledRuntimeIds: readonly string[], ) { + const visibleRuntimes = filterEnabledAcpRuntimes( + runtimes, + disabledRuntimeIds, + ); + const selectedRuntime = getDefaultPersonaRuntime( + visibleRuntimes, + globalConfig.preferred_runtime, + ); + if (!selectedRuntime) return { ready: false } as const; + return resolveAgentReadiness( - filterEnabledAcpRuntimes(runtimes, disabledRuntimeIds), - globalConfig, + visibleRuntimes, + { + ...globalConfig, + preferred_runtime: selectedRuntime.id, + }, + "preferred", ); }