Skip to content

feat(#2886): ToolAuditLedger — tamper-evident, Merkle-chained tool-action log#120

Merged
Skobeltsyn merged 1 commit into
mainfrom
feat/2886-tool-audit-ledger
Jun 1, 2026
Merged

feat(#2886): ToolAuditLedger — tamper-evident, Merkle-chained tool-action log#120
Skobeltsyn merged 1 commit into
mainfrom
feat/2886-tool-audit-ledger

Conversation

@Skobeltsyn
Copy link
Copy Markdown
Contributor

Slice 3 of epic #2882 — Pillar 2 (tamper-evident audit). New ToolAuditLedger in agents-kt-observability, sibling to JsonlAuditExporter. TDD.

What

An append-only, Merkle-chained, PII-safe record of every tool action. Each row's entryHash = SHA-256(prevHash ‖ sequence ‖ callId ‖ toolName ‖ decision ‖ denialReason ‖ resultHash ‖ timestamp) chains to the previous (genesis = 64 zeros), so ToolAuditLedger.verify(path) recomputes the chain and pinpoints the first edited / inserted / deleted / reordered row. The tool result is stored only as a hash — never raw.

  • agent.events.ledger(file) auto-wires it: ToolCalledAPPROVED, ToolDeniedDENIED (with reason), ToolHallucinatedHALLUCINATED; returns the ledger for later verify(...).
  • read(path) parses rows (flat-JSON, zero-dep).

Tests (9, TDD RED→GREEN)

chain links to genesis + prev · verify ok (untampered + empty) · tamper detection (edit-at-sequence / delete / reorder) · PII-safety (raw result absent, hash present) · callId+decision recorded + read-back · end-to-end auto-record via the observe hook.

Remaining for #2886 (kept open)

callId-keying of the denied/hallucinated rows needs PipelineEvent to carry the callId (the ledger core already accepts callId) — a scoped follow-up. Everything else of the acceptance is in: append-only, Merkle-chained, tamper-detecting, child-unreachable writer, PII-safe, covers approved/denied/hallucinated.

Verified: full ./gradlew build green (suite + detekt).

🤖 Generated with Claude Code

…tion log

Epic #2882 (Pillar 2), TDD. New ToolAuditLedger in agents-kt-observability
(sibling to JsonlAuditExporter).

- Append-only, Merkle-chained, PII-safe: each row's
  entryHash = SHA-256(prevHash | sequence | callId | toolName | decision |
  denialReason | resultHash | timestamp), chained to the previous (genesis =
  64 zeros). verify(path) recomputes the chain and pinpoints the first
  edited/inserted/deleted/reordered row. The tool result is stored only as a
  hash, never raw. read(path) parses rows (flat-JSON, zero-dep).
- Auto-wire: agent.events.ledger(file) records PipelineEvent.ToolCalled ->
  APPROVED, ToolDenied -> DENIED (reason), ToolHallucinated -> HALLUCINATED;
  returns the ledger for later verify().

Tests (9, TDD RED->GREEN): chain links to genesis + prev; verify ok on
untampered + empty; tamper detection (edit at sequence / delete / reorder);
PII-safety (raw result absent, hash present); callId + decision recorded +
read-back; end-to-end auto-record via the observe hook.

Remaining for #2886: callId-keying of denied/hallucinated rows needs
PipelineEvent to carry the callId (the ledger core already accepts it) — a
scoped follow-up. CHANGELOG. Full ./gradlew build green.
@Skobeltsyn Skobeltsyn merged commit d3d9f82 into main Jun 1, 2026
4 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.

1 participant