From 29299d5acb42833da6dcbf0001994af45a47f4f7 Mon Sep 17 00:00:00 2001 From: sniperguard Date: Sat, 1 Aug 2026 12:56:44 +0300 Subject: [PATCH] fix(acp): accept missing_binary and unknown setup surfaces `buzz-desktop` emits a `missing_binary` setup requirement when a managed agent's configured harness command cannot be resolved in PATH (desktop/src-tauri/src/managed_agents/runtime.rs). `buzz-acp` has no matching `RequirementPayload` variant, so `SetupPayload::from_env()` fails with a serde error and the harness exits 1 before it can nudge: the agent crash-loops at startup and never comes online. Add the `MissingBinary { command }` variant, and a `#[serde(other)]` `Unknown` fallback so a surface added on the desktop side degrades to a generic nudge instead of aborting the harness. Both sides ship in one bundle but are versioned independently during development, so this skew is reachable again. Repro on Buzz Desktop 0.5.3 (macOS arm64): give a managed agent a bare binary name that is not on the GUI app's PATH. The agent log shows the serde error and exit code 1, retried twice ~185ms apart. Setting an absolute path in Edit Agent -> Command works around it. Co-Authored-By: Claude Opus 5 Signed-off-by: sniperguard --- crates/buzz-acp/src/setup_mode.rs | 51 +++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/crates/buzz-acp/src/setup_mode.rs b/crates/buzz-acp/src/setup_mode.rs index b1a9372ea4..2b2ab9e537 100644 --- a/crates/buzz-acp/src/setup_mode.rs +++ b/crates/buzz-acp/src/setup_mode.rs @@ -115,6 +115,18 @@ pub(crate) enum RequirementPayload { }, /// Git for Windows is missing; open Agent runtimes for the installation guide. GitBash, + /// The configured harness command could not be resolved in the agent's PATH. + /// Buzz-managed, so Edit Agent can fix it — usually by giving an absolute path. + MissingBinary { command: String }, + /// A surface this build doesn't know about. + /// + /// The desktop and `buzz-acp` ship in the same bundle but are versioned + /// independently in development, and a surface added on the desktop side + /// used to abort the harness at startup with a serde error — the agent + /// crash-looped instead of nudging. Unknown surfaces degrade to a generic + /// nudge so the agent still starts and still tells the user something. + #[serde(other)] + Unknown, } impl RequirementPayload { @@ -189,6 +201,15 @@ impl RequirementPayload { RequirementPayload::GitBash => { "install Git for Windows (open Agent runtimes in Settings to diagnose)".to_string() } + RequirementPayload::MissingBinary { command } => { + format!( + "`{}` was not found in PATH — install it, or set an absolute path in Edit Agent → Command", + command + ) + } + RequirementPayload::Unknown => { + "this agent reported a configuration problem this version can't describe — open Edit Agent to review its settings".to_string() + } } } } @@ -687,6 +708,36 @@ mod tests { assert_eq!(payload.requirements.len(), 2); } + #[test] + fn setup_payload_deserializes_missing_binary_requirement() { + // Regression: the desktop emits this surface for a harness command it + // can't resolve in PATH. `buzz-acp` had no matching variant, so the + // harness aborted with a serde error and the agent crash-looped. + let payload: SetupPayload = serde_json::from_str( + r#"{"agent_name":"Kimi","agent_pubkey":"test","requirements":[{"surface":"missing_binary","command":"kimi"}]}"#, + ) + .unwrap(); + assert!(matches!( + payload.requirements.as_slice(), + [RequirementPayload::MissingBinary { command }] if command == "kimi" + )); + assert!(payload.requirements[0].instruction().contains("kimi")); + } + + #[test] + fn setup_payload_tolerates_unknown_surface() { + // Forward compatibility: a surface this build has never heard of must + // degrade to a generic nudge, never abort the harness. + let payload: SetupPayload = serde_json::from_str( + r#"{"agent_name":"Fizz","agent_pubkey":"test","requirements":[{"surface":"surface_from_the_future","detail":"whatever"}]}"#, + ) + .expect("unknown surface must parse, not error"); + assert!(matches!( + payload.requirements.as_slice(), + [RequirementPayload::Unknown] + )); + } + #[test] fn setup_payload_deserializes_git_bash_requirement() { let payload: SetupPayload = serde_json::from_str(