fix(desktop): rename SPROUT_ env vars to BUZZ_ for child agent processes (#971)

This commit is contained in:
tlongwell-block
2026-06-11 10:13:52 -04:00
committed by GitHub
parent 31b0665cff
commit 8c8312932a
10 changed files with 148 additions and 148 deletions
@@ -83,8 +83,8 @@ pub async fn get_agent_models(
}
cmd.arg("models")
.arg("--json")
.env("SPROUT_ACP_AGENT_COMMAND", &agent_command)
.env("SPROUT_ACP_AGENT_ARGS", agent_args.join(","));
.env("BUZZ_ACP_AGENT_COMMAND", &agent_command)
.env("BUZZ_ACP_AGENT_ARGS", agent_args.join(","));
if let Some(meta) = known_acp_runtime(&agent_command) {
for (key, value) in meta.default_env {
if std::env::var(key).is_err() {
@@ -152,8 +152,8 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
adapter_install_hint: "",
skill_dir: None,
supports_acp_model_switching: true,
model_env_var: Some("SPROUT_AGENT_MODEL"),
provider_env_var: Some("SPROUT_AGENT_PROVIDER"),
model_env_var: Some("BUZZ_AGENT_MODEL"),
provider_env_var: Some("BUZZ_AGENT_PROVIDER"),
provider_locked: false,
default_env: &[],
},
@@ -5,10 +5,10 @@
//! Precedence: desktop parent env < persona env < agent env (last wins on
//! key collision). See `runtime::spawn_agent_child`.
//!
//! A small set of *reserved* keys — Sprout's identity and secrets — are
//! A small set of *reserved* keys — Buzz's identity and secrets — are
//! rejected at save time and stripped at runtime so a typo or malicious
//! value can't swap the agent's nsec. Behavior knobs (GOOSE_MODE,
//! SPROUT_TOOLSETS, SPROUT_ACP_MODEL, SPROUT_ACP_SYSTEM_PROMPT, …) remain
//! BUZZ_TOOLSETS, BUZZ_ACP_MODEL, BUZZ_ACP_SYSTEM_PROMPT, …) remain
//! freely overridable — those have dedicated UI fields, but power users
//! may want to bypass them.
@@ -28,8 +28,8 @@ use std::collections::BTreeMap;
pub(crate) const DERIVED_PROVIDER_MODEL_ENV_KEYS: &[&str] = &[
"GOOSE_MODEL",
"GOOSE_PROVIDER",
"SPROUT_AGENT_MODEL",
"SPROUT_AGENT_PROVIDER",
"BUZZ_AGENT_MODEL",
"BUZZ_AGENT_PROVIDER",
];
/// 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(
/// example), or redirect the agent to an attacker-controlled relay.
///
/// This list is deliberately narrow — it only covers keys with security
/// implications. Behavior knobs (GOOSE_MODE, SPROUT_TOOLSETS,
/// SPROUT_ACP_MODEL, SPROUT_ACP_SYSTEM_PROMPT, …) remain freely
/// implications. Behavior knobs (GOOSE_MODE, BUZZ_TOOLSETS,
/// BUZZ_ACP_MODEL, BUZZ_ACP_SYSTEM_PROMPT, …) remain freely
/// overridable; those have dedicated UI fields but power users may want
/// to bypass them.
pub(crate) const RESERVED_ENV_KEYS: &[&str] = &[
// Identity / secrets.
"SPROUT_PRIVATE_KEY",
"BUZZ_PRIVATE_KEY",
"NOSTR_PRIVATE_KEY",
"SPROUT_AUTH_TAG",
"SPROUT_API_TOKEN",
"SPROUT_ACP_PRIVATE_KEY",
"SPROUT_ACP_API_TOKEN",
"BUZZ_AUTH_TAG",
"BUZZ_API_TOKEN",
"BUZZ_ACP_PRIVATE_KEY",
"BUZZ_ACP_API_TOKEN",
// Relay URL: overriding would let a malicious config redirect the
// agent to an attacker-controlled relay.
"SPROUT_RELAY_URL",
"BUZZ_RELAY_URL",
// Code-execution surface: overriding would let the user run arbitrary
// binaries/args as the agent process.
"SPROUT_ACP_AGENT_COMMAND",
"SPROUT_ACP_AGENT_ARGS",
"SPROUT_ACP_MCP_COMMAND",
"BUZZ_ACP_AGENT_COMMAND",
"BUZZ_ACP_AGENT_ARGS",
"BUZZ_ACP_MCP_COMMAND",
// Security gates: respond-to mode + allowlist + legacy owner-only
// fallback. Overriding would make the running agent's gate diverge
// from the saved/UI-visible settings.
"SPROUT_ACP_RESPOND_TO",
"SPROUT_ACP_RESPOND_TO_ALLOWLIST",
"SPROUT_ACP_AGENT_OWNER",
"BUZZ_ACP_RESPOND_TO",
"BUZZ_ACP_RESPOND_TO_ALLOWLIST",
"BUZZ_ACP_AGENT_OWNER",
];
pub(crate) fn is_reserved_env_key(key: &str) -> bool {
@@ -109,7 +109,7 @@ pub(crate) fn is_reserved_env_key(key: &str) -> bool {
/// where `getenv("FOO")` then matches whatever comes after the first
/// `=`. That means a key like `SPROUT_AUTH_TAG=x` with value `forged`
/// lands as `SPROUT_AUTH_TAG=x=forged` in the child env and
/// `getenv("SPROUT_AUTH_TAG")` returns `"x=forged"` — a full reserved-
/// `getenv("BUZZ_AUTH_TAG")` returns `"x=forged"` — a full reserved-
/// key bypass. Rejecting non-POSIX keys closes this hole at the
/// boundary where the input enters the system.
pub(crate) fn is_well_formed_env_key(key: &str) -> bool {
@@ -90,11 +90,11 @@ fn merged_env_strips_reserved_keys_from_persona() {
// persona data (e.g. older record from before validation existed),
// it must be stripped before reaching the child process.
let persona = map(&[
("SPROUT_PRIVATE_KEY", "nsec1evil"),
("BUZZ_PRIVATE_KEY", "nsec1evil"),
("ANTHROPIC_API_KEY", "ok"),
]);
let merged = merged_user_env(&persona, &BTreeMap::new());
assert!(!merged.contains_key("SPROUT_PRIVATE_KEY"));
assert!(!merged.contains_key("BUZZ_PRIVATE_KEY"));
assert_eq!(
merged.get("ANTHROPIC_API_KEY").map(String::as_str),
Some("ok")
@@ -105,12 +105,12 @@ fn merged_env_strips_reserved_keys_from_persona() {
fn merged_env_strips_reserved_keys_from_agent() {
let agent = map(&[
("NOSTR_PRIVATE_KEY", "nsec1evil"),
("SPROUT_AUTH_TAG", "{}"),
("BUZZ_AUTH_TAG", "{}"),
("FOO", "1"),
]);
let merged = merged_user_env(&BTreeMap::new(), &agent);
assert!(!merged.contains_key("NOSTR_PRIVATE_KEY"));
assert!(!merged.contains_key("SPROUT_AUTH_TAG"));
assert!(!merged.contains_key("BUZZ_AUTH_TAG"));
assert_eq!(merged.get("FOO").map(String::as_str), Some("1"));
assert_eq!(merged.len(), 1);
}
@@ -118,9 +118,9 @@ fn merged_env_strips_reserved_keys_from_agent() {
#[test]
fn merged_env_strips_reserved_case_insensitive() {
// Unix env vars are case-sensitive at the syscall level, but we
// refuse close-typo variants too — a lowercase `sprout_private_key`
// refuse close-typo variants too — a lowercase `buzz_private_key`
// is almost certainly a footgun, not a legitimate use.
let agent = map(&[("sprout_private_key", "x"), ("Sprout_Auth_Tag", "y")]);
let agent = map(&[("buzz_private_key", "x"), ("Buzz_Auth_Tag", "y")]);
let merged = merged_user_env(&BTreeMap::new(), &agent);
assert!(merged.is_empty());
}
@@ -132,16 +132,16 @@ fn is_reserved_recognises_full_list() {
}
assert!(!is_reserved_env_key("GOOSE_MODE"));
assert!(!is_reserved_env_key("ANTHROPIC_API_KEY"));
assert!(!is_reserved_env_key("SPROUT_ACP_MODEL")); // behavior knob
assert!(!is_reserved_env_key("SPROUT_TOOLSETS"));
assert!(!is_reserved_env_key("BUZZ_ACP_MODEL")); // behavior knob
assert!(!is_reserved_env_key("BUZZ_TOOLSETS"));
}
#[test]
fn reserved_keys_include_agent_owner_for_legacy_records() {
// Legacy records without auth_tag fall back to SPROUT_ACP_AGENT_OWNER
// Legacy records without auth_tag fall back to BUZZ_ACP_AGENT_OWNER
// to enforce the respond-to gate. Must not be user-overridable.
assert!(is_reserved_env_key("SPROUT_ACP_AGENT_OWNER"));
let agent = map(&[("SPROUT_ACP_AGENT_OWNER", "imposter")]);
assert!(is_reserved_env_key("BUZZ_ACP_AGENT_OWNER"));
let agent = map(&[("BUZZ_ACP_AGENT_OWNER", "imposter")]);
let merged = merged_user_env(&BTreeMap::new(), &agent);
assert!(merged.is_empty());
}
@@ -151,7 +151,7 @@ fn reserved_keys_include_respond_to_gate() {
// Respond-to mode + allowlist control who the agent answers.
// Overriding via env_vars would let the running agent answer
// anyone even when the UI/record says owner-only.
for key in ["SPROUT_ACP_RESPOND_TO", "SPROUT_ACP_RESPOND_TO_ALLOWLIST"] {
for key in ["BUZZ_ACP_RESPOND_TO", "BUZZ_ACP_RESPOND_TO_ALLOWLIST"] {
assert!(is_reserved_env_key(key), "{key} should be reserved");
let agent = map(&[(key, "anyone")]);
let merged = merged_user_env(&BTreeMap::new(), &agent);
@@ -164,9 +164,9 @@ fn reserved_keys_include_code_execution_surface() {
// The agent/MCP command + args are what Sprout actually exec's.
// Overriding lets the user run arbitrary code as the agent.
for key in [
"SPROUT_ACP_AGENT_COMMAND",
"SPROUT_ACP_AGENT_ARGS",
"SPROUT_ACP_MCP_COMMAND",
"BUZZ_ACP_AGENT_COMMAND",
"BUZZ_ACP_AGENT_ARGS",
"BUZZ_ACP_MCP_COMMAND",
] {
assert!(is_reserved_env_key(key), "{key} should be reserved");
}
@@ -176,8 +176,8 @@ fn reserved_keys_include_code_execution_surface() {
fn reserved_keys_include_relay_url() {
// Overriding the relay URL could redirect the agent to an
// attacker-controlled relay.
assert!(is_reserved_env_key("SPROUT_RELAY_URL"));
let agent = map(&[("SPROUT_RELAY_URL", "ws://attacker.example")]);
assert!(is_reserved_env_key("BUZZ_RELAY_URL"));
let agent = map(&[("BUZZ_RELAY_URL", "ws://attacker.example")]);
let merged = merged_user_env(&BTreeMap::new(), &agent);
assert!(merged.is_empty());
}
@@ -192,21 +192,21 @@ fn validate_keys_accepts_normal_env() {
#[test]
fn validate_keys_rejects_reserved() {
let env = map(&[("SPROUT_PRIVATE_KEY", "nsec1evil")]);
let env = map(&[("BUZZ_PRIVATE_KEY", "nsec1evil")]);
let err = validate_user_env_keys(&env).unwrap_err();
assert!(err.contains("SPROUT_PRIVATE_KEY"), "got: {err}");
assert!(err.contains("BUZZ_PRIVATE_KEY"), "got: {err}");
assert!(err.contains("reserved"), "got: {err}");
}
#[test]
fn validate_keys_lists_all_reserved_keys_found() {
let env = map(&[
("SPROUT_PRIVATE_KEY", "x"),
("BUZZ_PRIVATE_KEY", "x"),
("NOSTR_PRIVATE_KEY", "y"),
("ANTHROPIC_API_KEY", "ok"),
]);
let err = validate_user_env_keys(&env).unwrap_err();
assert!(err.contains("SPROUT_PRIVATE_KEY"));
assert!(err.contains("BUZZ_PRIVATE_KEY"));
assert!(err.contains("NOSTR_PRIVATE_KEY"));
}
@@ -228,8 +228,8 @@ fn validate_keys_accepts_empty_map() {
// Rust's `Command::env(k, v)` will accept a key containing `=` and
// pass it straight into the child's environ block, where
// `getenv("PREFIX")` matches anything after the first `=`. Concretely:
// `c.env("SPROUT_AUTH_TAG=x", "forged")` results in the child seeing
// `SPROUT_AUTH_TAG=x=forged` and `getenv("SPROUT_AUTH_TAG") == "x=forged"`.
// `c.env("BUZZ_AUTH_TAG=x", "forged")` results in the child seeing
// `BUZZ_AUTH_TAG=x=forged` and `getenv("BUZZ_AUTH_TAG") == "x=forged"`.
// That bypasses our reserved-key check, which compares strings.
// These tests pin the fix at the validator boundary.
@@ -250,19 +250,19 @@ fn is_well_formed_accepts_posix_keys() {
#[test]
fn is_well_formed_rejects_malformed_keys() {
for key in [
"", // empty
"=", // bare equals
"SPROUT_AUTH_TAG=x", // =-in-key bypass
"SPROUT_PRIVATE_KEY=", // trailing equals
"FOO BAR", // space
" FOO", // leading whitespace
"FOO\nBAR", // newline
"FOO\0BAR", // NUL
"123_LEADING_DIGIT", // POSIX forbids leading digit
"FOO-BAR", // hyphen
"FOO.BAR", // dot
"FOO/BAR", // slash
"ünicode_key", // non-ASCII
"", // empty
"=", // bare equals
"BUZZ_AUTH_TAG=x", // =-in-key bypass
"BUZZ_PRIVATE_KEY=", // trailing equals
"FOO BAR", // space
" FOO", // leading whitespace
"FOO\nBAR", // newline
"FOO\0BAR", // NUL
"123_LEADING_DIGIT", // POSIX forbids leading digit
"FOO-BAR", // hyphen
"FOO.BAR", // dot
"FOO/BAR", // slash
"ünicode_key", // non-ASCII
] {
assert!(!is_well_formed_env_key(key), "{key:?} should be malformed");
}
@@ -270,15 +270,15 @@ fn is_well_formed_rejects_malformed_keys() {
#[test]
fn validate_keys_rejects_equals_in_key_bypass() {
// The actual exploit: `SPROUT_AUTH_TAG=x` smuggles a value past
// The actual exploit: `BUZZ_AUTH_TAG=x` smuggles a value past
// the reserved-key string compare and into the child's environ.
let env = map(&[("SPROUT_AUTH_TAG=x", "forged")]);
let env = map(&[("BUZZ_AUTH_TAG=x", "forged")]);
let err = validate_user_env_keys(&env).unwrap_err();
assert!(err.contains("[A-Za-z_]"), "got: {err}");
// After P2 fix the key is truncated at `=` in the error to avoid
// surfacing pasted secrets — only the prefix should appear, with an
// ellipsis marking that we elided trailing content.
assert!(err.contains("SPROUT_AUTH_TAG"), "got: {err}");
assert!(err.contains("BUZZ_AUTH_TAG"), "got: {err}");
assert!(err.contains('…'), "expected ellipsis marker: {err}");
assert!(!err.contains("=x"), "leak of value past `=`: {err}");
}
@@ -296,7 +296,7 @@ fn validate_keys_reports_malformed_before_reserved() {
// the way that other key is reserved" — they've got a typo to fix
// first. Ordering is a UX detail but pinning it stops the message
// from churning.
let env = map(&[("SPROUT_AUTH_TAG=x", "v"), ("SPROUT_PRIVATE_KEY", "v")]);
let env = map(&[("BUZZ_AUTH_TAG=x", "v"), ("BUZZ_PRIVATE_KEY", "v")]);
let err = validate_user_env_keys(&env).unwrap_err();
assert!(err.contains("[A-Za-z_]"), "got: {err}");
assert!(!err.contains("reserved"), "got: {err}");
@@ -307,12 +307,12 @@ fn merged_env_drops_malformed_keys() {
// Defense in depth: on-disk records written before the validator
// tightened must not be able to smuggle reserved keys through.
let agent = map(&[
("SPROUT_AUTH_TAG=x", "forged"),
("BUZZ_AUTH_TAG=x", "forged"),
("FOO=bar", "v"),
("LEGIT", "ok"),
]);
let merged = merged_user_env(&BTreeMap::new(), &agent);
assert!(!merged.contains_key("SPROUT_AUTH_TAG=x"));
assert!(!merged.contains_key("BUZZ_AUTH_TAG=x"));
assert!(!merged.contains_key("FOO=bar"));
assert_eq!(merged.get("LEGIT").map(String::as_str), Some("ok"));
assert_eq!(merged.len(), 1);
@@ -420,7 +420,7 @@ fn merged_env_drops_oversize_value() {
// ── derived provider/model key filter ──────────────────────────────
//
// Pack import must strip derived env keys (GOOSE_MODEL, GOOSE_PROVIDER,
// SPROUT_AGENT_MODEL, SPROUT_AGENT_PROVIDER) so they don't shadow the
// BUZZ_AGENT_MODEL, BUZZ_AGENT_PROVIDER) so they don't shadow the
// structured PersonaRecord.model / PersonaRecord.provider fields after
// the user edits them in the UI.
@@ -438,8 +438,8 @@ fn is_derived_key_matches_all_known_keys() {
fn is_derived_key_is_case_insensitive() {
assert!(is_derived_provider_model_key("goose_model"));
assert!(is_derived_provider_model_key("Goose_Provider"));
assert!(is_derived_provider_model_key("sprout_agent_model"));
assert!(is_derived_provider_model_key("SPROUT_AGENT_PROVIDER"));
assert!(is_derived_provider_model_key("buzz_agent_model"));
assert!(is_derived_provider_model_key("BUZZ_AGENT_PROVIDER"));
}
#[test]
@@ -447,7 +447,7 @@ fn is_derived_key_does_not_match_unrelated_keys() {
assert!(!is_derived_provider_model_key("GOOSE_TEMPERATURE"));
assert!(!is_derived_provider_model_key("GOOSE_CONTEXT_LIMIT"));
assert!(!is_derived_provider_model_key("ANTHROPIC_API_KEY"));
assert!(!is_derived_provider_model_key("SPROUT_PRIVATE_KEY"));
assert!(!is_derived_provider_model_key("BUZZ_PRIVATE_KEY"));
assert!(!is_derived_provider_model_key("MODEL"));
assert!(!is_derived_provider_model_key("PROVIDER"));
}
@@ -460,8 +460,8 @@ fn filter_derived_strips_provider_model_keys_preserves_rest() {
"claude-sonnet-4-20250514".to_string(),
),
("GOOSE_PROVIDER".to_string(), "anthropic".to_string()),
("SPROUT_AGENT_MODEL".to_string(), "gpt-4o".to_string()),
("SPROUT_AGENT_PROVIDER".to_string(), "openai".to_string()),
("BUZZ_AGENT_MODEL".to_string(), "gpt-4o".to_string()),
("BUZZ_AGENT_PROVIDER".to_string(), "openai".to_string()),
("GOOSE_TEMPERATURE".to_string(), "0.7".to_string()),
("ANTHROPIC_API_KEY".to_string(), "sk-test".to_string()),
];
@@ -37,7 +37,7 @@ fn relay_mesh_model_id_from_env(record: &ManagedAgentRecord) -> Option<String> {
if base_url.trim_end_matches('/') != RELAY_MESH_API_BASE_URL {
return None;
}
let provider = record.env_vars.get("SPROUT_AGENT_PROVIDER")?.trim();
let provider = record.env_vars.get("BUZZ_AGENT_PROVIDER")?.trim();
if provider != "openai" {
return None;
}
@@ -105,7 +105,7 @@ mod tests {
fn relay_mesh_model_id_detects_mesh_preset_env() {
let mut rec = fixture();
rec.env_vars = BTreeMap::from([
("SPROUT_AGENT_PROVIDER".to_string(), "openai".to_string()),
("BUZZ_AGENT_PROVIDER".to_string(), "openai".to_string()),
(
"OPENAI_COMPAT_BASE_URL".to_string(),
"http://127.0.0.1:9337/v1/".to_string(),
@@ -125,7 +125,7 @@ mod tests {
fn relay_mesh_model_id_ignores_non_mesh_openai_env() {
let mut rec = fixture();
rec.env_vars = BTreeMap::from([
("SPROUT_AGENT_PROVIDER".to_string(), "openai".to_string()),
("BUZZ_AGENT_PROVIDER".to_string(), "openai".to_string()),
(
"OPENAI_COMPAT_BASE_URL".to_string(),
"https://api.openai.com/v1".to_string(),
@@ -141,7 +141,7 @@ mod tests {
fn relay_mesh_model_id_ignores_user_openai_on_same_local_port() {
let mut rec = fixture();
rec.env_vars = BTreeMap::from([
("SPROUT_AGENT_PROVIDER".to_string(), "openai".to_string()),
("BUZZ_AGENT_PROVIDER".to_string(), "openai".to_string()),
(
"OPENAI_COMPAT_BASE_URL".to_string(),
"http://127.0.0.1:9337/v1".to_string(),
@@ -181,7 +181,7 @@ mod tests {
model_ref: "typed-model".to_string(),
});
rec.env_vars = BTreeMap::from([
("SPROUT_AGENT_PROVIDER".to_string(), "openai".to_string()),
("BUZZ_AGENT_PROVIDER".to_string(), "openai".to_string()),
(
"OPENAI_COMPAT_BASE_URL".to_string(),
"http://127.0.0.1:9337/v1".to_string(),
@@ -202,7 +202,7 @@ mod tests {
let mut rec = fixture();
rec.relay_mesh = None;
rec.env_vars = BTreeMap::from([
("SPROUT_AGENT_PROVIDER".to_string(), "openai".to_string()),
("BUZZ_AGENT_PROVIDER".to_string(), "openai".to_string()),
(
"OPENAI_COMPAT_BASE_URL".to_string(),
"http://127.0.0.1:9337/v1".to_string(),
+49 -49
View File
@@ -116,7 +116,7 @@ pub(crate) fn process_belongs_to_us(_pid: u32) -> bool {
false
}
/// The value stamped into the `SPROUT_MANAGED_AGENT` env var of every agent we
/// The value stamped into the `BUZZ_MANAGED_AGENT` env var of every agent we
/// spawn, identifying *which* desktop instance owns it. We use the app's bundle
/// identifier (`xyz.block.sprout.app` for release, `xyz.block.sprout.app.dev`
/// for `just dev`) because it is stable across restarts — a relaunched dev
@@ -128,15 +128,15 @@ pub(crate) fn current_instance_id(app: &AppHandle) -> String {
app.config().identifier.clone()
}
/// Build the full `SPROUT_MANAGED_AGENT=<instance-id>` env entry we match
/// Build the full `BUZZ_MANAGED_AGENT=<instance-id>` env entry we match
/// against when scanning processes. Kept here so the spawn stamp and the sweep
/// matcher can never drift apart.
fn sprout_marker_entry(instance_id: &str) -> Vec<u8> {
format!("SPROUT_MANAGED_AGENT={instance_id}").into_bytes()
format!("BUZZ_MANAGED_AGENT={instance_id}").into_bytes()
}
/// Check if a running process is one of *our* managed agents: it must carry
/// `SPROUT_MANAGED_AGENT=<instance_id>` in its environment, where `instance_id`
/// `BUZZ_MANAGED_AGENT=<instance_id>` in its environment, where `instance_id`
/// is this desktop instance's id. A process stamped with a *different* instance
/// id belongs to another live Sprout app and must never be reaped here.
#[cfg(target_os = "macos")]
@@ -398,7 +398,7 @@ const _: () = assert!(std::mem::size_of::<BSDInfo>() == 136);
const PROC_PIDTBSDINFO: libc::c_int = 3;
/// Enumerate all processes on the system owned by the current user and kill any
/// agent binary stamped with *this* instance's `SPROUT_MANAGED_AGENT` marker
/// agent binary stamped with *this* instance's `BUZZ_MANAGED_AGENT` marker
/// (`instance_id`) that isn't in `skip_pids`. This catches orphans that escaped
/// PID-file-based cleanup (e.g. agent workers spawned with their own process
/// group whose parent harness already exited and had its PID file removed),
@@ -762,11 +762,11 @@ fn buffer_contains_identifier(buf: &[u8], id: &[u8]) -> bool {
})
}
/// Extract the `SPROUT_MANAGED_AGENT` value from a process's environment.
/// Extract the `BUZZ_MANAGED_AGENT` value from a process's environment.
/// Returns `None` if the process doesn't have the marker or can't be read.
#[cfg(target_os = "macos")]
fn extract_sprout_marker_value(pid: u32) -> Option<String> {
let prefix = b"SPROUT_MANAGED_AGENT=";
let prefix = b"BUZZ_MANAGED_AGENT=";
let mut mib: [libc::c_int; 3] = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as libc::c_int];
let mut buf_size: libc::size_t = 0;
@@ -843,7 +843,7 @@ fn extract_sprout_marker_value(pid: u32) -> Option<String> {
#[cfg(all(unix, not(target_os = "macos")))]
fn extract_sprout_marker_value(pid: u32) -> Option<String> {
let prefix = b"SPROUT_MANAGED_AGENT=";
let prefix = b"BUZZ_MANAGED_AGENT=";
let data = std::fs::read(format!("/proc/{pid}/environ")).ok()?;
for entry in data.split(|&b| b == 0) {
if entry.starts_with(prefix) {
@@ -1022,7 +1022,7 @@ fn desktop_is_alive_for_instance(_instance_id: &str) -> bool {
/// Reap agent processes belonging to dead Sprout desktop instances.
///
/// Scans all user processes for `SPROUT_MANAGED_AGENT=*`, groups them by
/// Scans all user processes for `BUZZ_MANAGED_AGENT=*`, groups them by
/// instance ID, and for each foreign instance (≠ `our_instance_id`) checks
/// whether a Sprout desktop binary is still alive for that instance. If not,
/// all agents from that dead instance are reaped.
@@ -1404,29 +1404,29 @@ pub(crate) fn build_respond_to_env(
let mut remove: Vec<&'static str> = Vec::new();
set.push((
"SPROUT_ACP_RESPOND_TO",
"BUZZ_ACP_RESPOND_TO",
record.respond_to.as_str().to_string(),
));
if record.respond_to == super::types::RespondTo::Allowlist {
set.push(("SPROUT_ACP_RESPOND_TO_ALLOWLIST", normalized.join(",")));
set.push(("BUZZ_ACP_RESPOND_TO_ALLOWLIST", normalized.join(",")));
} else {
remove.push("SPROUT_ACP_RESPOND_TO_ALLOWLIST");
remove.push("BUZZ_ACP_RESPOND_TO_ALLOWLIST");
}
// Legacy fallback: agents created before NIP-OA lack `auth_tag`. Without
// it the harness can't resolve the owner, and owner-dependent gate modes
// would drop every event. Forwarding the workspace owner pubkey via
// SPROUT_ACP_AGENT_OWNER keeps those records functional. Modern records
// (`auth_tag = Some(...)`) use `SPROUT_AUTH_TAG` as before.
// BUZZ_ACP_AGENT_OWNER keeps those records functional. Modern records
// (`auth_tag = Some(...)`) use `BUZZ_AUTH_TAG` as before.
if record.auth_tag.is_none() {
if let Some(owner) = owner_hex {
set.push(("SPROUT_ACP_AGENT_OWNER", owner.to_string()));
set.push(("BUZZ_ACP_AGENT_OWNER", owner.to_string()));
} else {
remove.push("SPROUT_ACP_AGENT_OWNER");
remove.push("BUZZ_ACP_AGENT_OWNER");
}
} else {
remove.push("SPROUT_ACP_AGENT_OWNER");
remove.push("BUZZ_ACP_AGENT_OWNER");
}
Ok((set, remove))
@@ -1535,16 +1535,16 @@ pub fn spawn_agent_child(
command.env("PATH", path);
}
command.env("RUST_LOG", child_rust_log_filter());
command.env("SPROUT_PRIVATE_KEY", &record.private_key_nsec);
command.env("SPROUT_RELAY_URL", &record.relay_url);
command.env("SPROUT_ACP_AGENT_COMMAND", &resolved_agent_command);
command.env("SPROUT_ACP_AGENT_ARGS", agent_args.join(","));
command.env("BUZZ_PRIVATE_KEY", &record.private_key_nsec);
command.env("BUZZ_RELAY_URL", &record.relay_url);
command.env("BUZZ_ACP_AGENT_COMMAND", &resolved_agent_command);
command.env("BUZZ_ACP_AGENT_ARGS", agent_args.join(","));
match &resolved_mcp_command {
Some(mcp_cmd) => {
command.env("SPROUT_ACP_MCP_COMMAND", mcp_cmd);
command.env("BUZZ_ACP_MCP_COMMAND", mcp_cmd);
}
None => {
command.env("SPROUT_ACP_MCP_COMMAND", "");
command.env("BUZZ_ACP_MCP_COMMAND", "");
}
}
// Enable MCP hook tools (_Stop, _PostCompact) for agents that need them.
@@ -1553,23 +1553,23 @@ pub fn spawn_agent_child(
if runtime_meta.is_some_and(|r| r.mcp_hooks) {
command.env("MCP_HOOK_SERVERS", "*");
}
// Only emit SPROUT_ACP_IDLE_TIMEOUT when the user has explicitly set an
// Only emit BUZZ_ACP_IDLE_TIMEOUT when the user has explicitly set an
// override. When unset, the sprout-acp harness applies its own default
// (see `DEFAULT_IDLE_TIMEOUT_SECS` in crates/sprout-acp/src/config.rs),
// which is the single source of truth. The previously-emitted
// `SPROUT_ACP_TURN_TIMEOUT` is deprecated upstream and was pinning every
// `BUZZ_ACP_TURN_TIMEOUT` is deprecated upstream and was pinning every
// agent to the desktop's stale default (320s), bypassing harness bumps.
if let Some(idle) = record.idle_timeout_seconds {
command.env("SPROUT_ACP_IDLE_TIMEOUT", idle.to_string());
command.env("BUZZ_ACP_IDLE_TIMEOUT", idle.to_string());
}
let max_dur = record
.max_turn_duration_seconds
.unwrap_or(super::types::DEFAULT_AGENT_MAX_TURN_DURATION_SECONDS);
command.env("SPROUT_ACP_MAX_TURN_DURATION", max_dur.to_string());
command.env("SPROUT_ACP_AGENTS", record.parallelism.to_string());
command.env("SPROUT_ACP_MULTIPLE_EVENT_HANDLING", "owner-interrupt");
command.env("SPROUT_ACP_DEDUP", "queue");
command.env("BUZZ_ACP_MAX_TURN_DURATION", max_dur.to_string());
command.env("BUZZ_ACP_AGENTS", record.parallelism.to_string());
command.env("BUZZ_ACP_MULTIPLE_EVENT_HANDLING", "owner-interrupt");
command.env("BUZZ_ACP_DEDUP", "queue");
if let Some(meta) = runtime_meta {
for (key, value) in meta.default_env {
if std::env::var(key).is_err() {
@@ -1580,8 +1580,8 @@ pub fn spawn_agent_child(
if let (Some(team_dir), Some(persona_name)) =
(&record.persona_team_dir, &record.persona_name_in_team)
{
command.env("SPROUT_ACP_PERSONA_PACK", team_dir);
command.env("SPROUT_ACP_PERSONA_NAME", persona_name);
command.env("BUZZ_ACP_PERSONA_PACK", team_dir);
command.env("BUZZ_ACP_PERSONA_NAME", persona_name);
}
// Resolve system prompt, model, and provider: the linked persona is the
@@ -1598,14 +1598,14 @@ pub fn spawn_agent_child(
);
if let Some(prompt) = &effective_prompt {
command.env("SPROUT_ACP_SYSTEM_PROMPT", prompt);
command.env("BUZZ_ACP_SYSTEM_PROMPT", prompt);
} else {
command.env_remove("SPROUT_ACP_SYSTEM_PROMPT");
command.env_remove("BUZZ_ACP_SYSTEM_PROMPT");
}
if let Some(model) = &effective_model {
command.env("SPROUT_ACP_MODEL", model);
command.env("BUZZ_ACP_MODEL", model);
} else {
command.env_remove("SPROUT_ACP_MODEL");
command.env_remove("BUZZ_ACP_MODEL");
}
if let Some(meta) = runtime_meta {
for (key, value) in runtime_metadata_env_vars(
@@ -1619,18 +1619,18 @@ pub fn spawn_agent_child(
}
}
if let Some(toolsets) = &record.mcp_toolsets {
command.env("SPROUT_TOOLSETS", toolsets);
command.env("BUZZ_TOOLSETS", toolsets);
} else {
command.env("SPROUT_TOOLSETS", "default,canvas,forums,dms,media");
command.env("BUZZ_TOOLSETS", "default,canvas,forums,dms,media");
}
command.env_remove("SPROUT_ACP_PRIVATE_KEY");
command.env_remove("SPROUT_ACP_API_TOKEN");
command.env_remove("SPROUT_API_TOKEN");
command.env_remove("BUZZ_ACP_PRIVATE_KEY");
command.env_remove("BUZZ_ACP_API_TOKEN");
command.env_remove("BUZZ_API_TOKEN");
if let Some(ref auth_tag) = record.auth_tag {
command.env("SPROUT_AUTH_TAG", auth_tag);
command.env("BUZZ_AUTH_TAG", auth_tag);
} else {
command.env_remove("SPROUT_AUTH_TAG");
command.env_remove("BUZZ_AUTH_TAG");
}
// Inbound author gate: who is this agent allowed to respond to?
@@ -1645,7 +1645,7 @@ pub fn spawn_agent_child(
command.env_remove(key);
}
command.env("SPROUT_ACP_RELAY_OBSERVER", "true");
command.env("BUZZ_ACP_RELAY_OBSERVER", "true");
// ── Git credential helper for Sprout relay ──────────────────────────
//
@@ -1657,7 +1657,7 @@ pub fn spawn_agent_child(
// filesystem writes) scoped to the relay's git URL so we don't
// interfere with other remotes (e.g. GitHub).
//
// NOSTR_PRIVATE_KEY mirrors SPROUT_PRIVATE_KEY — keep in sync.
// NOSTR_PRIVATE_KEY mirrors BUZZ_PRIVATE_KEY — keep in sync.
if let Some(cred_helper) = resolve_command("git-credential-nostr") {
let relay_http_url = crate::relay::relay_http_base_url(&record.relay_url);
@@ -1692,9 +1692,9 @@ pub fn spawn_agent_child(
//
// Precedence: desktop parent env < persona env_vars < agent env_vars.
// These writes go LAST so user-provided values win over every Sprout-set
// env above — EXCEPT reserved keys (SPROUT_PRIVATE_KEY, NOSTR_PRIVATE_KEY,
// SPROUT_AUTH_TAG, SPROUT_API_TOKEN, SPROUT_ACP_PRIVATE_KEY,
// SPROUT_ACP_API_TOKEN), which `merged_user_env` strips. Those carry
// env above — EXCEPT reserved keys (BUZZ_PRIVATE_KEY, NOSTR_PRIVATE_KEY,
// BUZZ_AUTH_TAG, BUZZ_API_TOKEN, BUZZ_ACP_PRIVATE_KEY,
// BUZZ_ACP_API_TOKEN), which `merged_user_env` strips. Those carry
// Sprout's identity and must never be GUI-overridable.
// Fail closed on persona-lookup errors: persona env_vars carry API
// credentials, so silently substituting an empty map would spawn an
@@ -1710,7 +1710,7 @@ pub fn spawn_agent_child(
// agents). Propagates automatically through the full tree (sprout-acp →
// goose → MCP servers) because neither sprout-acp nor goose calls
// env_clear().
command.env("SPROUT_MANAGED_AGENT", current_instance_id(app));
command.env("BUZZ_MANAGED_AGENT", current_instance_id(app));
// Spawn the harness in its own process group so we can kill the entire
// tree (harness + MCP servers + agent subprocesses) on shutdown.
@@ -29,7 +29,7 @@ fn identifier_exact_match_with_quote_boundary() {
#[test]
fn identifier_match_with_null_boundary() {
// In KERN_PROCARGS2, entries are null-delimited.
let mut buf = b"SPROUT_MANAGED_AGENT=xyz.block.sprout.app.dev".to_vec();
let mut buf = b"BUZZ_MANAGED_AGENT=xyz.block.sprout.app.dev".to_vec();
buf.push(0);
buf.extend_from_slice(b"OTHER_VAR=value");
let id = b"xyz.block.sprout.app.dev";
@@ -71,7 +71,7 @@ fn marker_entry_is_namespaced_by_instance_id() {
// release build's (`...app`) agents.
assert_eq!(
super::sprout_marker_entry("xyz.block.sprout.app"),
b"SPROUT_MANAGED_AGENT=xyz.block.sprout.app".to_vec()
b"BUZZ_MANAGED_AGENT=xyz.block.sprout.app".to_vec()
);
assert_ne!(
super::sprout_marker_entry("xyz.block.sprout.app"),
@@ -167,13 +167,13 @@ fn build_env_owner_only_sets_mode_and_removes_others() {
let (set, remove) = build_respond_to_env(&rec, Some("owner")).unwrap();
let set_map: std::collections::HashMap<_, _> = set.into_iter().collect();
assert_eq!(
set_map.get("SPROUT_ACP_RESPOND_TO").map(String::as_str),
set_map.get("BUZZ_ACP_RESPOND_TO").map(String::as_str),
Some("owner-only")
);
assert!(!set_map.contains_key("SPROUT_ACP_RESPOND_TO_ALLOWLIST"));
assert!(remove.contains(&"SPROUT_ACP_RESPOND_TO_ALLOWLIST"));
assert!(!set_map.contains_key("BUZZ_ACP_RESPOND_TO_ALLOWLIST"));
assert!(remove.contains(&"BUZZ_ACP_RESPOND_TO_ALLOWLIST"));
// auth_tag is present → no AGENT_OWNER fallback fires.
assert!(remove.contains(&"SPROUT_ACP_AGENT_OWNER"));
assert!(remove.contains(&"BUZZ_ACP_AGENT_OWNER"));
}
#[test]
@@ -188,12 +188,12 @@ fn build_env_allowlist_sets_both_envs_and_joins() {
let (set, _remove) = build_respond_to_env(&rec, Some("owner")).unwrap();
let set_map: std::collections::HashMap<_, _> = set.into_iter().collect();
assert_eq!(
set_map.get("SPROUT_ACP_RESPOND_TO").map(String::as_str),
set_map.get("BUZZ_ACP_RESPOND_TO").map(String::as_str),
Some("allowlist")
);
assert_eq!(
set_map
.get("SPROUT_ACP_RESPOND_TO_ALLOWLIST")
.get("BUZZ_ACP_RESPOND_TO_ALLOWLIST")
.map(String::as_str),
Some(format!("{a},{b}").as_str()),
);
@@ -205,11 +205,11 @@ fn build_env_anyone_omits_allowlist_var() {
let (set, remove) = build_respond_to_env(&rec, Some("owner")).unwrap();
let set_map: std::collections::HashMap<_, _> = set.into_iter().collect();
assert_eq!(
set_map.get("SPROUT_ACP_RESPOND_TO").map(String::as_str),
set_map.get("BUZZ_ACP_RESPOND_TO").map(String::as_str),
Some("anyone")
);
assert!(!set_map.contains_key("SPROUT_ACP_RESPOND_TO_ALLOWLIST"));
assert!(remove.contains(&"SPROUT_ACP_RESPOND_TO_ALLOWLIST"));
assert!(!set_map.contains_key("BUZZ_ACP_RESPOND_TO_ALLOWLIST"));
assert!(remove.contains(&"BUZZ_ACP_RESPOND_TO_ALLOWLIST"));
}
#[test]
@@ -218,10 +218,10 @@ fn build_env_legacy_record_without_auth_tag_emits_agent_owner() {
let (set, remove) = build_respond_to_env(&rec, Some("ownerhex")).unwrap();
let set_map: std::collections::HashMap<_, _> = set.into_iter().collect();
assert_eq!(
set_map.get("SPROUT_ACP_AGENT_OWNER").map(String::as_str),
set_map.get("BUZZ_ACP_AGENT_OWNER").map(String::as_str),
Some("ownerhex")
);
assert!(!remove.contains(&"SPROUT_ACP_AGENT_OWNER"));
assert!(!remove.contains(&"BUZZ_ACP_AGENT_OWNER"));
}
#[test]
@@ -230,7 +230,7 @@ fn build_env_legacy_record_without_owner_hex_removes_agent_owner() {
// env var from the parent.
let rec = fixture(RespondTo::OwnerOnly, vec![], None);
let (_set, remove) = build_respond_to_env(&rec, None).unwrap();
assert!(remove.contains(&"SPROUT_ACP_AGENT_OWNER"));
assert!(remove.contains(&"BUZZ_ACP_AGENT_OWNER"));
}
#[test]
@@ -377,8 +377,8 @@ fn runtime_metadata_env_vars_injects_model_even_with_acp_model_switching() {
// sprout-agent has supports_acp_model_switching=true but we still inject
// the model env var because ACP model switching is post-bootstrap
let vars = runtime_metadata_env_vars(
Some("SPROUT_AGENT_MODEL"),
Some("SPROUT_AGENT_PROVIDER"),
Some("BUZZ_AGENT_MODEL"),
Some("BUZZ_AGENT_PROVIDER"),
false,
Some("goose-claude-4-6-opus"),
Some("databricks"),
@@ -386,8 +386,8 @@ fn runtime_metadata_env_vars_injects_model_even_with_acp_model_switching() {
assert_eq!(
vars,
vec![
("SPROUT_AGENT_MODEL", "goose-claude-4-6-opus"),
("SPROUT_AGENT_PROVIDER", "databricks"),
("BUZZ_AGENT_MODEL", "goose-claude-4-6-opus"),
("BUZZ_AGENT_PROVIDER", "databricks"),
]
);
}
+2 -2
View File
@@ -19,8 +19,8 @@ use serde::{Deserialize, Serialize};
const DEFAULT_MESH_API_PORT: u16 = 9337;
const DEFAULT_MESH_CONSOLE_PORT: u16 = 3131;
const MESH_STATUS_KIND: u64 = 30_621;
const MESH_API_PORT_ENV: &str = "SPROUT_MESH_API_PORT";
const MESH_CONSOLE_PORT_ENV: &str = "SPROUT_MESH_CONSOLE_PORT";
const MESH_API_PORT_ENV: &str = "BUZZ_MESH_API_PORT";
const MESH_CONSOLE_PORT_ENV: &str = "BUZZ_MESH_CONSOLE_PORT";
const RELAY_MESH_API_KEY_PLACEHOLDER: &str = "sprout-mesh-local";
/// ACP provider relay-mesh agents run on. Sources of truth for its command +
/// MCP live in the runtime catalog (`known_acp_runtime_exact`); these are
+1 -1
View File
@@ -52,7 +52,7 @@ fn agent_preset_runs_on_sprout_agent_not_goose() {
assert_eq!(
preset
.env_vars
.get("SPROUT_AGENT_PROVIDER")
.get("BUZZ_AGENT_PROVIDER")
.map(String::as_str),
Some("openai")
);
+1 -1
View File
@@ -53,7 +53,7 @@ pub fn agent_preset(request: MeshAgentPresetRequest) -> Result<MeshAgentPreset,
mcp_command,
model: model.to_string(),
env_vars: BTreeMap::from([
("SPROUT_AGENT_PROVIDER".to_string(), "openai".to_string()),
("BUZZ_AGENT_PROVIDER".to_string(), "openai".to_string()),
(
"OPENAI_COMPAT_BASE_URL".to_string(),
relay_mesh_api_base_url()?,