feat(audit): seal an entry for every attempted turn, not only tool calls - #49
Merged
Merged
Conversation
A trail that recorded tool invocations alone could not answer the question an auditor actually asks. A session where the model read, reasoned, and answered in text left no line at all, so "this install did nothing that day" and "this install was busy and touched no tool" were the same silence. acton-ai 0.36.0 seals a turn entry per attempted turn. Garrison projects it into the same AuditEvent row the tool call uses: a `kind` of "turn", the turn outcome, and metadata only. Prompt and response byte counts, provider, model, and token counts answer the activity question without copying what a developer typed into a record that leaves the workstation. Admission is the gate that fills the columns the schema requires. A turn the gates admitted is `auto_approved` by `default`; a turn the model itself refused is `forbidden` by `policy`. Both are true statements about who decided, so the deployed schema did not have to be relaxed to accept the new kind. Nothing that was already written moved. The discriminator is absent on an invocation entry rather than set to a default, so entries a 1.0 daemon wrote hash to exactly what they hashed to; `kind()` reads that absence as "tool call". The new plane columns append after `detail` so the generated proto keeps its field numbers, which inserting mid-schema would have renumbered. The hook re-derives the turn columns from the sealed entry, as it already did for invocations, so an install cannot ship truthful evidence beside flattering metadata. One case is still not sealed: a turn Garrison's own admission gates refuse never reaches the model loop, and the audit writer's handle is not public, so there is nowhere to append it from. README says so plainly. Closes #28 Claude-Session: https://claude.ai/code/session_019QLkGsybQkgMocxu8eMsez
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.
What this fixes
The audit trail recorded tool invocations only. A turn where the model read,
reasoned, and answered in text left no line, so "this install did nothing" and
"this install was busy and touched no tool" were the same silence.
acton-ai 0.36.0 (pin bumped to
=0.36.0) seals a turn entry per attemptedturn. Garrison projects it into the same
AuditEventrow a tool call uses.Entry content
Metadata only: turn outcome, prompt and response byte counts, provider, model,
token counts. No prompt text and no response text, because acton-ai never seals
it. That answers the activity-and-length question a compliance regime asks
without copying what a developer typed into a record that leaves the
workstation and lands in a SIEM.
Plane mapping
kindgains"turn". Admission is the gate that fills the columns the schemarequires: an admitted turn is
auto_approvedbydefault, a refused one isforbiddenbypolicy. Both are true statements about who decided, so thedeployed schema did not have to be relaxed.
Turn outcomes map Completed to
success, Failed toerror, Interrupted toaborted, Refused to none.Nothing already written moved
default, so entries a 1.0 daemon wrote hash to exactly what they hashed to.
kind()reads that absence as "tool call".detailso the generated proto keeps itsfield numbers. Inserting mid-schema would have renumbered every later tag,
which is a wire break.
agent/tests/audit_fixture.rsholds the claim honest: a new test asserts thefrozen 1.0 trail carries no
entry_kindanywhere in its bytes and that everyone of its entries still reads as an invocation.
The hook still re-derives
hooks-service/src/hooks/audit_event.rsrecomputes the turn columns from thesealed entry, as it already did for invocations, so an install cannot ship
truthful evidence beside flattering metadata. Absent token counts land as 0,
which erases invented ones.
Tests
cargo nextest run --workspace --release --locked— 1249 passed, 1 skipped.clippy
-D warningsclean,cargo fmt --checkclean.Two existing tests needed real updates and are stronger for it:
agent/tests/session_persistence.rscounted trail lines; it now asserts theexact sequence
[invocation, turn, invocation, turn]across both daemons.hooks-service/tests/audit_shipping.rsshipped an all-invocation chain into areal Postgres-backed plane; it now ships a mixed chain and asserts turn rows
land with their metadata and none of the tool-call columns. That is the
end-to-end proof, including that the hook's re-derivation agrees.
Note:
cargo nextest runin debug aborts before listing tests with an AWS-LCFIPS integrity failure. That is pre-existing and environmental, unrelated to
this diff; everything above was run
--release.Known gap, stated in the README
A turn that Garrison's own admission gates refuse (lapsed seat, unreachable
plane, full shipping backlog) is turned away before the model loop is entered,
and nothing appends it. Garrison cannot seal it today:
ActonAI::audit()ispub(crate)in 0.36.0, theRecordTurnmessage types are public but thehandle to send them to is not reachable, and
AuditLoguses direct-messagehandlers rather than broker subscriptions. Closing it needs a public append
path upstream.
Closes #28
https://claude.ai/code/session_019QLkGsybQkgMocxu8eMsez