diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs index c4d7b71f4..efec312b7 100644 --- a/desktop/src-tauri/build.rs +++ b/desktop/src-tauri/build.rs @@ -2,6 +2,10 @@ // so the build-time validation below and the runtime parse cannot drift. include!("src/commands/reconnect_hook_config.rs"); +mod build_agent_env_policy { + include!("src/managed_agents/build_agent_env_policy.rs"); +} + use base64::Engine as _; fn main() { @@ -33,9 +37,8 @@ fn main() { println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_BUZZ_AGENT_MODEL={model}"); } - // Generic KEY=VALUE pairs to inject into every spawned agent process. - // Newline-delimited; each line must be non-empty and contain exactly one - // `=` separator with a non-empty key. OSS builds leave this unset. + // Explicitly allowlisted, non-secret KEY=VALUE settings to inject into + // every spawned agent process. OSS builds leave this unset. // The validated value is base64-encoded before emitting so the single-line // Cargo build-script output carries all pairs (Cargo output is line-oriented; // a raw multiline value would be silently truncated to the first line). @@ -60,6 +63,14 @@ fn main() { line ); } + if !build_agent_env_policy::is_allowed_build_agent_env_key(key) { + panic!( + "BUZZ_BUILD_AGENT_ENV line {}: key {:?} is not approved non-secret build configuration; allowed keys: {:?}", + line_no + 1, + key, + build_agent_env_policy::ALLOWED_BUILD_AGENT_ENV_KEYS + ); + } } let encoded = base64::engine::general_purpose::STANDARD.encode(raw.as_bytes()); println!("cargo:rustc-env=BUZZ_DESKTOP_BUILD_AGENT_ENV={encoded}"); diff --git a/desktop/src-tauri/src/commands/agent_config.rs b/desktop/src-tauri/src/commands/agent_config.rs index f6141a53e..c0fabc393 100644 --- a/desktop/src-tauri/src/commands/agent_config.rs +++ b/desktop/src-tauri/src/commands/agent_config.rs @@ -234,8 +234,8 @@ pub async fn get_runtime_file_config( /// Return the key names of all non-empty baked build env vars. /// -/// Internal (Block) builds bake provider credentials and other env pairs into -/// the binary at compile time via `BUZZ_BUILD_AGENT_ENV`. The backend readiness +/// Internal (Block) builds bake provider/model defaults and a small allowlist +/// of non-secret settings into the binary at compile time. The backend readiness /// gate already treats these keys as satisfying their requirements (Layer 1 of /// `resolve_effective_agent_env`). This command exposes the *key names only* — /// never the values — so the frontend dialogs can apply the same logic and avoid diff --git a/desktop/src-tauri/src/managed_agents/agent_env.rs b/desktop/src-tauri/src/managed_agents/agent_env.rs index bf6bcb229..f3d59273f 100644 --- a/desktop/src-tauri/src/managed_agents/agent_env.rs +++ b/desktop/src-tauri/src/managed_agents/agent_env.rs @@ -1,17 +1,20 @@ //! Build-time agent env passthrough. //! -//! Internal builds (buzz-releases) bake arbitrary `KEY=VALUE` pairs into the -//! binary via `BUZZ_BUILD_AGENT_ENV` (base64-encoded, newline-delimited). +//! Internal builds (buzz-releases) bake a small allowlist of non-secret +//! `KEY=VALUE` settings into the binary via `BUZZ_BUILD_AGENT_ENV` +//! (base64-encoded, newline-delimited). //! OSS builds leave the compile-time var unset — nothing is injected. use std::collections::BTreeMap; use base64::Engine as _; +use super::build_agent_env_policy::is_allowed_build_agent_env_key; + /// Return the baked-in build-time env pairs as a map. /// -/// Internal builds (buzz-releases) bake provider/model defaults and arbitrary -/// `KEY=VALUE` pairs into the binary at compile time. This function returns +/// Internal builds (buzz-releases) bake provider/model defaults and allowlisted +/// non-secret settings into the binary at compile time. This function returns /// those pairs as an owned map so callers can fold them into an in-process env /// at the **lowest** precedence layer — user/persona values layered on top /// override these baked defaults (last-write-wins, matching the existing @@ -53,7 +56,9 @@ fn build_env_map( if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(raw.as_bytes()) { if let Ok(text) = std::str::from_utf8(&decoded) { for (key, value) in parse_agent_env_lines(text) { - map.insert(key.to_string(), value.to_string()); + if is_allowed_build_agent_env_key(key) { + map.insert(key.to_string(), value.to_string()); + } } } } @@ -111,6 +116,7 @@ mod tests { baked_build_env, build_buzz_agent_provider_defaults, build_env_map, discovery_env_with_baked_floor, parse_agent_env_lines, }; + use crate::managed_agents::build_agent_env_policy::is_allowed_build_agent_env_key; #[test] fn buzz_agent_provider_defaults_empty_in_oss_build() { @@ -301,6 +307,25 @@ mod tests { ); } + #[test] + fn build_agent_env_policy_rejects_credentials_at_build_and_runtime_boundaries() { + use base64::Engine as _; + + assert!(is_allowed_build_agent_env_key("DATABRICKS_MODEL")); + for key in ["GITHUB_TOKEN", "ANTHROPIC_API_KEY", "DB_PASSWORD"] { + assert!(!is_allowed_build_agent_env_key(key), "{key}"); + } + + let raw = "DATABRICKS_MODEL=reviewed-model\nGITHUB_TOKEN=must-not-be-embedded"; + let blob = base64::engine::general_purpose::STANDARD.encode(raw.as_bytes()); + let map = build_env_map(None, None, Some(&blob)); + assert_eq!( + map.get("DATABRICKS_MODEL").map(String::as_str), + Some("reviewed-model") + ); + assert!(!map.contains_key("GITHUB_TOKEN")); + } + #[test] fn build_env_map_user_env_overrides_baked_via_btreemap_extend() { // Validates the precedence fold used in agent_models.rs: diff --git a/desktop/src-tauri/src/managed_agents/build_agent_env_policy.rs b/desktop/src-tauri/src/managed_agents/build_agent_env_policy.rs new file mode 100644 index 000000000..0e9115d58 --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/build_agent_env_policy.rs @@ -0,0 +1,12 @@ +/// Non-secret settings that internal release builds may embed through +/// `BUZZ_BUILD_AGENT_ENV`. +pub(crate) const ALLOWED_BUILD_AGENT_ENV_KEYS: &[&str] = &[ + "BUZZ_AGENT_THINKING_EFFORT", + "DATABRICKS_HOST", + "DATABRICKS_MODEL", +]; + +/// Return whether `key` is approved non-secret build configuration. +pub(crate) fn is_allowed_build_agent_env_key(key: &str) -> bool { + ALLOWED_BUILD_AGENT_ENV_KEYS.contains(&key) +} diff --git a/desktop/src-tauri/src/managed_agents/mod.rs b/desktop/src-tauri/src/managed_agents/mod.rs index afe7f6f40..c8d4cdd46 100644 --- a/desktop/src-tauri/src/managed_agents/mod.rs +++ b/desktop/src-tauri/src/managed_agents/mod.rs @@ -1,6 +1,7 @@ mod agent_env; pub(crate) mod agent_events; pub(crate) mod agent_snapshot; +mod build_agent_env_policy; pub(crate) mod team_snapshot; pub(crate) use agent_env::{ baked_build_env, build_buzz_agent_provider_defaults, discovery_env_with_baked_floor,