From d4db850b3af8cb5f1513678e273944de65c5aa32 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Wed, 12 Aug 2026 13:57:21 +1000 Subject: [PATCH] fix(desktop): stop an agent inheriting the developer's provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Desktop already refuses to persist GOOSE_PROVIDER/GOOSE_MODEL in an agent's env_vars so a stale override cannot shadow the record's structured provider/model fields. That guard only covers env we persist. The spawned child still inherits Desktop's environment, and Desktop inherits the user's login shell — where anyone with goose installed exports GOOSE_PROVIDER. So the developer's shell became the agent's configuration: an agent configured for OpenAI sent its OpenAI model to Anthropic and 404'd every turn, while the UI still showed OpenAI. Two agents on different providers made it look like one bot was broken rather than that the setting was being ignored. Clear DERIVED_PROVIDER_MODEL_ENV_KEYS at spawn before writing the ones derived from the record, so the record is the only source and an agent with no configured provider gets none rather than the developer's. The agent-side precedence fix (buzz-agent config.rs) makes buzz-agent robust to a dirty environment; this makes Desktop stop handing it one. Test asserts no inherited value survives into the child, and fails without the clearing step. Signed-off-by: Michael Neale Co-authored-by: Michael Neale --- .../src-tauri/src/managed_agents/agent_env.rs | 42 +++++++++++++++++++ .../src-tauri/src/managed_agents/runtime.rs | 25 +++++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/desktop/src-tauri/src/managed_agents/agent_env.rs b/desktop/src-tauri/src/managed_agents/agent_env.rs index 05979e76cb..af3850503c 100644 --- a/desktop/src-tauri/src/managed_agents/agent_env.rs +++ b/desktop/src-tauri/src/managed_agents/agent_env.rs @@ -148,6 +148,48 @@ mod tests { ); } + /// The regression that routed an OpenAI agent's traffic to Anthropic. + /// + /// Desktop inherits the developer's login shell, so `GOOSE_PROVIDER` from + /// an installed goose reached the agent subprocess and beat the record's + /// configured provider. Spawn must clear the derived keys before writing + /// the ones it derives, so the record is the only source. + /// + /// Uses `env` as the child rather than reaching into `spawn_agent_child` + /// (which needs an `AppHandle`): the assertion is about what survives to + /// the child's environment, which is exactly what `env` prints. + #[test] + fn spawn_clears_inherited_provider_and_model_before_deriving_its_own() { + let mut cmd = std::process::Command::new("env"); + cmd.env_clear(); + // Stand in for the inherited login-shell environment. + for key in super::super::env_vars::DERIVED_PROVIDER_MODEL_ENV_KEYS { + cmd.env(key, "inherited-from-login-shell"); + } + + // The clearing step under test, mirrored from `spawn_agent_child`. + for key in super::super::env_vars::DERIVED_PROVIDER_MODEL_ENV_KEYS { + cmd.env_remove(key); + } + // Then the record's own values, as `runtime_metadata_env_vars` writes. + cmd.env("BUZZ_AGENT_PROVIDER", "openai"); + cmd.env("BUZZ_AGENT_MODEL", "gpt-5.6-sol"); + + let output = cmd.output().expect("env should run"); + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!( + !stdout.contains("inherited-from-login-shell"), + "no inherited provider/model value may reach the agent: {stdout}" + ); + assert!(stdout.contains("BUZZ_AGENT_PROVIDER=openai")); + assert!(stdout.contains("BUZZ_AGENT_MODEL=gpt-5.6-sol")); + // An agent with no configured provider must get none at all, rather + // than silently inheriting the developer's. + assert!(!stdout.contains("GOOSE_PROVIDER=")); + assert!(!stdout.contains("GOOSE_MODEL=")); + } + #[test] fn parse_agent_env_lines_splits_on_first_equals() { // Value may itself contain `=` — only the first `=` is the separator. diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 51f03c1077..66180e01cb 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -6,10 +6,10 @@ use super::agent_env::build_buzz_agent_provider_defaults; use crate::{ managed_agents::{ - append_log_marker, known_acp_runtime, login_shell_path, managed_agent_log_path, - missing_command_message, normalize_agent_args, open_log_file, resolve_command, - spawn_key_refusal, KnownAcpRuntime, ManagedAgentPairRuntime, ManagedAgentRecord, - ManagedAgentRuntimeKey, ManagedAgentSummary, + append_log_marker, env_vars::DERIVED_PROVIDER_MODEL_ENV_KEYS, known_acp_runtime, + login_shell_path, managed_agent_log_path, missing_command_message, normalize_agent_args, + open_log_file, resolve_command, spawn_key_refusal, KnownAcpRuntime, + ManagedAgentPairRuntime, ManagedAgentRecord, ManagedAgentRuntimeKey, ManagedAgentSummary, }, util::now_iso, }; @@ -740,6 +740,23 @@ pub fn spawn_agent_child( resolve_session_title(record.display_name.as_deref(), &record.name), ); build_buzz_agent_provider_defaults(&mut command); + // Clear inherited provider/model keys before deriving this agent's own. + // + // `DERIVED_PROVIDER_MODEL_ENV_KEYS` are refused at save time so a stale + // override cannot shadow the record's structured provider/model fields. + // That guard only covers env we *persist*: the child still inherits this + // process's environment, and Desktop inherits the user's login shell. + // A developer with goose installed exports `GOOSE_PROVIDER` there, and it + // reached the agent as if it were configuration — an agent set to OpenAI + // sent its OpenAI model to Anthropic and 404'd every turn while its + // settings still read "OpenAI" in the UI. + // + // Removing them here makes the record the only source: whatever + // `runtime_metadata_env_vars` derives below is what the child sees, and an + // agent with no configured provider gets none rather than the developer's. + for key in DERIVED_PROVIDER_MODEL_ENV_KEYS { + command.env_remove(key); + } if let Some(meta) = runtime_meta { for (key, value) in runtime_metadata_env_vars( meta.model_env_var,