fix(buzz-agent): the agent's configured provider must beat an inherited one

An agent configured for OpenAI sent its OpenAI model to Anthropic and
404'd every turn (llm model not found: gpt-5.6-sol at
api.anthropic.com). Found in live testing, not in a test.

project_goose_env used set_if_absent, so an inherited GOOSE_PROVIDER
won over the agent's own BUZZ_AGENT_PROVIDER. That looks defensive but
inverts the intended precedence: BUZZ_AGENT_PROVIDER/MODEL are not
ambient config, they are derived by the desktop from the agent record's
structured provider/model fields at spawn time, and the desktop
deliberately refuses to persist GOOSE_PROVIDER/GOOSE_MODEL in an
agent's env so they cannot shadow those fields
(env_vars.rs:DERIVED_PROVIDER_MODEL_ENV_KEYS). The subprocess still
inherits the desktop's environment, though, and anyone with goose
installed exports GOOSE_PROVIDER from their login shell.

The failure is silent in the worst way: the agent's settings read
correctly in the UI while its traffic goes to another provider.

Set both unconditionally when the agent has them, and likewise map
OPENAI_COMPAT_API_KEY over OPENAI_API_KEY: an inherited OPENAI_API_KEY
would otherwise authenticate and bill the agent as whoever that key
belongs to. With no BUZZ_AGENT_* value there is nothing to override
with, so an ambient GOOSE_* is still honoured.

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 12:57:32 +10:00
parent 69c6091216
commit 42fb755040

View File

@@ -172,22 +172,44 @@ impl Config {
///
/// Mirrors `goose_env.rs` from PR #1526.
fn project_goose_env() {
// Provider: Buzz's `openai-compat` and `relay-mesh` are both
// OpenAI-wire-compatible, and Goose knows them as plain `openai`.
// Provider and model **override** any ambient `GOOSE_*`, they do not
// defer to it.
//
// `BUZZ_AGENT_PROVIDER` / `BUZZ_AGENT_MODEL` are not ambient config:
// the desktop derives them from the agent record's structured
// provider/model fields at spawn time, and deliberately refuses to
// persist `GOOSE_PROVIDER` / `GOOSE_MODEL` in an agent's own env so
// they cannot shadow those fields
// (`managed_agents/env_vars.rs:DERIVED_PROVIDER_MODEL_ENV_KEYS`).
//
// But the agent subprocess still inherits the desktop's environment,
// and the desktop inherits the user's login shell. Anyone with goose
// installed exports `GOOSE_PROVIDER`. With `set_if_absent` that
// inherited value won, so an agent configured for OpenAI sent its
// OpenAI model to Anthropic and got `404 model: gpt-…` on every turn
// — the agent looked broken while its settings looked correct.
// Observed in live testing, not hypothetical.
if let Some(provider) = env_str("BUZZ_AGENT_PROVIDER") {
set_if_absent("GOOSE_PROVIDER", goose_provider_name(&provider));
std::env::set_var("GOOSE_PROVIDER", goose_provider_name(&provider));
}
if let Some(model) = env_str("BUZZ_AGENT_MODEL") {
set_if_absent("GOOSE_MODEL", &model);
std::env::set_var("GOOSE_MODEL", &model);
}
// Key/base-url aliasing: native Goose names win if already present.
// Credentials override too, and for the same reason as the provider:
// the desktop hands the agent its configured key as
// `OPENAI_COMPAT_API_KEY`, while `OPENAI_API_KEY` is very commonly
// exported in a developer's login shell and inherited here. Deferring
// to the inherited one would bill and authenticate the agent as
// whoever that key belongs to rather than as configured — the same
// silent-wrong-target failure as the provider, with a credential
// instead of a hostname.
if let Some(key) = env_str("OPENAI_COMPAT_API_KEY") {
set_if_absent("OPENAI_API_KEY", &key);
std::env::set_var("OPENAI_API_KEY", &key);
}
if let Some(base) = env_str("OPENAI_COMPAT_BASE_URL") {
set_if_absent("OPENAI_BASE_URL", &base);
std::env::set_var("OPENAI_BASE_URL", &base);
}
if let Some(effort) = env_str("BUZZ_AGENT_THINKING_EFFORT") {
@@ -231,6 +253,53 @@ mod tests {
std::env::remove_var("BUZZ_TEST_MISSING");
}
/// The regression that made a live OpenAI agent 404 against Anthropic.
///
/// The desktop refuses to persist `GOOSE_PROVIDER` in an agent's env
/// precisely so it cannot shadow the agent's configured provider — but the
/// subprocess still inherits it from the user's login shell if goose is
/// installed. Deferring to the inherited value silently reroutes the
/// agent's traffic to the wrong provider while its settings still read
/// correctly.
#[test]
fn the_agents_configured_provider_beats_an_inherited_goose_provider() {
// One test: `project_goose_env` mutates process-global env, so
// splitting these would race the rest of the suite.
let restore: Vec<(&str, Option<String>)> = [
"BUZZ_AGENT_PROVIDER",
"BUZZ_AGENT_MODEL",
"GOOSE_PROVIDER",
"GOOSE_MODEL",
]
.iter()
.map(|key| (*key, std::env::var(key).ok()))
.collect();
std::env::set_var("GOOSE_PROVIDER", "anthropic");
std::env::set_var("GOOSE_MODEL", "claude-opus-5");
std::env::set_var("BUZZ_AGENT_PROVIDER", "openai");
std::env::set_var("BUZZ_AGENT_MODEL", "gpt-5.6-sol");
Config::project_goose_env();
assert_eq!(std::env::var("GOOSE_PROVIDER").unwrap(), "openai");
assert_eq!(std::env::var("GOOSE_MODEL").unwrap(), "gpt-5.6-sol");
// With no agent-configured provider there is nothing to override with,
// so an ambient value is still the best answer available.
std::env::remove_var("BUZZ_AGENT_PROVIDER");
std::env::set_var("GOOSE_PROVIDER", "anthropic");
Config::project_goose_env();
assert_eq!(std::env::var("GOOSE_PROVIDER").unwrap(), "anthropic");
for (key, value) in restore {
match value {
Some(value) => std::env::set_var(key, value),
None => std::env::remove_var(key),
}
}
}
#[test]
fn databricks_v2_hyphen_is_aliased_for_goose() {
// The desktop persists "databricks-v2" (agent_models.rs:757) but goose