Skip to content

Commit 8c83129

Browse files
fix(desktop): rename SPROUT_ env vars to BUZZ_ for child agent processes (#971)
1 parent 31b0665 commit 8c83129

10 files changed

Lines changed: 148 additions & 148 deletions

File tree

desktop/src-tauri/src/commands/agent_models.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ pub async fn get_agent_models(
8383
}
8484
cmd.arg("models")
8585
.arg("--json")
86-
.env("SPROUT_ACP_AGENT_COMMAND", &agent_command)
87-
.env("SPROUT_ACP_AGENT_ARGS", agent_args.join(","));
86+
.env("BUZZ_ACP_AGENT_COMMAND", &agent_command)
87+
.env("BUZZ_ACP_AGENT_ARGS", agent_args.join(","));
8888
if let Some(meta) = known_acp_runtime(&agent_command) {
8989
for (key, value) in meta.default_env {
9090
if std::env::var(key).is_err() {

desktop/src-tauri/src/managed_agents/discovery.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
152152
adapter_install_hint: "",
153153
skill_dir: None,
154154
supports_acp_model_switching: true,
155-
model_env_var: Some("SPROUT_AGENT_MODEL"),
156-
provider_env_var: Some("SPROUT_AGENT_PROVIDER"),
155+
model_env_var: Some("BUZZ_AGENT_MODEL"),
156+
provider_env_var: Some("BUZZ_AGENT_PROVIDER"),
157157
provider_locked: false,
158158
default_env: &[],
159159
},

desktop/src-tauri/src/managed_agents/env_vars.rs

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
//! Precedence: desktop parent env < persona env < agent env (last wins on
66
//! key collision). See `runtime::spawn_agent_child`.
77
//!
8-
//! A small set of *reserved* keys — Sprout's identity and secrets — are
8+
//! A small set of *reserved* keys — Buzz's identity and secrets — are
99
//! rejected at save time and stripped at runtime so a typo or malicious
1010
//! value can't swap the agent's nsec. Behavior knobs (GOOSE_MODE,
11-
//! SPROUT_TOOLSETS, SPROUT_ACP_MODEL, SPROUT_ACP_SYSTEM_PROMPT, …) remain
11+
//! BUZZ_TOOLSETS, BUZZ_ACP_MODEL, BUZZ_ACP_SYSTEM_PROMPT, …) remain
1212
//! freely overridable — those have dedicated UI fields, but power users
1313
//! may want to bypass them.
1414
@@ -28,8 +28,8 @@ use std::collections::BTreeMap;
2828
pub(crate) const DERIVED_PROVIDER_MODEL_ENV_KEYS: &[&str] = &[
2929
"GOOSE_MODEL",
3030
"GOOSE_PROVIDER",
31-
"SPROUT_AGENT_MODEL",
32-
"SPROUT_AGENT_PROVIDER",
31+
"BUZZ_AGENT_MODEL",
32+
"BUZZ_AGENT_PROVIDER",
3333
];
3434

3535
/// Returns `true` if `key` is a derived provider/model env key that should be
@@ -68,32 +68,32 @@ pub(crate) fn filter_derived_provider_model_env_vars(
6868
/// example), or redirect the agent to an attacker-controlled relay.
6969
///
7070
/// This list is deliberately narrow — it only covers keys with security
71-
/// implications. Behavior knobs (GOOSE_MODE, SPROUT_TOOLSETS,
72-
/// SPROUT_ACP_MODEL, SPROUT_ACP_SYSTEM_PROMPT, …) remain freely
71+
/// implications. Behavior knobs (GOOSE_MODE, BUZZ_TOOLSETS,
72+
/// BUZZ_ACP_MODEL, BUZZ_ACP_SYSTEM_PROMPT, …) remain freely
7373
/// overridable; those have dedicated UI fields but power users may want
7474
/// to bypass them.
7575
pub(crate) const RESERVED_ENV_KEYS: &[&str] = &[
7676
// Identity / secrets.
77-
"SPROUT_PRIVATE_KEY",
77+
"BUZZ_PRIVATE_KEY",
7878
"NOSTR_PRIVATE_KEY",
79-
"SPROUT_AUTH_TAG",
80-
"SPROUT_API_TOKEN",
81-
"SPROUT_ACP_PRIVATE_KEY",
82-
"SPROUT_ACP_API_TOKEN",
79+
"BUZZ_AUTH_TAG",
80+
"BUZZ_API_TOKEN",
81+
"BUZZ_ACP_PRIVATE_KEY",
82+
"BUZZ_ACP_API_TOKEN",
8383
// Relay URL: overriding would let a malicious config redirect the
8484
// agent to an attacker-controlled relay.
85-
"SPROUT_RELAY_URL",
85+
"BUZZ_RELAY_URL",
8686
// Code-execution surface: overriding would let the user run arbitrary
8787
// binaries/args as the agent process.
88-
"SPROUT_ACP_AGENT_COMMAND",
89-
"SPROUT_ACP_AGENT_ARGS",
90-
"SPROUT_ACP_MCP_COMMAND",
88+
"BUZZ_ACP_AGENT_COMMAND",
89+
"BUZZ_ACP_AGENT_ARGS",
90+
"BUZZ_ACP_MCP_COMMAND",
9191
// Security gates: respond-to mode + allowlist + legacy owner-only
9292
// fallback. Overriding would make the running agent's gate diverge
9393
// from the saved/UI-visible settings.
94-
"SPROUT_ACP_RESPOND_TO",
95-
"SPROUT_ACP_RESPOND_TO_ALLOWLIST",
96-
"SPROUT_ACP_AGENT_OWNER",
94+
"BUZZ_ACP_RESPOND_TO",
95+
"BUZZ_ACP_RESPOND_TO_ALLOWLIST",
96+
"BUZZ_ACP_AGENT_OWNER",
9797
];
9898

9999
pub(crate) fn is_reserved_env_key(key: &str) -> bool {
@@ -109,7 +109,7 @@ pub(crate) fn is_reserved_env_key(key: &str) -> bool {
109109
/// where `getenv("FOO")` then matches whatever comes after the first
110110
/// `=`. That means a key like `SPROUT_AUTH_TAG=x` with value `forged`
111111
/// lands as `SPROUT_AUTH_TAG=x=forged` in the child env and
112-
/// `getenv("SPROUT_AUTH_TAG")` returns `"x=forged"` — a full reserved-
112+
/// `getenv("BUZZ_AUTH_TAG")` returns `"x=forged"` — a full reserved-
113113
/// key bypass. Rejecting non-POSIX keys closes this hole at the
114114
/// boundary where the input enters the system.
115115
pub(crate) fn is_well_formed_env_key(key: &str) -> bool {

desktop/src-tauri/src/managed_agents/env_vars/tests.rs

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,11 @@ fn merged_env_strips_reserved_keys_from_persona() {
9090
// persona data (e.g. older record from before validation existed),
9191
// it must be stripped before reaching the child process.
9292
let persona = map(&[
93-
("SPROUT_PRIVATE_KEY", "nsec1evil"),
93+
("BUZZ_PRIVATE_KEY", "nsec1evil"),
9494
("ANTHROPIC_API_KEY", "ok"),
9595
]);
9696
let merged = merged_user_env(&persona, &BTreeMap::new());
97-
assert!(!merged.contains_key("SPROUT_PRIVATE_KEY"));
97+
assert!(!merged.contains_key("BUZZ_PRIVATE_KEY"));
9898
assert_eq!(
9999
merged.get("ANTHROPIC_API_KEY").map(String::as_str),
100100
Some("ok")
@@ -105,22 +105,22 @@ fn merged_env_strips_reserved_keys_from_persona() {
105105
fn merged_env_strips_reserved_keys_from_agent() {
106106
let agent = map(&[
107107
("NOSTR_PRIVATE_KEY", "nsec1evil"),
108-
("SPROUT_AUTH_TAG", "{}"),
108+
("BUZZ_AUTH_TAG", "{}"),
109109
("FOO", "1"),
110110
]);
111111
let merged = merged_user_env(&BTreeMap::new(), &agent);
112112
assert!(!merged.contains_key("NOSTR_PRIVATE_KEY"));
113-
assert!(!merged.contains_key("SPROUT_AUTH_TAG"));
113+
assert!(!merged.contains_key("BUZZ_AUTH_TAG"));
114114
assert_eq!(merged.get("FOO").map(String::as_str), Some("1"));
115115
assert_eq!(merged.len(), 1);
116116
}
117117

118118
#[test]
119119
fn merged_env_strips_reserved_case_insensitive() {
120120
// Unix env vars are case-sensitive at the syscall level, but we
121-
// refuse close-typo variants too — a lowercase `sprout_private_key`
121+
// refuse close-typo variants too — a lowercase `buzz_private_key`
122122
// is almost certainly a footgun, not a legitimate use.
123-
let agent = map(&[("sprout_private_key", "x"), ("Sprout_Auth_Tag", "y")]);
123+
let agent = map(&[("buzz_private_key", "x"), ("Buzz_Auth_Tag", "y")]);
124124
let merged = merged_user_env(&BTreeMap::new(), &agent);
125125
assert!(merged.is_empty());
126126
}
@@ -132,16 +132,16 @@ fn is_reserved_recognises_full_list() {
132132
}
133133
assert!(!is_reserved_env_key("GOOSE_MODE"));
134134
assert!(!is_reserved_env_key("ANTHROPIC_API_KEY"));
135-
assert!(!is_reserved_env_key("SPROUT_ACP_MODEL")); // behavior knob
136-
assert!(!is_reserved_env_key("SPROUT_TOOLSETS"));
135+
assert!(!is_reserved_env_key("BUZZ_ACP_MODEL")); // behavior knob
136+
assert!(!is_reserved_env_key("BUZZ_TOOLSETS"));
137137
}
138138

139139
#[test]
140140
fn reserved_keys_include_agent_owner_for_legacy_records() {
141-
// Legacy records without auth_tag fall back to SPROUT_ACP_AGENT_OWNER
141+
// Legacy records without auth_tag fall back to BUZZ_ACP_AGENT_OWNER
142142
// to enforce the respond-to gate. Must not be user-overridable.
143-
assert!(is_reserved_env_key("SPROUT_ACP_AGENT_OWNER"));
144-
let agent = map(&[("SPROUT_ACP_AGENT_OWNER", "imposter")]);
143+
assert!(is_reserved_env_key("BUZZ_ACP_AGENT_OWNER"));
144+
let agent = map(&[("BUZZ_ACP_AGENT_OWNER", "imposter")]);
145145
let merged = merged_user_env(&BTreeMap::new(), &agent);
146146
assert!(merged.is_empty());
147147
}
@@ -151,7 +151,7 @@ fn reserved_keys_include_respond_to_gate() {
151151
// Respond-to mode + allowlist control who the agent answers.
152152
// Overriding via env_vars would let the running agent answer
153153
// anyone even when the UI/record says owner-only.
154-
for key in ["SPROUT_ACP_RESPOND_TO", "SPROUT_ACP_RESPOND_TO_ALLOWLIST"] {
154+
for key in ["BUZZ_ACP_RESPOND_TO", "BUZZ_ACP_RESPOND_TO_ALLOWLIST"] {
155155
assert!(is_reserved_env_key(key), "{key} should be reserved");
156156
let agent = map(&[(key, "anyone")]);
157157
let merged = merged_user_env(&BTreeMap::new(), &agent);
@@ -164,9 +164,9 @@ fn reserved_keys_include_code_execution_surface() {
164164
// The agent/MCP command + args are what Sprout actually exec's.
165165
// Overriding lets the user run arbitrary code as the agent.
166166
for key in [
167-
"SPROUT_ACP_AGENT_COMMAND",
168-
"SPROUT_ACP_AGENT_ARGS",
169-
"SPROUT_ACP_MCP_COMMAND",
167+
"BUZZ_ACP_AGENT_COMMAND",
168+
"BUZZ_ACP_AGENT_ARGS",
169+
"BUZZ_ACP_MCP_COMMAND",
170170
] {
171171
assert!(is_reserved_env_key(key), "{key} should be reserved");
172172
}
@@ -176,8 +176,8 @@ fn reserved_keys_include_code_execution_surface() {
176176
fn reserved_keys_include_relay_url() {
177177
// Overriding the relay URL could redirect the agent to an
178178
// attacker-controlled relay.
179-
assert!(is_reserved_env_key("SPROUT_RELAY_URL"));
180-
let agent = map(&[("SPROUT_RELAY_URL", "ws://attacker.example")]);
179+
assert!(is_reserved_env_key("BUZZ_RELAY_URL"));
180+
let agent = map(&[("BUZZ_RELAY_URL", "ws://attacker.example")]);
181181
let merged = merged_user_env(&BTreeMap::new(), &agent);
182182
assert!(merged.is_empty());
183183
}
@@ -192,21 +192,21 @@ fn validate_keys_accepts_normal_env() {
192192

193193
#[test]
194194
fn validate_keys_rejects_reserved() {
195-
let env = map(&[("SPROUT_PRIVATE_KEY", "nsec1evil")]);
195+
let env = map(&[("BUZZ_PRIVATE_KEY", "nsec1evil")]);
196196
let err = validate_user_env_keys(&env).unwrap_err();
197-
assert!(err.contains("SPROUT_PRIVATE_KEY"), "got: {err}");
197+
assert!(err.contains("BUZZ_PRIVATE_KEY"), "got: {err}");
198198
assert!(err.contains("reserved"), "got: {err}");
199199
}
200200

201201
#[test]
202202
fn validate_keys_lists_all_reserved_keys_found() {
203203
let env = map(&[
204-
("SPROUT_PRIVATE_KEY", "x"),
204+
("BUZZ_PRIVATE_KEY", "x"),
205205
("NOSTR_PRIVATE_KEY", "y"),
206206
("ANTHROPIC_API_KEY", "ok"),
207207
]);
208208
let err = validate_user_env_keys(&env).unwrap_err();
209-
assert!(err.contains("SPROUT_PRIVATE_KEY"));
209+
assert!(err.contains("BUZZ_PRIVATE_KEY"));
210210
assert!(err.contains("NOSTR_PRIVATE_KEY"));
211211
}
212212

@@ -228,8 +228,8 @@ fn validate_keys_accepts_empty_map() {
228228
// Rust's `Command::env(k, v)` will accept a key containing `=` and
229229
// pass it straight into the child's environ block, where
230230
// `getenv("PREFIX")` matches anything after the first `=`. Concretely:
231-
// `c.env("SPROUT_AUTH_TAG=x", "forged")` results in the child seeing
232-
// `SPROUT_AUTH_TAG=x=forged` and `getenv("SPROUT_AUTH_TAG") == "x=forged"`.
231+
// `c.env("BUZZ_AUTH_TAG=x", "forged")` results in the child seeing
232+
// `BUZZ_AUTH_TAG=x=forged` and `getenv("BUZZ_AUTH_TAG") == "x=forged"`.
233233
// That bypasses our reserved-key check, which compares strings.
234234
// These tests pin the fix at the validator boundary.
235235

@@ -250,35 +250,35 @@ fn is_well_formed_accepts_posix_keys() {
250250
#[test]
251251
fn is_well_formed_rejects_malformed_keys() {
252252
for key in [
253-
"", // empty
254-
"=", // bare equals
255-
"SPROUT_AUTH_TAG=x", // =-in-key bypass
256-
"SPROUT_PRIVATE_KEY=", // trailing equals
257-
"FOO BAR", // space
258-
" FOO", // leading whitespace
259-
"FOO\nBAR", // newline
260-
"FOO\0BAR", // NUL
261-
"123_LEADING_DIGIT", // POSIX forbids leading digit
262-
"FOO-BAR", // hyphen
263-
"FOO.BAR", // dot
264-
"FOO/BAR", // slash
265-
"ünicode_key", // non-ASCII
253+
"", // empty
254+
"=", // bare equals
255+
"BUZZ_AUTH_TAG=x", // =-in-key bypass
256+
"BUZZ_PRIVATE_KEY=", // trailing equals
257+
"FOO BAR", // space
258+
" FOO", // leading whitespace
259+
"FOO\nBAR", // newline
260+
"FOO\0BAR", // NUL
261+
"123_LEADING_DIGIT", // POSIX forbids leading digit
262+
"FOO-BAR", // hyphen
263+
"FOO.BAR", // dot
264+
"FOO/BAR", // slash
265+
"ünicode_key", // non-ASCII
266266
] {
267267
assert!(!is_well_formed_env_key(key), "{key:?} should be malformed");
268268
}
269269
}
270270

271271
#[test]
272272
fn validate_keys_rejects_equals_in_key_bypass() {
273-
// The actual exploit: `SPROUT_AUTH_TAG=x` smuggles a value past
273+
// The actual exploit: `BUZZ_AUTH_TAG=x` smuggles a value past
274274
// the reserved-key string compare and into the child's environ.
275-
let env = map(&[("SPROUT_AUTH_TAG=x", "forged")]);
275+
let env = map(&[("BUZZ_AUTH_TAG=x", "forged")]);
276276
let err = validate_user_env_keys(&env).unwrap_err();
277277
assert!(err.contains("[A-Za-z_]"), "got: {err}");
278278
// After P2 fix the key is truncated at `=` in the error to avoid
279279
// surfacing pasted secrets — only the prefix should appear, with an
280280
// ellipsis marking that we elided trailing content.
281-
assert!(err.contains("SPROUT_AUTH_TAG"), "got: {err}");
281+
assert!(err.contains("BUZZ_AUTH_TAG"), "got: {err}");
282282
assert!(err.contains('…'), "expected ellipsis marker: {err}");
283283
assert!(!err.contains("=x"), "leak of value past `=`: {err}");
284284
}
@@ -296,7 +296,7 @@ fn validate_keys_reports_malformed_before_reserved() {
296296
// the way that other key is reserved" — they've got a typo to fix
297297
// first. Ordering is a UX detail but pinning it stops the message
298298
// from churning.
299-
let env = map(&[("SPROUT_AUTH_TAG=x", "v"), ("SPROUT_PRIVATE_KEY", "v")]);
299+
let env = map(&[("BUZZ_AUTH_TAG=x", "v"), ("BUZZ_PRIVATE_KEY", "v")]);
300300
let err = validate_user_env_keys(&env).unwrap_err();
301301
assert!(err.contains("[A-Za-z_]"), "got: {err}");
302302
assert!(!err.contains("reserved"), "got: {err}");
@@ -307,12 +307,12 @@ fn merged_env_drops_malformed_keys() {
307307
// Defense in depth: on-disk records written before the validator
308308
// tightened must not be able to smuggle reserved keys through.
309309
let agent = map(&[
310-
("SPROUT_AUTH_TAG=x", "forged"),
310+
("BUZZ_AUTH_TAG=x", "forged"),
311311
("FOO=bar", "v"),
312312
("LEGIT", "ok"),
313313
]);
314314
let merged = merged_user_env(&BTreeMap::new(), &agent);
315-
assert!(!merged.contains_key("SPROUT_AUTH_TAG=x"));
315+
assert!(!merged.contains_key("BUZZ_AUTH_TAG=x"));
316316
assert!(!merged.contains_key("FOO=bar"));
317317
assert_eq!(merged.get("LEGIT").map(String::as_str), Some("ok"));
318318
assert_eq!(merged.len(), 1);
@@ -420,7 +420,7 @@ fn merged_env_drops_oversize_value() {
420420
// ── derived provider/model key filter ──────────────────────────────
421421
//
422422
// Pack import must strip derived env keys (GOOSE_MODEL, GOOSE_PROVIDER,
423-
// SPROUT_AGENT_MODEL, SPROUT_AGENT_PROVIDER) so they don't shadow the
423+
// BUZZ_AGENT_MODEL, BUZZ_AGENT_PROVIDER) so they don't shadow the
424424
// structured PersonaRecord.model / PersonaRecord.provider fields after
425425
// the user edits them in the UI.
426426

@@ -438,16 +438,16 @@ fn is_derived_key_matches_all_known_keys() {
438438
fn is_derived_key_is_case_insensitive() {
439439
assert!(is_derived_provider_model_key("goose_model"));
440440
assert!(is_derived_provider_model_key("Goose_Provider"));
441-
assert!(is_derived_provider_model_key("sprout_agent_model"));
442-
assert!(is_derived_provider_model_key("SPROUT_AGENT_PROVIDER"));
441+
assert!(is_derived_provider_model_key("buzz_agent_model"));
442+
assert!(is_derived_provider_model_key("BUZZ_AGENT_PROVIDER"));
443443
}
444444

445445
#[test]
446446
fn is_derived_key_does_not_match_unrelated_keys() {
447447
assert!(!is_derived_provider_model_key("GOOSE_TEMPERATURE"));
448448
assert!(!is_derived_provider_model_key("GOOSE_CONTEXT_LIMIT"));
449449
assert!(!is_derived_provider_model_key("ANTHROPIC_API_KEY"));
450-
assert!(!is_derived_provider_model_key("SPROUT_PRIVATE_KEY"));
450+
assert!(!is_derived_provider_model_key("BUZZ_PRIVATE_KEY"));
451451
assert!(!is_derived_provider_model_key("MODEL"));
452452
assert!(!is_derived_provider_model_key("PROVIDER"));
453453
}
@@ -460,8 +460,8 @@ fn filter_derived_strips_provider_model_keys_preserves_rest() {
460460
"claude-sonnet-4-20250514".to_string(),
461461
),
462462
("GOOSE_PROVIDER".to_string(), "anthropic".to_string()),
463-
("SPROUT_AGENT_MODEL".to_string(), "gpt-4o".to_string()),
464-
("SPROUT_AGENT_PROVIDER".to_string(), "openai".to_string()),
463+
("BUZZ_AGENT_MODEL".to_string(), "gpt-4o".to_string()),
464+
("BUZZ_AGENT_PROVIDER".to_string(), "openai".to_string()),
465465
("GOOSE_TEMPERATURE".to_string(), "0.7".to_string()),
466466
("ANTHROPIC_API_KEY".to_string(), "sk-test".to_string()),
467467
];

0 commit comments

Comments
 (0)