Skip to content

Park guest permission requests until an approver answers - #21

Open
QuicksilverSlick wants to merge 2 commits into
mainfrom
feat/approval-park
Open

Park guest permission requests until an approver answers#21
QuicksilverSlick wants to merge 2 commits into
mainfrom
feat/approval-park

Conversation

@QuicksilverSlick

Copy link
Copy Markdown
Owner

What this does

Slice A of the approval loop. A guest turn can now park a session/request_permission instead of refusing it outright. Nothing is written back to the agent until a verdict arrives on that request's own oneshot, or until the wait expires to reject_once.

Production behaviour is unchanged, for three reasons:

  • Every prompt call in pool.rs still passes no approval link, so nothing parks.
  • Nothing routes a parked request to the owner yet (slice B).
  • Shared agents still run in bypassPermissions (slice C).

Commits

  1. feat(buzz-acp): park guest permission requests until an approver answers
    • approval_gate.rs (new, pure, unit-tested on every platform):
      • Parking limits: a 600 s wait, 4 parked requests per connection, 6 asks per turn, a 30 s minimum window, and a 10 s margin inside the hard deadline.
      • RequestDigest, which binds each verdict to its request.
      • DenyReason.
      • AllowVia. Standing grants are refunded against the per-turn cap.
      • TurnClock. While a request waits, the idle deadline is suspended and the hard deadline is credited for the wait, capped at 3 timeouts.
    • pending_permissions.rs: parked asks, retirement, and a lazy verdict poll.
    • acp.rs: the read loop polls verdicts first under biased and expires overdue asks, closing the channel before writing the refusal. It answers every held or owed request on exit and on cancel.
    • Six review fixes, each with a test that fails when the fix is undone.
  2. fix(buzz-acp): report a prompt error read just before a control signal
    • Found while verifying the review fixes.
    • The bug: once the prompt's JSON-RPC error has been read, the prompt no longer counts as in flight. A steer, interrupt or stop that dropped the prompt future during its exit drain therefore sent the pool down its "completed before the control signal" branch. That branch reported the failed turn as a success, recorded its batch as delivered, and never retried it.
    • The fix: the client keeps the error until the prompt call returns. The branch now reports it as PromptOutcome::Error, with no delivery record. The batch follows the signal, as it would after a cancel (classify_completed_before_control_signal, table-tested).

Known limits (for slice B)

  • Restarts. Held requests live only in harness memory, so a harness or desktop restart ends them. Slice B has to reconcile any approval cards it posted when it starts up.
  • Stale owed ids. These are answered after last_prompt_id is set, which the Race 1 invariant requires. A drop during those writes ends as CancelDrainTimeout after the 5 s grace. That is a safe failure, never a false success.
  • Expiry test gap. an_expired_ask_is_closed_while_its_refusal_is_still_being_written cannot tell close() from drop(). A verdict that lands between try_recv and close() is not covered deterministically.
  • Bootstrap runs as Owner. The session bootstrap prompt (initial_message, pool.rs) is sent as TurnAuthority::Owner. Before slice C turns the gate on, confirm that nothing a guest writes can reach it.

Tests

  • Windows, cargo test -p buzz-acp --lib: 944 passed, 32 failed. By name, the 32 are the 29 known Windows timing failures plus the 3 wire tests from Stop steered guest messages and session setup inheriting the owner's trust #19 that need Unix bash.
  • Lint: cargo fmt --check and cargo clippy -p buzz-acp --all-targets are clean.
  • Runs on Windows: the new stuck-write tests use only cat and sleep.
  • Mutation checks: each review fix, and each part of the Race 1 fix, was undone in turn, and its test failed.

🤖 Generated with Claude Code

QuicksilverSlick and others added 2 commits September 11, 2026 12:27
Slice A of the approval loop. A guest turn can now park a
session/request_permission instead of refusing it: nothing is written to
the agent until a verdict arrives on that request's own oneshot, or the
wait expires to reject_once. Nothing routes requests to the owner yet
(slice B), and shared agents still run in bypassPermissions (slice C), so
behaviour in production is unchanged.

- approval_gate.rs (new, pure): the parking rules and limits (600 s wait,
  4 parked per connection, 6 asks per turn, a 30 s minimum window, a 10 s
  margin inside the hard deadline); RequestDigest, which binds each
  verdict to its request; DenyReason; AllowVia, with standing grants
  refunded against the per-turn cap; and TurnClock. While a request
  waits, the idle deadline is suspended and the hard deadline is credited
  for the wait, up to 3 timeouts.
- pending_permissions.rs: parked asks, retirement, and a lazy verdict poll.
- acp.rs: the read loop polls verdicts first under `biased`, expires
  overdue asks (closing the channel before writing the refusal), and
  answers every held or owed request on exit and on cancel.
- Review fixes, each with a test that fails without it:
  - the prompt stops counting as in flight once its result or error is
    read, before the exit drain;
  - a new prompt first answers ids an earlier turn left owed;
  - the dropped-future cancel path resets authority to Guest;
  - an expired ask is closed before its refusal is written.

Windows: 941 pass. The 32 failures are the 29 known timing failures plus
the 3 wire tests that need Unix bash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With the slice A review fix, the prompt stops counting as in flight as
soon as its response is read, and that includes a JSON-RPC error. If a
steer, interrupt or stop then drops the prompt future during its exit
drain, the pool finds no prompt in flight and takes its "completed
before the control signal" branch. That branch reported every such turn
as a success: the batch was recorded as delivered and never retried.

The client now keeps the error it read until the prompt call returns,
and that branch takes it. A failed turn is reported as the error it
was, with no delivery record and an Error metric. Its batch follows the
signal, as a cancel's would: a steer or interrupt re-prompts it, and a
stop or rotate drops it. The copy is cleared when the prompt call
returns and again when the next prompt starts, so it can never be
reported against a later turn.

Each test fails with its part of the fix undone:
- a_prompt_error_read_before_a_drop_is_kept_for_the_pool
- an_error_the_prompt_returned_is_not_kept_for_the_pool
- test_classify_completed_before_control_signal_reports_a_read_error

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant