diff --git a/desktop/src-tauri/src/commands/agent_config_tests.rs b/desktop/src-tauri/src/commands/agent_config_tests.rs index 14e7da098d..3ff6824c37 100644 --- a/desktop/src-tauri/src/commands/agent_config_tests.rs +++ b/desktop/src-tauri/src/commands/agent_config_tests.rs @@ -714,6 +714,31 @@ fn baked_env_thinking_summary_is_unmasked() { assert!(!summary.masked); } +#[test] +fn baked_env_openai_compat_endpoint_is_unmasked_but_key_stays_masked() { + // The OpenAI-compatible endpoint, model id, and API flavor are + // non-secret defaults; the API key next to them is a credential. + let entries = baked_env_from_map(&[ + ("OPENAI_COMPAT_BASE_URL", "https://llm.example/v1"), + ("OPENAI_COMPAT_MODEL", "balanced"), + ("OPENAI_COMPAT_API", "chat"), + ("OPENAI_COMPAT_API_KEY", "placeholder"), + ]); + assert_eq!(entries.len(), 4); + let find = |k: &str| entries.iter().find(|e| e.key == k).unwrap(); + assert_eq!( + find("OPENAI_COMPAT_BASE_URL").value, + "https://llm.example/v1" + ); + assert!(!find("OPENAI_COMPAT_BASE_URL").masked); + assert_eq!(find("OPENAI_COMPAT_MODEL").value, "balanced"); + assert!(!find("OPENAI_COMPAT_MODEL").masked); + assert_eq!(find("OPENAI_COMPAT_API").value, "chat"); + assert!(!find("OPENAI_COMPAT_API").masked); + assert!(find("OPENAI_COMPAT_API_KEY").masked); + assert_eq!(find("OPENAI_COMPAT_API_KEY").value, "••••••"); +} + #[test] fn baked_env_allowlist_is_case_insensitive() { // Known-safe keys — case-insensitive match must allow them. @@ -729,6 +754,11 @@ fn baked_env_allowlist_is_case_insensitive() { assert!(super::is_safe_to_reveal("DATABRICKS_HOST")); assert!(super::is_safe_to_reveal("databricks_model")); assert!(super::is_safe_to_reveal("DATABRICKS_MODEL")); + assert!(super::is_safe_to_reveal("openai_compat_base_url")); + assert!(super::is_safe_to_reveal("OPENAI_COMPAT_BASE_URL")); + assert!(super::is_safe_to_reveal("OPENAI_COMPAT_MODEL")); + assert!(super::is_safe_to_reveal("OPENAI_COMPAT_API")); + assert!(!super::is_safe_to_reveal("OPENAI_COMPAT_API_KEY")); // Keys NOT in the allowlist — masked regardless of naming pattern. assert!(!super::is_safe_to_reveal("my_api_key")); assert!(!super::is_safe_to_reveal("GITHUB_TOKEN")); diff --git a/desktop/src-tauri/src/managed_agents/env_vars.rs b/desktop/src-tauri/src/managed_agents/env_vars.rs index 6b12fbcd2b..2fb74c1af4 100644 --- a/desktop/src-tauri/src/managed_agents/env_vars.rs +++ b/desktop/src-tauri/src/managed_agents/env_vars.rs @@ -181,6 +181,9 @@ pub fn validate_user_env_keys(env_vars: &BTreeMap) -> Result<(), /// - `BUZZ_AGENT_THINKING_EFFORT` — non-secret enum (none/minimal/low/medium/high/xhigh/max) /// - `BUZZ_AGENT_THINKING_SUMMARY` — non-secret enum (auto/concise/detailed) /// - `DATABRICKS_HOST`, `DATABRICKS_MODEL`, `DATABRICKS_MODEL_FILTER` — Block non-secret defaults +/// - `OPENAI_COMPAT_BASE_URL`, `OPENAI_COMPAT_MODEL`, `OPENAI_COMPAT_API` — OpenAI-compatible +/// endpoint, model id, and API flavor (`auto|chat|responses`); the key itself +/// (`OPENAI_COMPAT_API_KEY`) is not listed and stays masked pub(crate) fn is_safe_to_reveal(key: &str) -> bool { const SAFE_KEYS: &[&str] = &[ "BUZZ_AGENT_PROVIDER", @@ -190,6 +193,9 @@ pub(crate) fn is_safe_to_reveal(key: &str) -> bool { "DATABRICKS_HOST", "DATABRICKS_MODEL", "DATABRICKS_MODEL_FILTER", + "OPENAI_COMPAT_BASE_URL", + "OPENAI_COMPAT_MODEL", + "OPENAI_COMPAT_API", ]; let upper = key.to_ascii_uppercase(); SAFE_KEYS.iter().any(|safe| upper == *safe)