mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Every LLM-issued MCP tool call now asks the client to authorize it before executing. buzz-agent always asks; the client applies BUZZ_ACP_PERMISSION_POLICY. The agent never reads policy, matching the layering of the other ACP harnesses. A crate-local PermissionBroker owns the full correlation lifecycle: a process-wide admission semaphore (BUZZ_AGENT_MAX_PENDING_PERMISSIONS, default 32) acquired before any correlation entry is inserted, a monotonic id allocator, an abort-safe PendingPermission lease whose Drop synchronously removes the entry and releases the slot, claim-before-wake delivery for at-most-once resolution, and a single absolute deadline (BUZZ_AGENT_PERMISSION_TIMEOUT_SECS, default 330s) shared by admission and response wait. Cancellation races inside the wait, never depending on the outer abort drain. The request builder is version-aware, keyed on the protocol version negotiated at initialize and stored for the connection lifetime: v2 nests the tool call under subject, v1 uses the legacy top-level shape. Authorization is fail-closed: execute IFF outcome is "selected" and the selected optionId equals the offered allow option; every other shape denies with a synthetic tool error and the turn continues. Argument-shape validation is hoisted ahead of the ask so a malformed call is rejected locally without prompting. load_skill and _Stop/ _PostCompact lifecycle hooks are exempt — they are not model-issued. Tests: a subprocess + fake-MCP boundary suite proves no call reaches MCP before approval, exact-allow reaches it once, and reject/cancelled/ error/malformed/unknown-outcome/wrong-option/stale-id all fail closed, plus crossed-parallel isolation and the two exemptions; broker unit tests prove timeout, abort, and multi-session admission invariants with an injectable deadline. Co-authored-by: Will Pfleger <pfleger.will@gmail.com> Signed-off-by: Will Pfleger <pfleger.will@gmail.com>