Files
buzz/docs
DuncanandWill Pfleger 127298f1f9 feat(acp): implement permission policy (#4938)
Add a three-value BUZZ_ACP_PERMISSION_POLICY (allow | ask | reject) that
gates how session/request_permission calls are handled:

- reject (headless default): synchronous denial, byte-for-byte unchanged
  from today's dontAsk behavior; ResolvedPermissionConfig derives dontAsk
  mode so the adapter self-denies before Buzz sees the request.

- allow: synchronous auto-selection of the unique allow_once option from
  the exact options in the request; zero/multiple allow_once candidates
  or malformed options fail closed with a denial. Never allow_always,
  never hardcoded IDs.

- ask: interactive — emits an acp_read telemetry frame with an
  authorization envelope (requestNonce, actionable, reason) and registers
  a pending entry in a bounded map (cap=8) on AcpClient. The desktop
  delivers a permission_decision control frame carrying the nonce and
  chosen optionId; the read loop matches by nonce, validates the optionId
  against the captured option snapshot, and writes the ACP response.
  Per-request timeout min(300s, remaining hard deadline) fails closed.

Key implementation details:

- ResolvedPermissionConfig computed once at startup; transmits
  effective_mode via set_config_option for every agent that advertises
  the mode field (goose skipped).

- Admission preflight (synchronous, before map insertion): options
  nonempty, count ≤ 16, every optionId unique+nonempty, required
  kind/name fields, duplicate live requestId → immediate denial with
  original untouched, map at cap → deny, serialized payload ≤
  OBSERVER_MAX_PLAINTEXT_LEN.

- Cancel during writing → PermissionPoisoned error: surfaces through
  cancel_with_cleanup_grace so classify_control_cancel_failure triggers
  respawn (not pool return). PermissionPoisoned added to is_transport_error.
  Pending entries drained with cancelled responses before session/cancel.

- ask without observer or unresolved owner downgrades to reject with a
  loud warning.

- acp_read generic emit suppressed for ask permission requests; replaced
  with a single post-preflight enveloped emit (one frame per request).

- Decision receiver arm placed ahead of reader arm in the biased select!
  for inbound fairness.

- ObserverEvent gains optional authorization: Option<AuthorizationEnvelope>
  with skip_serializing_if. Payload bytes remain raw ACP, never mutated.

- NIP-AO.md reconciled: adds authorization envelope, permission_decision
  control type, control_result telemetry kind, switch_model control type,
  single-use nonce semantics, best-effort delivery with mandatory timeout,
  cancel-during-write poison behavior, and 5-minute desktop live lookback.

Tests: 720 passing (31 new pinned tests covering mode matrix, admission
preflight, allow selector, ask map lifecycle, cancel-during-writing poison,
policy × mode combinations, and decision arm behavior).

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
2026-08-06 16:17:26 -04:00
..
…