fix(desktop): stop an agent inheriting the developer's provider

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 <michael.neale@gmail.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Michael Neale
2026-08-12 13:57:21 +10:00
parent 42fb755040
commit d4db850b3a
2 changed files with 63 additions and 4 deletions

View File

@@ -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.

View File

@@ -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,