mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): hash spawn config as the env receives it, not raw record fields (#1621)
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
@@ -1752,7 +1752,7 @@ pub fn spawn_agent_child(
|
||||
if let Some(toolsets) = &record.mcp_toolsets {
|
||||
command.env("BUZZ_TOOLSETS", toolsets);
|
||||
} else {
|
||||
command.env("BUZZ_TOOLSETS", "default,canvas,forums,dms,media");
|
||||
command.env("BUZZ_TOOLSETS", super::types::DEFAULT_MCP_TOOLSETS);
|
||||
}
|
||||
command.env_remove("BUZZ_ACP_PRIVATE_KEY");
|
||||
command.env_remove("BUZZ_ACP_API_TOKEN");
|
||||
|
||||
@@ -92,11 +92,31 @@ pub(crate) fn spawn_config_hash(
|
||||
record.provider.hash(&mut hasher);
|
||||
record.auth_tag.hash(&mut hasher);
|
||||
record.respond_to.as_str().hash(&mut hasher);
|
||||
record.respond_to_allowlist.hash(&mut hasher);
|
||||
// The allowlist is hashed as the env receives it: spawn sets
|
||||
// BUZZ_ACP_RESPOND_TO_ALLOWLIST only in allowlist mode, and normalized
|
||||
// (trim/lowercase/dedup via `validate_respond_to_allowlist`) — so edits
|
||||
// that don't survive normalization, or edits while another mode is
|
||||
// active, must not badge. A list spawn would reject hashes raw: the
|
||||
// stamped hash comes from a successful spawn, so any invalid edit
|
||||
// correctly compares unequal.
|
||||
if record.respond_to == super::types::RespondTo::Allowlist {
|
||||
super::types::validate_respond_to_allowlist(&record.respond_to_allowlist)
|
||||
.unwrap_or_else(|_| record.respond_to_allowlist.clone())
|
||||
.hash(&mut hasher);
|
||||
}
|
||||
record.idle_timeout_seconds.hash(&mut hasher);
|
||||
record.max_turn_duration_seconds.hash(&mut hasher);
|
||||
// Spawn writes BUZZ_ACP_MAX_TURN_DURATION and BUZZ_TOOLSETS with defaults
|
||||
// filled in, so None and an explicit default are the same spawned value.
|
||||
record
|
||||
.max_turn_duration_seconds
|
||||
.unwrap_or(super::types::DEFAULT_AGENT_MAX_TURN_DURATION_SECONDS)
|
||||
.hash(&mut hasher);
|
||||
record.parallelism.hash(&mut hasher);
|
||||
record.mcp_toolsets.hash(&mut hasher);
|
||||
record
|
||||
.mcp_toolsets
|
||||
.as_deref()
|
||||
.unwrap_or(super::types::DEFAULT_MCP_TOOLSETS)
|
||||
.hash(&mut hasher);
|
||||
record.persona_team_dir.hash(&mut hasher);
|
||||
record.persona_name_in_team.hash(&mut hasher);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use crate::managed_agents::types::RespondTo;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
fn record() -> ManagedAgentRecord {
|
||||
@@ -183,6 +184,7 @@ fn workspace_relay_change_ignored_for_pinned_record_relay() {
|
||||
fn respond_to_allowlist_edit_changes_hash() {
|
||||
let rec = record();
|
||||
let mut edited = record();
|
||||
edited.respond_to = RespondTo::Allowlist;
|
||||
edited.respond_to_allowlist = vec!["a".repeat(64)];
|
||||
assert_ne!(
|
||||
spawn_config_hash(&rec, &[], "wss://ws.example"),
|
||||
@@ -190,6 +192,99 @@ fn respond_to_allowlist_edit_changes_hash() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allowlist_ignored_when_mode_is_not_allowlist() {
|
||||
// Spawn only sets BUZZ_ACP_RESPOND_TO_ALLOWLIST in allowlist mode, so
|
||||
// editing the (dormant) list under owner-only must not badge.
|
||||
let rec = record();
|
||||
let mut edited = record();
|
||||
edited.respond_to_allowlist = vec!["a".repeat(64)];
|
||||
assert_eq!(
|
||||
spawn_config_hash(&rec, &[], "wss://ws.example"),
|
||||
spawn_config_hash(&edited, &[], "wss://ws.example")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allowlist_normalization_equivalent_edits_do_not_change_hash() {
|
||||
// The env receives the normalized list (trim/lowercase/dedup), so edits
|
||||
// that normalize to the same value must not badge.
|
||||
let mut rec = record();
|
||||
rec.respond_to = RespondTo::Allowlist;
|
||||
rec.respond_to_allowlist = vec!["a".repeat(64)];
|
||||
let mut edited = rec.clone();
|
||||
edited.respond_to_allowlist = vec![
|
||||
format!(" {} ", "A".repeat(64)), // whitespace + case
|
||||
"a".repeat(64), // duplicate
|
||||
];
|
||||
assert_eq!(
|
||||
spawn_config_hash(&rec, &[], "wss://ws.example"),
|
||||
spawn_config_hash(&edited, &[], "wss://ws.example")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allowlist_content_edit_still_changes_hash() {
|
||||
let mut rec = record();
|
||||
rec.respond_to = RespondTo::Allowlist;
|
||||
rec.respond_to_allowlist = vec!["a".repeat(64)];
|
||||
let mut edited = rec.clone();
|
||||
edited.respond_to_allowlist = vec!["b".repeat(64)];
|
||||
assert_ne!(
|
||||
spawn_config_hash(&rec, &[], "wss://ws.example"),
|
||||
spawn_config_hash(&edited, &[], "wss://ws.example")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_default_max_turn_duration_does_not_change_hash() {
|
||||
// Spawn writes BUZZ_ACP_MAX_TURN_DURATION with the default filled in, so
|
||||
// None → Some(default) is the same spawned value and must not badge.
|
||||
let rec = record();
|
||||
let mut edited = record();
|
||||
edited.max_turn_duration_seconds =
|
||||
Some(crate::managed_agents::types::DEFAULT_AGENT_MAX_TURN_DURATION_SECONDS);
|
||||
assert_eq!(
|
||||
spawn_config_hash(&rec, &[], "wss://ws.example"),
|
||||
spawn_config_hash(&edited, &[], "wss://ws.example")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_default_max_turn_duration_changes_hash() {
|
||||
let rec = record();
|
||||
let mut edited = record();
|
||||
edited.max_turn_duration_seconds = Some(42);
|
||||
assert_ne!(
|
||||
spawn_config_hash(&rec, &[], "wss://ws.example"),
|
||||
spawn_config_hash(&edited, &[], "wss://ws.example")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_default_toolsets_do_not_change_hash() {
|
||||
// Spawn falls BUZZ_TOOLSETS back to the default set, so None → an
|
||||
// explicit copy of the default is the same spawned value.
|
||||
let rec = record();
|
||||
let mut edited = record();
|
||||
edited.mcp_toolsets = Some(crate::managed_agents::types::DEFAULT_MCP_TOOLSETS.to_string());
|
||||
assert_eq!(
|
||||
spawn_config_hash(&rec, &[], "wss://ws.example"),
|
||||
spawn_config_hash(&edited, &[], "wss://ws.example")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_default_toolsets_change_hash() {
|
||||
let rec = record();
|
||||
let mut edited = record();
|
||||
edited.mcp_toolsets = Some("default,canvas".to_string());
|
||||
assert_ne!(
|
||||
spawn_config_hash(&rec, &[], "wss://ws.example"),
|
||||
spawn_config_hash(&edited, &[], "wss://ws.example")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_spawn_bookkeeping_fields_do_not_change_hash() {
|
||||
// updated_at / runtime_pid / last_* are lifecycle bookkeeping, not spawn
|
||||
|
||||
@@ -768,6 +768,9 @@ pub const DEFAULT_AGENT_TURN_TIMEOUT_SECONDS: u64 = 320;
|
||||
/// 1 hour — absolute wall-clock safety cap per turn.
|
||||
pub const DEFAULT_AGENT_MAX_TURN_DURATION_SECONDS: u64 = 3600;
|
||||
pub const DEFAULT_AGENT_PARALLELISM: u32 = 24;
|
||||
/// Toolsets injected as `BUZZ_TOOLSETS` when the record doesn't pin its own —
|
||||
/// single source of truth for the spawn env and the spawn-config hash.
|
||||
pub const DEFAULT_MCP_TOOLSETS: &str = "default,canvas,forums,dms,media";
|
||||
|
||||
fn default_agent_parallelism() -> u32 {
|
||||
DEFAULT_AGENT_PARALLELISM
|
||||
|
||||
Reference in New Issue
Block a user