fix(desktop): reject baked agent credentials (BUZZ-SEC-051)

This commit is contained in:
Jordan Mecom
2026-07-15 11:31:52 -07:00
parent 371ab872e1
commit 05a59a4e5c
5 changed files with 59 additions and 10 deletions
+14 -3
View File
@@ -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}");
@@ -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
@@ -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:
@@ -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)
}
@@ -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,