> ## Documentation Index
> Fetch the complete documentation index at: https://schemabrain.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Audit Viewer

> A visual walk through the hash-chained audit log. Every tool call lands as one row; the page recomputes each row's Merkle inclusion proof in your browser and reconciles it to one root.

# Audit Viewer

<Note>
  **Route:** `/audit` on the dashboard. **Backed by:** `GET /api/audit/rows`, `GET /api/audit/rows/{id}`, `GET /api/audit/verify`, `GET /api/audit/merkle/root`, `GET /api/audit/rows/{id}/proof`.
</Note>

<Frame caption="Audit Viewer — the hash-chained log spined by the derived Merkle root, with a per-row 'Verify against root' inclusion-proof check.">
  <img src="https://mintcdn.com/schemabrain/vxsNBZD3F847_0vI/assets/dashboard-audit-viewer.png?fit=max&auto=format&n=vxsNBZD3F847_0vI&q=85&s=e41dea9e60514300a652b065667300d5" alt="Audit Viewer dashboard surface — append-only audit log with the derived Merkle root and per-row inclusion proofs" width="1920" height="1333" data-path="assets/dashboard-audit-viewer.png" />
</Frame>

The Audit Viewer is the visual face of the [tamper-evident audit chain](/mechanism/audit-chain). Every MCP tool call writes exactly one row, regardless of outcome — success, empty, partial, degraded, error, or refused. Each row carries a 32-byte `chain_hash` computed as `sha256(prev_chain_hash || canonical(row))`. On top of that chain the page derives an [RFC-6962](https://www.rfc-editor.org/rfc/rfc6962) Merkle root over every row and, for any row, an O(log n) **inclusion proof** — a short list of sibling hashes that the browser folds back up to that one root. Clicking **Verify against root** re-walks the hash chain across the whole log (the tamper tripwire) and recomputes each visible row's inclusion proof locally.

***

## What the surface shows

The top of the page carries the integrity summary — the spine of the surface:

* **rows in tree** — how many audit rows the Merkle tree commits to.
* **merkle root** — the 64-hex root over every row, computed this request. Click it to copy; archive it off-host so a later change is caught (see below).
* **integrity** — `not verified this session` until you run a verify pass, then `proving · n/N`, `verified · n/N intact`, or `N rows edited after write` (a `failed proof` / `partial` variant covers the rarer internal-inconsistency and unverifiable cases).
* **in view** — how many rows are loaded (the most recent page); when the log is larger than the page, the surface discloses that verify covers the rows in view while the root commits to all of them.

Below it, the formula eyebrow `H(rowₙ) = SHA256( dataₙ ‖ H(rowₙ₋₁) )` and the **Verify against root** action, then the row grid. Each row shows:

* `time` — relative age plus an HH:MM clock.
* `row` — the monotonic rowid (`#N`).
* `tool · caller` — which MCP tool was called and the caller id.
* `cost` — `small`, `medium`, `large`, or `refused`.
* `chain hash` (truncated, cyan) — the row's anchor in the chain. Refused rows are calm (a `ban` marker), never alarm-colored.
* `idx` — the row's 0-based **leaf index** in the tree (oldest = 0). Audit rows render newest-first but are indexed oldest-first; this column makes that mapping explicit.
* `proof` — the row's state in the current verify pass (`proving`, `proven`, `edited`, `no proof`, `log grew`, `missing`).

Selecting a row expands its detail in place (an accordion, not a separate drawer):

* The **inclusion proof ladder** — the leaf hash, each sibling hash on its rung with the `0x00`/`0x01` fold notes, terminating in `= root … · matches the root` (or a divergence verdict). This is the actual proof the browser just recomputed.
* The **audit row** chain link — leaf index, source, `fingerprint`, and `chain_hash`, all copyable.
* The **row payload** — the structured columns hashed into the row. The original request text is never stored.

This is the same information `schemabrain audit list --json` exposes from the CLI — the dashboard adds the visual proof ladder and the prev-hash → row → root linkage.

***

## Verify against root

The **Verify against root** button does a *real* check, not a cosmetic animation. It runs two complementary passes:

1. Fetches the **whole-log chain walk** (`GET /api/audit/verify?full=true`) and the global root (`GET /api/audit/merkle/root`) once. The chain walk is the tamper tripwire — it covers every row, not just the visible ones, so its result drives the integrity verdict.
2. For each visible row (oldest-first, the leaf order), if the chain walk flagged it the row reads `edited`; otherwise the browser fetches `GET /api/audit/rows/{id}/proof` and recomputes the root from the leaf hash and the audit path using the Web Crypto API (`crypto.subtle`), folding with the same RFC-6962 `0x00` leaf / `0x01` node prefixes the server uses, and requires the result to equal the global root.

A row that folds to the root reads `proven`; every row the chain walk flagged is shown `edited` (the pass does not stop at the first), and the integrity stat counts them all — including edited rows outside the visible window. Because the browser recomputes the hashes itself, "verified" means the cryptography held — not that a timer finished.

<Note>
  **What this proves, precisely.** Every visible row reconciles to one self-consistent root that the sidecar computed now. It does **not** prove the underlying data is authentic — the browser is given each row's leaf hash by the same sidecar that computed the root. The honest, external defence is archiving the root (next section).
</Note>

The older server-side chain walk is still available: `GET /api/audit/verify` (and `schemabrain audit verify --full` from the CLI) re-walks the stored-vs-recomputed `chain_hash` for every row. It answers a slightly different question — "does each stored chain hash still match its recomputed value?" — and supports `--since <hash-prefix>` cursors for scoped forensic walks. See [audit verify](/reference/cli/audit).

<Warning>
  The root and the proofs are recomputed per request from the rows on disk. On stores with hundreds of thousands of rows this can take noticeable time. The chain walk covers the whole log (so an edited row outside the visible window still trips the verdict), while the per-row inclusion proofs are recomputed only for the rows in view — the surface discloses that when the log is larger than the page.
</Warning>

***

## Tamper-evident, not tamper-proof

The chain and the derived root are **detection, not prevention**. Anyone who can write the SQLite file could rewrite the rows and recompute a coherent new `chain_hash` and Merkle root — so the root the page shows is never, on its own, proof the data is untouched.

The real defence is cheap: **copy the root and store it somewhere the audit file's writer cannot reach** (a different host, an object store, a ticket). A change to any earlier row produces a different root, so a later re-check against your archived value catches it. The page never claims more than this — it surfaces the root, the proofs, and the honest framing, and leaves the anchoring to you. See the [audit chain mechanism](/mechanism/audit-chain) for the cron-driven anchoring pattern.

***

## Freshness

The list reflects the audit log as of page load (and as of the last **Verify against root** pass). It does not auto-push new rows — reload to pick up tool calls written since you opened the page. If the log happens to grow mid-verify, the surface marks the affected rows `log advanced` and invites a re-verify rather than reporting a false break. (The sidecar exposes a `GET /api/audit/stream` SSE endpoint for live row push; this surface does not subscribe to it yet.)

This is an operator's after-the-fact view, **not a security-alerting channel.** For sub-second tamper detection, drive it from a cron that compares an archived root against a fresh `GET /api/audit/merkle/root`.

***

## What you cannot do here

The viewer is read-only and intentionally minimal:

* **No filtering by tool or status in the UI yet.** Use `schemabrain audit list --tool ... --status ...` from the CLI.
* **No export.** Use `schemabrain audit list --json` and pipe through `jq`.
* **No tampering reproduction.** The "tamper, then re-verify" walkthrough lives in the [audit chain](/mechanism/audit-chain) doc.

These are deliberate. The dashboard helps an operator answer "does every row reconcile to one root, and what landed since I last looked?" — not run a forensic toolkit. The CLI remains the source of truth for deep investigation.

***

## When a row is flagged

If a verify pass flags a row **edited** (the common case — the chain walk found its stored hash no longer matches its contents) or **failed proof** (the rarer case — the browser folded its leaf up the audit path and the result did not equal the root the sidecar reported), the claim is precise. Possible causes, in order of likelihood:

1. **A row was rewritten.** The append-only triggers on `mcp_audit` reject normal `UPDATE` / `DELETE`, so the rewrite required dropping the triggers first or writing to the file outside SQLite. Either is a forensic signal worth investigating — and if you archived an earlier root, comparing it locates the change.
2. **A schema migration changed `canonical(row)` semantics without bumping `fingerprint_version`.** This is a SchemaBrain bug — please file an issue with the row's `id`, `fingerprint_version`, and the output of `schemabrain audit verify --full`.
3. **The file was restored from an inconsistent backup.** A partial restore that mixed rows from two timelines will fail on every divergent row.

`schemabrain audit verify --full` enumerates every divergent row from the chain side; that is the right next step.

***

## Related

<CardGroup cols={2}>
  <Card title="Tamper-evident audit chain" icon="signature" href="/mechanism/audit-chain">
    The mechanism the viewer reflects — append-only triggers, chain hash, the derived Merkle root, recovery semantics.
  </Card>

  <Card title="schemabrain audit verify" icon="terminal" href="/reference/cli/audit">
    The CLI surface for the server-side chain re-walk, with `--since` cursor support.
  </Card>

  <Card title="Refusals" icon="ban" href="/dashboard/refusals">
    Refused rows live in the chain too; this is the envelope-facing view of the same data.
  </Card>

  <Card title="Observability" icon="chart-line" href="/observability">
    Where the audit row sits in the event-bus substrate.
  </Card>
</CardGroup>
