From 2a7ab3b542e94b79cf86dd7da05f0b502c834cbe Mon Sep 17 00:00:00 2001 From: kenny lopez Date: Sun, 26 Jul 2026 10:49:02 +0100 Subject: [PATCH] Mask hidden harness effort defaults Signed-off-by: kenny lopez --- .../src-tauri/src/commands/agents_deploy.rs | 5 +- .../src/managed_agents/global_config/mod.rs | 57 ++++++++++++++----- .../src/managed_agents/global_config/tests.rs | 16 +++++- desktop/src-tauri/src/managed_agents/mod.rs | 5 +- .../src-tauri/src/managed_agents/readiness.rs | 4 +- desktop/src/features/agents/AGENTS.md | 5 +- .../lib/runtimeVisibilityPreference.test.mjs | 12 +++- .../agents/lib/runtimeVisibilityPreference.ts | 7 ++- .../features/agents/ui/agentConfigOptions.tsx | 7 +-- .../src/features/agents/ui/buzzAgentConfig.ts | 9 +++ desktop/src/shared/api/types.ts | 2 +- 11 files changed, 95 insertions(+), 34 deletions(-) diff --git a/desktop/src-tauri/src/commands/agents_deploy.rs b/desktop/src-tauri/src/commands/agents_deploy.rs index af785711d..6faa390f8 100644 --- a/desktop/src-tauri/src/commands/agents_deploy.rs +++ b/desktop/src-tauri/src/commands/agents_deploy.rs @@ -69,7 +69,9 @@ pub(super) fn build_deploy_payload( // on key collision). Without this, provider-backed agents wouldn't receive // credentials saved on the persona or the agent itself. let global_config = crate::managed_agents::load_global_agent_config(app).unwrap_or_default(); - let global_env = global_config.env_vars.clone(); + let personas = load_personas(app).unwrap_or_default(); + let global_env = + crate::managed_agents::global_env_vars_for_record(record, &personas, &global_config); let persona_env = crate::managed_agents::resolve_persona_env(app, record.persona_id.as_deref())?; // Merge: global < persona (persona wins over global). @@ -78,7 +80,6 @@ pub(super) fn build_deploy_payload( let merged_env = crate::managed_agents::merged_user_env(&global_persona_merged, &record.env_vars); - let personas = load_personas(app).unwrap_or_default(); let cfg = crate::managed_agents::effective_config::resolve_effective_config( record, &personas, 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 334fc06ab..ac3ff9429 100644 --- a/desktop/src-tauri/src/managed_agents/global_config/mod.rs +++ b/desktop/src-tauri/src/managed_agents/global_config/mod.rs @@ -33,6 +33,8 @@ use crate::managed_agents::env_vars::{ use crate::managed_agents::storage::{atomic_write_json_restricted, managed_agents_base_dir}; use crate::managed_agents::types::{AgentDefinition, ManagedAgentRecord}; +const HARNESS_DEPENDENT_GLOBAL_ENV_KEYS: &[&str] = &["BUZZ_AGENT_THINKING_EFFORT"]; + /// The global agent configuration record. /// /// Shape mirrors the per-agent/persona trio (`env_vars` + `provider` + `model`) @@ -45,11 +47,12 @@ use crate::managed_agents::types::{AgentDefinition, ManagedAgentRecord}; /// consulted); for a definition-less instance, instance → global. #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct GlobalAgentConfig { - /// Global env vars injected into ALL agents unconditionally. + /// Global env vars injected below persona and per-agent overrides. /// /// Lowest user-settable layer — per-agent and persona values win on any - /// key collision. Reserved and derived keys are rejected at save time and - /// stripped at spawn time. + /// key collision. Harness-dependent effort is masked when a runtime-less + /// definition falls back away from the preferred runtime. Reserved and + /// derived keys are rejected at save time and stripped at spawn time. #[serde(default)] pub env_vars: BTreeMap, @@ -207,11 +210,11 @@ pub fn save_global_agent_config(app: &AppHandle, config: &GlobalAgentConfig) -> atomic_write_json_restricted(&path, &payload) } -/// Return the global provider/model values that are safe for this record. +/// Whether harness-dependent global defaults are safe for this record. /// /// A runtime-less definition can be started on a fallback runtime when its /// saved global preference is hidden or unavailable. In that case the selected -/// command is stored as the record snapshot, and provider/model defaults +/// command is stored as the record snapshot, and harness-dependent defaults /// belonging to a different preferred runtime must not cross the harness /// boundary. A non-empty `agent_command_override` keeps normal global /// inheritance only when it represents an explicit harness selection. An @@ -220,14 +223,13 @@ pub fn save_global_agent_config(app: &AppHandle, config: &GlobalAgentConfig) -> /// harness. 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>( +fn global_harness_defaults_apply_to_record( record: &ManagedAgentRecord, personas: &[AgentDefinition], - global: &'a GlobalAgentConfig, -) -> (Option<&'a str>, Option<&'a str>) { - let global_values = (global.model.as_deref(), global.provider.as_deref()); + global: &GlobalAgentConfig, +) -> bool { if record.persona_id.is_none() { - return global_values; + return true; } let definition_runtime = record.runtime.as_deref().or_else(|| { @@ -238,7 +240,7 @@ pub(crate) fn global_model_provider_for_record<'a>( .and_then(|persona| persona.runtime.as_deref()) }); if definition_runtime.is_some_and(|runtime| !runtime.trim().is_empty()) { - return global_values; + return true; } if !record.agent_command_override_is_implicit @@ -247,7 +249,7 @@ pub(crate) fn global_model_provider_for_record<'a>( .as_deref() .is_some_and(|command| !command.trim().is_empty()) { - return global_values; + return true; } let preferred_runtime = global @@ -258,15 +260,40 @@ pub(crate) fn global_model_provider_for_record<'a>( 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| { + selected_runtime.is_some_and(|selected| { preferred_runtime.is_some_and(|preferred| preferred.id == selected.id) - }) { - global_values + }) +} + +/// Return the global provider/model values that are safe for this record. +pub(crate) fn global_model_provider_for_record<'a>( + record: &ManagedAgentRecord, + personas: &[AgentDefinition], + global: &'a GlobalAgentConfig, +) -> (Option<&'a str>, Option<&'a str>) { + if global_harness_defaults_apply_to_record(record, personas, global) { + (global.model.as_deref(), global.provider.as_deref()) } else { (None, None) } } +/// Return global env vars with harness-dependent defaults masked when an +/// implicit fallback crosses the preferred-runtime boundary. +pub(crate) fn global_env_vars_for_record( + record: &ManagedAgentRecord, + personas: &[AgentDefinition], + global: &GlobalAgentConfig, +) -> BTreeMap { + let mut env_vars = global.env_vars.clone(); + if !global_harness_defaults_apply_to_record(record, personas, global) { + for key in HARNESS_DEPENDENT_GLOBAL_ENV_KEYS { + env_vars.remove(*key); + } + } + env_vars +} + /// Resolve the effective model and provider for an agent. /// /// Delegates to `effective_config::resolve_effective_config` which enforces 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 4a519db49..cd9768d56 100644 --- a/desktop/src-tauri/src/managed_agents/global_config/tests.rs +++ b/desktop/src-tauri/src/managed_agents/global_config/tests.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; use super::{ - normalize_global_config_fields, resolve_effective_model_provider, strip_empty_env_vars, - validate_global_config, GlobalAgentConfig, + global_env_vars_for_record, normalize_global_config_fields, resolve_effective_model_provider, + strip_empty_env_vars, validate_global_config, GlobalAgentConfig, }; use crate::managed_agents::{AgentDefinition, BackendKind, ManagedAgentRecord, RespondTo}; @@ -494,6 +494,10 @@ fn explicit_harness_override_keeps_defaults_from_a_different_preferred_runtime() global_env_vars_for_record(&record, &personas, &global), global.env_vars ); + assert_eq!( + global_env_vars_for_record(&record, &personas, &global), + global.env_vars + ); } #[test] @@ -505,6 +509,10 @@ fn implicit_fallback_override_masks_defaults_from_a_different_preferred_runtime( record.agent_command_override_is_implicit = true; let personas = vec![persona("p1", None, None)]; let global = GlobalAgentConfig { + env_vars: BTreeMap::from([ + ("BUZZ_AGENT_THINKING_EFFORT".to_string(), "high".to_string()), + ("SHARED_API_KEY".to_string(), "kept".to_string()), + ]), model: Some("auto".to_string()), provider: Some("relay-mesh".to_string()), preferred_runtime: Some("buzz-agent".to_string()), @@ -515,6 +523,10 @@ fn implicit_fallback_override_masks_defaults_from_a_different_preferred_runtime( resolve_effective_model_provider(&record, &personas, &global), (None, None) ); + assert_eq!( + global_env_vars_for_record(&record, &personas, &global), + BTreeMap::from([("SHARED_API_KEY".to_string(), "kept".to_string())]) + ); } #[test] diff --git a/desktop/src-tauri/src/managed_agents/mod.rs b/desktop/src-tauri/src/managed_agents/mod.rs index 105f9c191..6dda8c6b5 100644 --- a/desktop/src-tauri/src/managed_agents/mod.rs +++ b/desktop/src-tauri/src/managed_agents/mod.rs @@ -52,8 +52,9 @@ pub use env_vars::*; pub(crate) use git_bash::git_bash_available; pub(crate) use git_bash::{discover_git_bash, GitBashPrerequisite}; pub(crate) use global_config::{ - global_model_provider_for_record, load_global_agent_config, resolve_effective_model_provider, - save_global_agent_config, validate_global_config, GlobalAgentConfig, + global_env_vars_for_record, global_model_provider_for_record, load_global_agent_config, + resolve_effective_model_provider, save_global_agent_config, validate_global_config, + GlobalAgentConfig, }; pub(crate) use managed_node_paths::*; pub use nest::*; diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index a1b1b31a5..e2145caba 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -256,7 +256,9 @@ fn resolve_effective_agent_env_with_def( // Injected before persona/agent so per-agent values win on collision. // `merged_user_env` with an empty "lower" map applies reserved/malformed-key // filtering to the global map for free. - let global_env = merged_user_env(&BTreeMap::new(), &global.env_vars); + let applicable_global_env = + super::global_config::global_env_vars_for_record(record, personas, global); + let global_env = merged_user_env(&BTreeMap::new(), &applicable_global_env); env.extend(global_env); // Layer 3b: merged user env — live persona env under the record's own diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index e88f92042..3e1aa8aa8 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -94,8 +94,9 @@ with a TypeScript lookup table or an id comparison in a component. agent that already uses it. If the disabled runtime was the saved global default, consumers immediately ignore that preference and the defaults 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. + provider/model/effort 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. A persisted diff --git a/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs b/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs index 16ec7ff4c..10aa7f22e 100644 --- a/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs +++ b/desktop/src/features/agents/lib/runtimeVisibilityPreference.test.mjs @@ -104,7 +104,10 @@ test("stored runtime visibility is read from the versioned device key", () => { test("a disabled saved runtime and its dependent defaults are masked", () => { const config = { - env_vars: {}, + env_vars: { + BUZZ_AGENT_THINKING_EFFORT: "high", + SHARED_API_KEY: "kept", + }, provider: "relay-mesh", model: "auto", preferred_runtime: "Goose", @@ -112,6 +115,7 @@ test("a disabled saved runtime and its dependent defaults are masked", () => { assert.deepEqual(maskDisabledAcpRuntimePreference(config, ["goose"]), { ...config, + env_vars: { SHARED_API_KEY: "kept" }, provider: null, model: null, preferred_runtime: null, @@ -121,7 +125,10 @@ test("a disabled saved runtime and its dependent defaults are masked", () => { test("legacy defaults without a saved runtime are owned by buzz-agent", () => { const config = { - env_vars: {}, + env_vars: { + BUZZ_AGENT_THINKING_EFFORT: "medium", + SHARED_API_KEY: "kept", + }, provider: "relay-mesh", model: "auto", preferred_runtime: null, @@ -129,6 +136,7 @@ test("legacy defaults without a saved runtime are owned by buzz-agent", () => { assert.deepEqual(maskDisabledAcpRuntimePreference(config, ["buzz-agent"]), { ...config, + env_vars: { SHARED_API_KEY: "kept" }, provider: null, model: null, }); diff --git a/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts b/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts index 45e53e614..b8fe817fa 100644 --- a/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts +++ b/desktop/src/features/agents/lib/runtimeVisibilityPreference.ts @@ -1,4 +1,5 @@ import * as React from "react"; +import { withoutHarnessDependentAgentEnvVars } from "../ui/buzzAgentConfig"; export const ACP_RUNTIME_VISIBILITY_STORAGE_KEY = "buzz-agent-runtime-visibility.v1"; @@ -166,11 +167,12 @@ 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. Configs written before - * preferred_runtime existed implicitly belong to Buzz Agent. + * provider, model, or effort selected for the hidden harness. Configs written + * before preferred_runtime existed implicitly belong to Buzz Agent. */ export function maskDisabledAcpRuntimePreference< T extends { + env_vars: Record; model: string | null; preferred_runtime: string | null; provider: string | null; @@ -188,6 +190,7 @@ export function maskDisabledAcpRuntimePreference< return { ...config, + env_vars: withoutHarnessDependentAgentEnvVars(config.env_vars), model: null, preferred_runtime: null, provider: null, diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx index f31a20441..a002f6f83 100644 --- a/desktop/src/features/agents/ui/agentConfigOptions.tsx +++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx @@ -2,7 +2,7 @@ import type { AcpRuntimeCatalogEntry, GlobalAgentConfig, } from "@/shared/api/types"; -import { BUZZ_AGENT_THINKING_EFFORT } from "./buzzAgentConfig"; +import { withoutHarnessDependentAgentEnvVars } from "./buzzAgentConfig"; import type { RuntimeFileConfigSubset } from "@/shared/api/tauri"; // Dialogs import getDefaultPersonaRuntime via this re-export; lib code imports // directly from lib/resolvePersonaRuntime. @@ -222,12 +222,9 @@ export function resetConfigForHarnessChange( config: GlobalAgentConfig, runtimeId: string, ): GlobalAgentConfig { - const nextEnvVars = { ...config.env_vars }; - delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT]; - return { ...config, - env_vars: nextEnvVars, + env_vars: withoutHarnessDependentAgentEnvVars(config.env_vars), model: null, preferred_runtime: runtimeId || null, provider: diff --git a/desktop/src/features/agents/ui/buzzAgentConfig.ts b/desktop/src/features/agents/ui/buzzAgentConfig.ts index a0271fec0..c8b597ed8 100644 --- a/desktop/src/features/agents/ui/buzzAgentConfig.ts +++ b/desktop/src/features/agents/ui/buzzAgentConfig.ts @@ -8,6 +8,15 @@ /** Env var key for the thinking/effort level sent to the LLM. */ export const BUZZ_AGENT_THINKING_EFFORT = "BUZZ_AGENT_THINKING_EFFORT"; +/** Remove env-backed values whose semantics change with the harness. */ +export function withoutHarnessDependentAgentEnvVars( + envVars: Record, +): Record { + const next = { ...envVars }; + delete next[BUZZ_AGENT_THINKING_EFFORT]; + return next; +} + /** Env var key for the maximum output token count per turn. */ export const BUZZ_AGENT_MAX_OUTPUT_TOKENS = "BUZZ_AGENT_MAX_OUTPUT_TOKENS"; diff --git a/desktop/src/shared/api/types.ts b/desktop/src/shared/api/types.ts index 225e50cca..d221c5c24 100644 --- a/desktop/src/shared/api/types.ts +++ b/desktop/src/shared/api/types.ts @@ -1037,7 +1037,7 @@ export type ChannelMessagesPageResponse = { * Precedence: baked floor < global < persona < per-agent. */ export type GlobalAgentConfig = { - /** Global env vars injected into all agents unconditionally. */ + /** Global env vars injected below persona and per-agent overrides. */ env_vars: Record; /** Global fallback provider (e.g. "anthropic", "databricks_v2"). Null = no global default. */ provider: string | null;