Skip to content

feat(audit): seal which AGENTS.md steered a turn into the chain - #51

Merged
rrrodzilla merged 1 commit into
mainfrom
worktree-context-sources
Aug 30, 2026
Merged

feat(audit): seal which AGENTS.md steered a turn into the chain#51
rrrodzilla merged 1 commit into
mainfrom
worktree-context-sources

Conversation

@rrrodzilla

Copy link
Copy Markdown
Contributor

Closes the gap deliberately left open by #29, now that Govcraft/acton-ai#18 has shipped in acton-ai 0.37.0.

What this does

#29 landed AGENTS.md discovery gated and confined, but its provenance was only logged, not sealed — acton-ai 0.36.0's TurnRecord had nowhere to put it. 0.37.0 adds ContextSource and PromptBuilder::context_sources(), so the record now lives in the same tamper-evident chain that already answers "what did this turn do".

  • Discovered::layers is now Vec<ContextSource> (acton-ai's own metadata-only fingerprint: scope, path, BLAKE3 of the content — never the content). Garrison's interim LoadedLayer and its hand-rolled hashing are gone.
  • drive_turn passes those to PromptBuilder::context_sources(), unconditionally. acton-ai skips the field when empty, so a turn no AGENTS.md reached hashes over exactly the bytes it did before.
  • The tracing call stays but is reworded as operational visibility, not the record.

One subtlety worth the review: the fingerprints are built from the layers that survived filter_layers, never from AgentInstructions::context_sources(). That convenience fingerprints every layer discovery found, including ones a restricted bundle refused to load — an entry claiming a turn was steered by a withheld file would be worse than none. There's a test pinning that.

blake3 drops from dependency to dev-dependency: its one remaining use is a test that independently recomputes the digest and checks it against the one acton-ai sealed, which is worth keeping precisely because it doesn't let the crate under test grade its own homework.

Verification

  • cargo check --workspace --all-targets --locked — clean
  • cargo clippy --locked --workspace --all-targets -- -D warnings — zero warnings
  • cargo test -p garrison-wire --lib — 38/38
  • garrison-agent's tests hit this machine's known AWS-LC-FIPS abort; CI is the real check here, and specifically agent/tests/audit_fixture.rs: the frozen 1.0 trail must still verify under 0.37.0. That test existing is why the exact-pin policy in docs/compatibility.md calls for a reviewed commit, and that row now records the 0.36 → 0.37 rationale.

Noted, not addressed here

Two things surfaced while verifying this; neither is in scope for this PR and I'd rather raise them than fold them in:

  1. acton-ai's chain hash depends on a Cargo feature. The pre-image serializes tool arguments as a serde_json::Value. With serde_json/preserve_order the object's key order round-trips; without it keys are sorted, producing a different hash for identical data. garrison-agent gets preserve_order transitively (via agent-client-protocol-schemaserde_with), so trails verify today — but dropping an unrelated dependency could silently strand every trail on disk. I hit this building a scratch harness in garrison-wire, which lacks the feature and could not verify the frozen fixture. Probably worth an upstream issue.
  2. Garrison's own turn refusals still aren't in the chain. admission::admit refusals return TurnResult::Refused and are only tracing::info!-logged, so a governed install that refuses a turn leaves no tamper-evident trace that it did. acton-ai 0.37.0 also shipped ActonAI::record_refused_turn (No public path to seal a turn the host refused before the runtime saw it acton-ai#17), which is exactly the writer for this.

@rrrodzilla
rrrodzilla force-pushed the worktree-context-sources branch from 091ca27 to 95f25fe Compare August 30, 2026 23:28
@rrrodzilla

Copy link
Copy Markdown
Contributor Author

Rebased onto green main (#53 fixed two pre-existing TUI failures that were red before this branch existed).

The back-compat question is now answered, and locally rather than by inference. Temporarily swapping acton-ai's fips feature for tls-ring sidesteps this machine's AWS-LC-FIPS self-test abort, so the suite actually runs here:

  • audit_fixture::the_frozen_trail_still_parses_and_verifiesPASS under 0.37.0. The trail a daemon wrote before this change still verifies, so context_sources genuinely serializes away on entries that have none and no existing byte moved.
  • an_edited_argument_breaks_the_chain_where_it_was_edited and the rest of the fixture suite — PASS.
  • Whole workspace: 1295 passed, 0 failed.

I confirmed serde_json/preserve_order is still enabled under that swap, so the fixture test remained a valid check rather than passing for the wrong reason. The swap is a local diagnostic only; agent/Cargo.toml and Cargo.lock are restored and the pushed tree has fips intact.

Bumps acton-ai to 0.37.0 and replaces the interim tracing-only record of
project instructions with the real thing: every layer that survives the
policy gate is now named in the turn's own audit entry, hash-chained
alongside what the turn did.

This closes the gap opened deliberately in #29. That work shipped with
discovery gated and confined but its provenance only logged, because
acton-ai 0.36.0's TurnRecord had nowhere to put it; Govcraft/acton-ai#18
was filed to give it one, and 0.37.0 delivers it.

What changed:

- agent/src/instructions.rs no longer carries its own LoadedLayer type or
  hashes content itself. Discovered::layers is now Vec<ContextSource>,
  acton-ai's own metadata-only fingerprint (scope, path, BLAKE3 of the
  content, never the content), built with
  ContextSource::from_instruction_layer and handed straight to the
  builder.
- agent/src/thread.rs calls PromptBuilder::context_sources() with those
  fingerprints. Set unconditionally, including the empty case: acton-ai
  skips the field entirely when empty, so a turn no AGENTS.md reached
  hashes over exactly the bytes it did before this existed.

The fingerprints are deliberately built from the layers that survived
filter_layers, never from AgentInstructions::context_sources(). That
convenience fingerprints every layer discovery *found*, including the
ones a `restricted` bundle just refused to load — an entry claiming a
turn was steered by a file the gate withheld would be worse than no entry
at all. A test pins that distinction rather than leaving it to a comment.

The tracing call stays, reworded: it is operational visibility for
somebody tailing logs, and no longer pretends to be the record. The
sealed entry is what an auditor reads.

blake3 moves from a dependency to a dev-dependency. The only remaining
use is a test that recomputes a digest independently and checks it
against the one acton-ai sealed, which is worth keeping precisely because
it does not trust the crate under test to grade its own homework.

wire/src/audit.rs's fixture skeleton gains context_sources: Vec::new(),
matching the shape acton-ai writes for an entry no instructions steered.

docs/compatibility.md records the 0.36 -> 0.37 rationale in the row that
requires an exact pin to be a reviewed commit, and the "logged, not
sealed" limitation is struck from docs/control-plane.md and
docs/garrison-agent-design.md because it is no longer true.
@rrrodzilla
rrrodzilla force-pushed the worktree-context-sources branch from 95f25fe to c45f61d Compare August 30, 2026 23:43
@rrrodzilla
rrrodzilla merged commit 85ecd08 into main Aug 30, 2026
3 checks passed
@rrrodzilla
rrrodzilla deleted the worktree-context-sources branch August 30, 2026 23:52
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