Skip to content

feat(audit): seal an entry for every attempted turn, not only tool calls - #49

Merged
rrrodzilla merged 1 commit into
mainfrom
worktree-fix+audit-turn-entries
Aug 30, 2026
Merged

rrrodzilla merged 1 commit into
mainfrom
worktree-fix+audit-turn-entries

Conversation

@rrrodzilla

Copy link
Copy Markdown
Contributor

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 attempted
turn. Garrison projects it into the same AuditEvent row 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

kind gains "turn". Admission is the gate that fills the columns the schema
requires: an admitted turn is auto_approved by default, a refused one is
forbidden by policy. Both are true statements about who decided, so the
deployed schema did not have to be relaxed.

Turn outcomes map Completed to success, Failed to error, Interrupted to
aborted, Refused to none.

Nothing 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".
  • New plane columns append after detail so the generated proto keeps its
    field numbers. Inserting mid-schema would have renumbered every later tag,
    which is a wire break.
  • agent/tests/audit_fixture.rs holds the claim honest: a new test asserts the
    frozen 1.0 trail carries no entry_kind anywhere in its bytes and that every
    one of its entries still reads as an invocation.

The hook still re-derives

hooks-service/src/hooks/audit_event.rs recomputes the turn columns from the
sealed 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 --locked1249 passed, 1 skipped.
clippy -D warnings clean, cargo fmt --check clean.

Two existing tests needed real updates and are stronger for it:

  • agent/tests/session_persistence.rs counted trail lines; it now asserts the
    exact sequence [invocation, turn, invocation, turn] across both daemons.
  • hooks-service/tests/audit_shipping.rs shipped an all-invocation chain into a
    real 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 run in debug aborts before listing tests with an AWS-LC
FIPS 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() is
pub(crate) in 0.36.0, the RecordTurn message types are public but the
handle to send them to is not reachable, and AuditLog uses direct-message
handlers rather than broker subscriptions. Closing it needs a public append
path upstream.

Closes #28

https://claude.ai/code/session_019QLkGsybQkgMocxu8eMsez

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
@rrrodzilla
rrrodzilla merged commit 30601ac into main Aug 30, 2026
3 checks passed
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.

The audit trail records tool invocations only: a prompt-only turn leaves no entry

1 participant