Repository navigation
Stop steered guest messages and session setup inheriting the owner's trust - #19
Merged
Merged
Conversation
…wner trust Two holes in how a turn's authority is decided. Both are latent today: the shipped permission mode is bypassPermissions, so every request is auto-approved anyway. Both would defeat the guest gate the moment it turns on. The approval-loop plan's critics found them independently. 1. STEERED GUEST TEXT RAN WITH THE OWNER'S AUTHORITY. Authority was decided once, at dispatch, from the batch that started the turn. MultipleEventHandling defaults to Steer, so when a guest posted while the owner's turn was running, steer_or_interrupt wrote the guest's words straight into that turn, which was still marked Owner. The agent's next tool request was then approved as though the owner had asked. A guest only had to wait until the owner was mid-turn. A steer now carries its author's authority. SteerRequest has a required `authority` field, so every construction site must state it. The read loop lowers the turn's authority to that author's BEFORE the steer is written. The loop is single-threaded, so no permission request can be decided in between. Within a turn, trust can only go down, never up (TurnAuthority::weaker). The steer's authority comes from the event's signing key, not the derived "effective" author. That is the exact input dispatch uses, so an event can never be trusted more as a steer than it would be if it had started the turn. Dispatch and steering now share one comparison, authority_for_author, so the two cannot drift apart. Lowering was chosen over interrupting. The steer stays instant and the turn keeps running. The cost is that the owner's own later tool calls in that turn are also decided as a guest. That is correct: the agent cannot tell which parts of its plan came from whom. 2. SESSION SETUP REUSED THE PREVIOUS TURN'S AUTHORITY. current_turn_authority was written only at startup and at prompt dispatch, and never reset. read_until_response, the loop that runs session/new and set_config_option, decided any permission request with whatever the last turn had left behind. After an owner turn, a guest batch that opened a new session ran its setup as Owner. handle_permission_request now takes authority as a required parameter instead of reading the field. The setup loop passes Guest explicitly, because no turn is running there. The prompt loop passes the turn's current authority. Also corrects the TurnAuthority doc. It said Owner covered "the owner, or one of the owner's own sibling agents". Dispatch has only ever compared against the owner's key, so sibling agents have always classified as Guest. Tests, 5 new: - a_turn_is_only_as_trusted_as_its_least_trusted_author: pure, runs on every platform. - only_the_owners_own_key_counts_as_the_owner: pure. Case-insensitive, and an unknown owner verifies nobody. - a_guest_steer_lowers_the_turn_before_the_agent_can_ask: over the wire. A fake agent blocks until the steer arrives, then asks; the harness must answer reject_once. - an_owner_steer_leaves_an_owner_turn_trusted: the control. Steering itself does not lower trust; the owner's own steer is still approved. - setup_decides_permissions_as_a_guest_even_after_an_owner_turn: over the wire. After an owner turn, setup must still answer reject_once. The three wire tests are for Linux CI. On Windows, Rust resolves `bash` to WSL's launcher before PATH, and WSL cannot see a C:/ path. That is the same reason the crate's other bash-script tests fail there. Verified on Windows: buzz-acp 903 passed, 32 failed. The 32 are the 3 new wire tests plus 29 pre-existing failures. Run against unchanged main, those exact 29 give 0 passed, 29 failed. The failing set is identical, so nothing regressed. cargo fmt is clean; clippy -D warnings reports 0 findings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
QuicksilverSlick
force-pushed
the
fix/turn-authority-holes
branch
from
September 11, 2026 14:57
4889cd5 to
d3bdb40
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two holes in how a turn's trust is decided
Both are latent today, because the shipped permission mode is
bypassPermissionsand every request is auto-approved anyway. Both would defeat the guest gate the moment it turns on. The two critics in the approval-loop plan found them independently, and both said to close them first, as their own small PR, before anything turns the gate on.1. A guest's steered message ran with the owner's authority
Authority was decided once, at dispatch, from the batch that started the turn.
MultipleEventHandlingdefaults to Steer, so when a guest posted while the owner's turn was running,steer_or_interruptwrote the guest's words straight into that turn — which was still markedOwner. The agent's next tool request was then approved as though the owner had asked.A guest only had to wait until the owner was mid-turn.
Fix. A steer now carries its author's authority.
SteerRequest.authorityis a required field, so every construction site must state it. The read loop lowers the turn's authority to that author's before the steer is written. The loop is single-threaded, so no permission request can be decided in between.Within a turn, trust only ever goes down (
TurnAuthority::weaker).The steer's authority is taken from the event's signing key, not the derived "effective" author. That is the exact input dispatch uses, so an event can never be trusted more as a steer than it would be if it had started the turn. Dispatch and steering now share one comparison,
authority_for_author, so they cannot drift apart.Lower, not interrupt. The steer stays instant and the turn keeps running. The cost: the owner's own later tool calls in that turn are also decided as a guest. That's correct — the agent can't tell which parts of its plan came from whom. Interrupting and restarting would be equally safe, just more disruptive.
2. Session setup reused the previous turn's authority
current_turn_authoritywas written only at startup and at prompt dispatch, and never reset.read_until_response, the loop that runssession/newandset_config_option, decided any permission request with whatever the last turn left behind. After an owner turn, a guest batch that opened a new session ran its setup asOwner.Fix.
handle_permission_requestnow takes authority as a required parameter instead of reading the field. The setup loop passesGuestexplicitly, because no turn is running there. The prompt loop passes the turn's current authority.Also
The
TurnAuthoritydoc saidOwnercovered "the owner, or one of the owner's own sibling agents." Dispatch has only ever compared against the owner's key, so siblings have always classified asGuest. The doc now says so.Tests
a_turn_is_only_as_trusted_as_its_least_trusted_authoronly_the_owners_own_key_counts_as_the_ownera_guest_steer_lowers_the_turn_before_the_agent_can_askreject_oncean_owner_steer_leaves_an_owner_turn_trustedsetup_decides_permissions_as_a_guest_even_after_an_owner_turnreject_onceThe wire tests' fake agent blocks on
readuntil the steer arrives, so the order is fixed — steer written, then the request, then the answer. There's no timing race.The three wire tests are for Linux CI. On Windows, Rust resolves
bashto WSL's launcher beforePATH, and WSL can't see aC:/…path — the same reason the crate's other bash-script tests fail there.Verification
The failing set was compared, not only the count — equal counts could hide one test flipping each way.
🤖 Generated with Claude Code