Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions desktop/src-tauri/src/commands/agent_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,18 +216,23 @@ pub struct BakedEnvEntry {
///
/// Allowlist (case-insensitive):
/// - `BUZZ_AGENT_PROVIDER`, `BUZZ_AGENT_MODEL` — agent runtime selection
/// - `BUZZ_AGENT_THINKING_EFFORT` — non-secret enum (none/minimal/low/medium/high/xhigh/max)
/// - All known native thinking-effort keys (non-secret enum values) — derived
/// from `ALL_KNOWN_EFFORT_KEYS` so this list stays in sync with runtime
/// metadata declarations (`BUZZ_AGENT_THINKING_EFFORT`, `GOOSE_THINKING_EFFORT`).
/// - `DATABRICKS_HOST`, `DATABRICKS_MODEL` — Block non-secret defaults
fn is_safe_to_reveal(key: &str) -> bool {
use crate::managed_agents::config_bridge::ALL_KNOWN_EFFORT_KEYS;
const SAFE_KEYS: &[&str] = &[
"BUZZ_AGENT_PROVIDER",
"BUZZ_AGENT_MODEL",
"BUZZ_AGENT_THINKING_EFFORT",
"DATABRICKS_HOST",
"DATABRICKS_MODEL",
];
let upper = key.to_ascii_uppercase();
SAFE_KEYS.iter().any(|safe| upper == *safe)
|| ALL_KNOWN_EFFORT_KEYS
.iter()
.any(|effort| upper == effort.to_ascii_uppercase())
}

/// Expose the baked build env to the frontend with values shown, but any
Expand Down
152 changes: 76 additions & 76 deletions desktop/src-tauri/src/commands/agent_config_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,7 @@ fn with_no_goose_config<T>(body: impl FnOnce() -> T) -> T {
}

fn goose_runtime() -> &'static KnownAcpRuntime {
&KnownAcpRuntime {
id: "goose",
label: "Goose",
commands: &["goose"],
aliases: &[],
avatar_url: "",
mcp_command: None,
mcp_hooks: false,
underlying_cli: None,
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
cli_install_instructions_url: "",
adapter_install_instructions_url: "",
cli_install_hint: "",
adapter_install_hint: "",
skill_dir: None,
supports_acp_model_switching: false,
model_env_var: Some("GOOSE_MODEL"),
provider_env_var: Some("GOOSE_PROVIDER"),
provider_locked: false,
default_env: &[],
config_file_path: Some("~/.config/goose/config.yaml"),
config_file_format: Some("yaml"),
supports_acp_native_config: true,
thinking_env_var: Some("GOOSE_THINKING_EFFORT"),
max_tokens_env_var: Some("GOOSE_MAX_TOKENS"),
context_limit_env_var: Some("GOOSE_CONTEXT_LIMIT"),
max_rounds_env_var: None,
required_normalized_fields: &["model", "provider"],
login_hint: None,
auth_probe_args: None,
}
crate::managed_agents::known_acp_runtime_exact("goose").expect("goose must be in catalog")
}

fn agent_record() -> ManagedAgentRecord {
Expand Down Expand Up @@ -175,13 +143,15 @@ fn linked_stale_record_model_never_outranks_persona_model() {
record.model = Some("stale-explicit-model".to_string());
let personas = vec![persona_with_model("persona-model")];

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
None,
&Default::default(),
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
None,
&Default::default(),
)
});

let model = surface.normalized.model.as_ref().expect("model resolved");
assert_eq!(model.value.as_deref(), Some("persona-model"));
Expand All @@ -205,7 +175,9 @@ fn linked_blank_definition_model_falls_through_to_global_default() {
..Default::default()
};

let surface = resolve_config_surface(record, &personas, Some(goose_runtime()), None, &global);
let surface = with_no_goose_config(|| {
resolve_config_surface(record, &personas, Some(goose_runtime()), None, &global)
});

let model = surface.normalized.model.as_ref().expect("model resolved");
assert_eq!(model.value.as_deref(), Some("global-model"));
Expand All @@ -222,13 +194,15 @@ fn definition_less_explicit_record_model_keeps_buzz_explicit_origin() {
record.model = Some("explicit-model".to_string());
let personas = vec![persona_with_model("persona-model")];

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
None,
&Default::default(),
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
None,
&Default::default(),
)
});

let model = surface.normalized.model.as_ref().expect("model resolved");
assert_eq!(model.value.as_deref(), Some("explicit-model"));
Expand All @@ -249,13 +223,15 @@ fn pending_pick_keeps_explicit_x_and_does_not_surface_live_y() {
let personas: Vec<AgentDefinition> = vec![];
let cache = session_cache("model-y", false);

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
)
});
let model = surface.normalized.model.expect("model resolved");

assert_eq!(model.value.as_deref(), Some("model-x"));
Expand All @@ -277,13 +253,15 @@ fn genuine_explicit_live_switch_renders_y_over_x_buzz_explicit_secondary() {
let personas: Vec<AgentDefinition> = vec![];
let cache = session_cache("model-y", true);

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
)
});
let model = surface.normalized.model.expect("model resolved");

assert_eq!(model.value.as_deref(), Some("model-y"));
Expand Down Expand Up @@ -340,13 +318,15 @@ fn persona_linked_live_switch_keeps_persona_default_secondary() {
let personas = vec![persona_with_model("persona-model")];
let cache = session_cache("model-y", true);

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&Default::default(),
)
});
let model = surface.normalized.model.expect("model resolved");

assert_eq!(model.value.as_deref(), Some("model-y"));
Expand Down Expand Up @@ -375,13 +355,15 @@ fn global_default_live_switch_renders_global_model_as_secondary_global_default()
..Default::default()
};

let surface = resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&global,
);
let surface = with_no_goose_config(|| {
resolve_config_surface(
record,
&personas,
Some(goose_runtime()),
Some(&cache),
&global,
)
});
let model = surface.normalized.model.expect("model resolved");

// Live model wins as primary.
Expand Down Expand Up @@ -623,6 +605,21 @@ fn baked_env_thinking_effort_is_unmasked() {
assert!(!effort.masked);
}

#[test]
fn baked_env_goose_thinking_effort_is_unmasked() {
// GOOSE_THINKING_EFFORT is a non-secret canonical enum (off/low/medium/high/max) —
// must be revealed so the native baked lookup in `bakedEnvHelpers.ts` can read
// it as inherited effort for Goose agents (plan v3 pass-3 ★ pin).
let entries = baked_env_from_map(&[("GOOSE_THINKING_EFFORT", "high")]);
assert_eq!(entries.len(), 1);
let effort = entries
.iter()
.find(|e| e.key == "GOOSE_THINKING_EFFORT")
.unwrap();
assert_eq!(effort.value, "high");
assert!(!effort.masked);
}

#[test]
fn baked_env_allowlist_is_case_insensitive() {
// Known-safe keys — case-insensitive match must allow them.
Expand All @@ -632,6 +629,9 @@ fn baked_env_allowlist_is_case_insensitive() {
assert!(super::is_safe_to_reveal("BUZZ_AGENT_MODEL"));
assert!(super::is_safe_to_reveal("buzz_agent_thinking_effort"));
assert!(super::is_safe_to_reveal("BUZZ_AGENT_THINKING_EFFORT"));
// Goose native effort key — derived from ALL_KNOWN_EFFORT_KEYS.
assert!(super::is_safe_to_reveal("goose_thinking_effort"));
assert!(super::is_safe_to_reveal("GOOSE_THINKING_EFFORT"));
assert!(super::is_safe_to_reveal("databricks_host"));
assert!(super::is_safe_to_reveal("DATABRICKS_HOST"));
assert!(super::is_safe_to_reveal("databricks_model"));
Expand Down
4 changes: 2 additions & 2 deletions desktop/src-tauri/src/commands/agent_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ pub(crate) fn plan_adapter_install<'c>(
{
None
}
// Codex adapter is outdated: uninstall the old package first so npm
// doesn't hit EEXIST on the shared `codex-acp` bin-link, then install.
// Codex adapter outdated: uninstall old package first (npm refuses to overwrite a bin from another pkg).
Some(_) => Some(vec![
"npm uninstall -g @zed-industries/codex-acp",
"npm install -g @agentclientprotocol/codex-acp",
Expand Down Expand Up @@ -165,6 +164,7 @@ pub async fn save_custom_harness(
model_env_var: None,
provider_env_var: None,
thinking_env_var: None,
accepted_effort_values: None,
max_tokens_env_var: None,
context_limit_env_var: None,
max_rounds_env_var: None,
Expand Down
65 changes: 65 additions & 0 deletions desktop/src-tauri/src/commands/agents_deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,69 @@ mod tests {
assert_eq!(launch["policy_env"]["BUZZ_ACP_AGENTS"], "4");
assert_eq!(launch["owner_pubkey"], "owner-hex");
}

/// Deploy parity (plan v3 Delta 4): for a legacy-only Goose record, the bridged
/// descriptor feeds `launch.env` with the native key, while the separately-merged
/// top-level `env_vars` retains the legacy key untouched.
///
/// Contract: providers execute `launch`; top-level `env_vars` is compatibility
/// bookkeeping. This test pins that boundary.
#[test]
fn deploy_parity_launch_env_carries_native_goose_effort() {
use crate::managed_agents::known_acp_runtime_exact;
use crate::managed_agents::{
global_config::GlobalAgentConfig, resolve_effective_agent_env,
};

// Goose record with only legacy BUZZ_AGENT_THINKING_EFFORT.
let record: ManagedAgentRecord = serde_json::from_value(serde_json::json!({
"pubkey": "pk",
"name": "goose-agent",
"private_key_nsec": "",
"relay_url": "",
"acp_command": "goose-acp",
"agent_command": "goose",
"agent_args": [],
"mcp_command": "",
"turn_timeout_seconds": 320,
"parallelism": 1,
"respond_to": "owner-only",
"respond_to_allowlist": [],
"env_vars": { "BUZZ_AGENT_THINKING_EFFORT": "high" },
"created_at": "2026-01-01T00:00:00Z",
"updated_at": "2026-01-01T00:00:00Z"
}))
.unwrap();

let runtime = known_acp_runtime_exact("goose");
let global = GlobalAgentConfig::default();
// The descriptor (= what launch.env uses) comes from resolve_effective_agent_env.
let descriptor = resolve_effective_agent_env(&record, &[], runtime, &global);

// launch.env: legacy key translated → native key.
assert_eq!(
descriptor
.env
.get("GOOSE_THINKING_EFFORT")
.map(String::as_str),
Some("high"),
"launch.env must carry native GOOSE_THINKING_EFFORT"
);
assert!(
!descriptor.env.contains_key("BUZZ_AGENT_THINKING_EFFORT"),
"launch.env must not carry legacy key"
);

// top-level env_vars (unmodified raw input): legacy key is still there.
// (The deploy payload's `env_vars` field is merged_user_env of the raw record —
// the bridge only affects the descriptor/launch path.)
assert_eq!(
record
.env_vars
.get("BUZZ_AGENT_THINKING_EFFORT")
.map(String::as_str),
Some("high"),
"top-level env_vars retains legacy key as compatibility bookkeeping"
);
}
}
Loading
Loading