Audit chain.

SHA-256 hash chain · GENESIS-rooted · rowid-ordered · tamper-evident by construction.

Every grant check, allow or deny, becomes a row in a cryptographically chained log. Each row's hash depends on the previous row's, so any edit — add, remove, reorder — breaks the chain at exactly the edited position. /usage/verify walks the chain and reports where it broke, if anywhere.

Row shape

{
  row_id:         u64,          // monotonic, insert-ordered
  timestamp:      unix_ms,
  consumer_id:    string,
  usage_type:     "grants.check.allow" | "grants.check.deny" | ...,
  data_type:      "obsidian_vault" | "api_keys" | ...,
  path:           string | null,
  permission:     "read" | "write" | "list" | null,
  grant_id:       uuid | null,  // matched grant (null on deny)
  agent_id:       uuid | null,  // Option A or B
  agent_name:     string | null,
  agent_verified: bool,         // true = Ed25519 signed (Option B)
  prev_hash:      sha256_hex,
  row_hash:       sha256_hex,   // sha256(prev_hash || canonical(row))
}

Chain invariants

Verification

th audit verify (or GET /usage/verify) walks the chain row by row, recomputes each hash, and confirms the chain head.

$ th audit verify
✓ chain valid  rows_verified=90  pre_chain=0  head=fd8754cb3d28…

On tamper, you'd see something like:

$ th audit verify
✗ chain broken at row 57  expected_prev=a3b1…  found=5f8e…
  reason: hash mismatch, likely in-place edit

Agent verification levels

Two shipping levels + one roadmap:

Compliance export

GET /usage/export?format=ndjson emits the full chain plus its verification proof in one bundle. Every row includes its row_hash and prev_hash, so a downstream auditor can re-verify without talking to the wallet.

Consumer view Consumers can pull their slice via /my/usage/* without admin scope — customer sees what vendor's agents did against customer's data. Same chain, filtered by the caller's consumer token.

What verified doesn't prove

It proves: which agent identity requested this, that the request was authentic at submission time, that the row hasn't been tampered with since.

It does not prove: that the agent is the one you think it is at the socket layer (the private key could be exfiltrated — the wallet can't detect that). That's Option C's job. Today, per-device-per-agent keys at rest are the threat model.