feat(audit): seal the turns Garrison's own gates refuse - #52
Merged
Merged
Conversation
Turn entries closed most of the gap, but not the case the issue opened with. The admission gates are asked before the prompt loop, so a turn refused for a lapsed seat, an unreachable plane, a full shipping backlog, or an interrupted predecessor never reached the runtime, and nothing wrote it down. An install refused fifty times in an afternoon still left a trail indistinguishable from an install nobody touched, which is the one thing the trail exists to tell apart. acton-ai 0.37.0 adds the public single-writer path this needed, so the gate now seals before it returns. The record is the same metadata a turn entry already carries: the stable reason, the rendered refusal, and the prompt's byte count. The prompt itself is counted, never copied. `TurnRefusal::decision` is the word the trail records. Display renders a refusal for a human reading an error and may be reworded whenever it reads badly; an auditor's saved query must not break when it is. Every arm answers a fixed lowercase word, and an arm added later adds a word rather than changing one. Sealing does not change the verdict. A refusal that cannot be recorded is still a refusal: the turn was already being turned away, and failing louder would not admit it. The failure goes to the log, and the audit health status already reports is what an operator acts on. An install with no trail configured is skipped rather than logged at, since it has nothing to seal into. Proved end to end against a real daemon: the interrupted-turn test now reads the trail it left, and finds the refusal sealed with its decision, its reason, and a prompt byte count whose text appears nowhere in the file. Claude-Session: https://claude.ai/code/session_019QLkGsybQkgMocxu8eMsez
rrrodzilla
force-pushed
the
worktree-seal-refused-turns
branch
from
August 30, 2026 23:25
407ae5f to
9962ac2
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.
The case #28 opened with
Turn entries closed most of that issue. They did not close the case it opened
with. Garrison's admission gates are asked before the prompt loop, so a turn
refused for a lapsed seat, an unreachable plane, a full shipping backlog, or an
interrupted predecessor never reached the runtime, and nothing wrote it down.
An install refused fifty times in an afternoon still left a trail
indistinguishable from an install nobody touched. Telling those two apart is
most of what the trail is for.
acton-ai 0.37.0 (Govcraft/acton-ai#17) added the public single-writer path this
needed, so the gate now seals before it returns.
What changed
agent/src/thread.rs:958:seal_refusalcounts the prompt the way the admitted path does, from the lastuser message, and passes the same conversation
checkpointedwould have used.The prompt is counted, never copied. An install with no trail configured is
skipped rather than logged at, because it has nothing to seal into.
TurnRefusal::decision()is new: the stable lowercase word the trail records(
seat,plane_unavailable,policy,audit_shipping,audit_degraded,store_unavailable,turn_interrupted,gate_unreachable).Displaystaysthe prose a human reads in an error and can be reworded whenever it reads
badly. An auditor's saved query filters on the slug, so rewording the prose
never breaks it.
Sealing does not change the verdict
A refusal that cannot be recorded is still a refusal. The turn was already
being turned away, and failing louder would not admit it. The failure goes to
the log, and the audit health
_garrison/statusalready reports is what anoperator acts on.
Proof
a_turn_the_daemon_died_in_blocks_the_session_until_it_is_settledalreadyproduced a genuine gate refusal through a real daemon. It now also reads the
trail that daemon left and asserts the refusal was sealed:
entry_kindofturn, a decision ofturn_interrupted, its reason, aprompt_size_bytesof27, and that the prompt text itself appears nowhere in the file. The append is
durable and awaited before the refusal returns, so it is on disk by the time
the client sees the error code.
Pins
Both crates moved to
=0.37.0viacargo add.AuditEntrygrew acontext_sourcesfield, skipped when empty on both the entry and the hashpre-image, so the fixture skeleton initializes it and no existing byte moved.
Projecting that field into the plane is deliberately not in this PR.
Test status
cargo nextest run --workspace --release --locked --no-fail-fast:1290 passed, 2 failed, 1 skipped.
Both failures are on
mainalready and are untouched by this branch:tui::composer::tests::an_empty_composer_shows_a_hint_with_the_caret_after_the_prompttui::compositor::tests::disabling_color_preserves_text_and_non_color_attributesBoth assert on colors that
499fe55 Improve terminal text contrastchangedwithout updating the assertions. This branch touches no TUI code.
https://claude.ai/code/session_019QLkGsybQkgMocxu8eMsez