Repository navigation
[0.8.0] - 2026-10-13
"Auditoria formal" (formal audit): a SHA-256 hash chain over every stored event (#105) with periodic Ed25519-signed
checkpoints (#387, #106), named ingestion keys with submitter identity (#107) and scoped read-only auditor keys
(#108), chain-compatible retention and redaction that tombstones a row without breaking the chain (#109), and an
offline-verifiable audit bundle with a standalone zero-dependency verifier script (#110). This release also ships
the audit guide, key management guide, threat model, migration notes from the single shared token, and a docs
hygiene check wired into CI (#111, split into sub-issues #408 to #411 per .claude/agent-policy.md's size rule).
The guarantee, in one sentence, from docs/threat-model.md: "a hash chain proves that stored history was not
altered after the fact; it does not prove that what an agent reported is true."
Upgrading
Purely additive: no breaking change to the event contract, the HTTP API or the embeddable library's props. The
hash chain, checkpoints, named/scoped keys and the audit bundle are all SQLite-only and opt-in by use; a
deployment running memory storage or the single shared AGENT_VIEWER_API_TOKEN keeps working exactly as before,
with every audit route reporting enabled: false or 409 audit_requires_chain in memory mode. See
docs/migrating-to-0.8.md for the practical runbook from the single shared token to
named keys, and docs/threat-model.md for what the chain proves and what it does not.
Known gaps
GET /api/v1/audit/verify/agent-viewer verifywere not tombstone-aware until the fix below landed in this
same release; any 0.8.0 deployment running an earlier pre-release build of this milestone should re-verify.- Closing an open SSE connection when its key is revoked or expires,
submittedByon SSE reconnect-replay
frames, andsubmittedByas a rollup/export dimension remain deferred (documented on issue #108). - Pricing-version digests in the audit bundle are not yet chain-anchored (documented in
server/audit/bundle.ts
anddocs/audit-bundle.md, deferred to a follow-up of #83). - A full Spanish translation of
docs/audit.md,docs/key-management.md/docs/api-keys.mdand
docs/threat-model.mdis deferred; the Spanish README mirrors only the changed Security table rows.
Fixed
GET /api/v1/audit/verify/agent-viewer verifyare now tombstone-aware (issue #408, v0.8.0 milestone): a row legitimately tombstoned by issue #109's retention/redaction routes rewrites its ownevent_jsonby design, so it never recomputes to its own storedhashagain; before this fixserver/audit/verify.ts's walk had no tombstone handling at all and reportedhash_mismatchat the first such row, indistinguishable from real tampering, contradicting #109's and the 0.8.0 exit criterion's stated goal ("after a retention purge the chain still verifies through tombstones"). The walk now skips the content-hash recompute for a row withtombstoned_at IS NOT NULL(its link to the previous row is still checked normally, since links are never touched by a tombstone) and instead cross-checks it against the chainedaudit.retention.purged/audit.redaction.appliedevent itstombstone_audit_seqnames: that event must exist, be of the right type, and have committed exactly this row's originalhash/content_hash, or the newtombstone_invalidBreakKindis reported (a forged or missing tombstone is not a silent pass). Mirrors the logicscripts/verify-audit-bundle.mjs'sREDACTIONcheck (#401) already implemented andtests/audit-retention.test.mjs'stombstoneAwareWalkhelper already pinned in tests.docs/audit-chain.mdgains a "Tombstones do not break verification" section;docs/retention.md's "Known limits" entry for this gap is removed (closed, not reworded);docs/threat-model.md's "what the chain does not prove" bullet about this gap moves to "what the chain proves", corrected. New tests intests/audit-verify.test.mjs: a clean tombstoned chain, a tombstone pointing nowhere, one pointing at a real but unrelated row, and one whose committing audit event records the wrong original hash.
Added
-
Migration guide from the single shared token (issue #409, sub-issue of #111, docs only): new
docs/migrating-to-0.8.mdcovers the practical runbook for an operator running today's single shared
AGENT_VIEWER_API_TOKEN: what changes (nothing functional) for the bootstrap token, the CLI's generated
session token, the Dockerapi-tokenfile (AGENT_VIEWER_TOKEN_FILE) andAGENT_VIEWER_WEBHOOK_SECRET;
step by step from backing up the SQLite file through creating one named ingest key per client, an auditor
key, switching clients over and retiring routine use of the bootstrap token; and the exact, code-verified
shape in which rows stored before the hash chain existed are reported byGET /api/v1/audit/verify/
agent-viewer verify(chain.unchainedcount plus alegacy_rowswarning, never a separate "pre-chain"
status, never silently counted as individually verified). Linked fromREADME.md's and
docs/README.es.md's Security tables and fromdocs/api-keys.md's bootstrap-token section. New
tests/audit-migration.test.mjs: builds a SQLite database staged at schema version 16 (the "0.7.0 shape",
the same staging patterntests/audit-verify.test.mjs's legacy-rows case and
tests/usage-rollup-migration.test.mjsalready use), appends pre-chain events, starts the real 0.8.0 server
on that file (running migrations 17-22) and asserts both documented claims over real HTTP. No server, CLI,
SDK or library runtime code changed. -
Docs hygiene check in CI: relative links, README linkage, em dash scan (issue #410, sub-issue of #111): new
scripts/check-docs.mjs,npm run check:docs, wired into.github/workflows/ci.ymlright after the existing token/api_key doc-content check. Three checks: (1) every relative markdown link and image inREADME.md, the top-leveldocs/*.mdfiles (includingdocs/README.es.md) and.github/SECURITY.mdresolves to a real file, and a#anchorresolves to a real heading in the target, using a from-scratch reimplementation of GitHub's own heading-slug algorithm (code spans stripped, non letter/digit/space/hyphen/underscore characters dropped, then each remaining whitespace character replaced with a hyphen individually, deliberately without trimming or collapsing runs, which is what makes emoji-prefixed headings slug the way GitHub actually renders them); (2) every 0.8.0 doc this issue names (docs/audit.md,docs/audit-chain.md,docs/audit-bundle.md,docs/api-keys.md,docs/retention.md,docs/threat-model.md) is linked fromREADME.md; (3) no U+2014 in any file changed since thev0.7.0tag (git diff v0.7.0..HEAD, added/copied/modified/renamed only), chosen over a repo-wide retroactive scan because the latter already failed on untouched pre-0.7.0 prose outside this issue's scope. Running this script against the current repo surfaced and fixed real pre-existing bugs it was built to catch: two em dashes indocs/retention.mdand one each indocs/crew/evidence/{ambience-148,lamps-149}/README.md(all introduced sincev0.7.0), four links to a staledocs/otlp.md#otlp-metrics-post-v1metricsanchor missing the heading's own-issue-73suffix (README.md, docs/README.es.md, docs/integration.md, docs/otlp.md's own table of contents), two links to a staledocs/integration.md#reconnect-replay-and-resyncanchor missing-issue-54(docs/library.md, docs/library.es.md), anddocs/audit.mditself, shipped with issue #388, never having been linked fromREADME.mdat all (added to the existingGET /api/v1/audit/verifyrow). Newtests/check-docs.test.mjs: pure-function unit tests for the slug algorithm (including the two real, previously-fragile emoji/ampersand anchors above as fixtures) and link/em-dash checks against synthetic fixture repos, plus CLI invocations against the real repo. The CI workflow needs no extra checkout change: the script resolvesv0.7.0itself, fetching just that one tag shallowly fromoriginif the default shallow checkout does not already have it. -
Threat model for the 0.8.0 formal-audit work (issue #111, first sub-issue, docs only): new
docs/threat-model.mdstates, in one place, what the hash chain (#105), signed checkpoints (#387), chain verification (#106), named and scoped keys (#107/#108), chain-compatible retention/redaction (#109) and the offline-verifiable audit bundle (#110) actually guarantee: the assets at risk, every actor from a compromised agent to an operator holding both the database and the signing key, explicit "what the chain proves" and "what it does not" sections, memory-mode behavior (every audit route refuses rather than returning a degraded result), bundle confidentiality (an exported bundle is not redacted, unlikeGET /api/v1/usage/export), the invoice-cross-check rules (unknown is never zero, estimated is never mixed with reported), residual risks and recommended mitigations, and explicit non-goals (not tamper-proof against a fully privileged operator, not a compliance certification, not provider billing). Cross-linked fromREADME.md/docs/README.es.md's Security tables,.github/SECURITY.md(which also gets a corrected "Data exfiltration surface" paragraph reflecting shipped scoped keys instead of describing them as a future 0.8.0 item),docs/integration.md's audit-trail section,docs/audit-chain.mdanddocs/api-keys.md. No server, CLI, SDK or library runtime code changed. Also documents, rather than silently assumes, thatGET /api/v1/audit/verify/agent-viewer verifyare not yet tombstone-aware and reporthash_mismatchon a legitimately redacted/retained row even though the standalone bundle verifier (#401) already handles this correctly; filed as issue #408 rather than papered over in the text. Split per.claude/agent-policy.md's size rule from the rest of #111 (migration guide from the single shared token, the docs hygiene check, and the final version bump/release), tracked as issues #409, #410 and #411; also found and filed separately, a staleness bug indocs/README.es.md's Security table describing shipped scoped keys as a not-yet-shipped 0.8.0 feature (issue #412). -
Crew asset recovery audit (#115): reconcile PR #43 and #162-164 revisions, the preserved 53-asset bank and reference, three showroom layout hashes, vector-study classification, independent-runtime evidence and unverified local work. Correct the historical migration matrix without approving prototype art or merging the legacy skin.
-
agent-viewer verifyCLI andGET /api/v1/audit/entries/:seq(issue #388, second sub-issue of #106, v0.8.0 milestone):agent-viewer verify [--url <url>] [--token <token>] [--scope full|recent] [--json] [--public-key <file>] [--checkpoints <file>]verifies a live chain throughGET /api/v1/audit/verify(issue #106), the same walk the server itself runs;--db <file>runs the identical verifier (server/audit/verify.ts) directly against a local SQLite file instead, no server involved (a usage error combined with--url/--token).--public-key <file>pins trust to one PEM public key instead of whatever the chain's ownaudit_keystable reports as active, independently re-checking every checkpoint and settingkeyTrust: "pinned"regardless of the outcome.--checkpoints <file>cross-checks an exported checkpoints file (GET /api/v1/audit/checkpoints?format=jsonl's own shape) against the chain just verified, oneseqat a time through the newGET /api/v1/audit/entries/:seqroute, without the server ever seeing the file; fills the newVerifyReport.exportedfield (server/audit/verify.ts's newparseExportedCheckpointsFile/crossCheckExportedCheckpoints). New route:GET /api/v1/audit/entries/:seqreturns{ seq, eventId, entryHash, prevHash }for one chained row,404 seq_not_foundoutside the chain's range,409 audit_requires_chainin memory mode; needs theauditscope (issue #108). Exit codes:0clean,1integrity failure,2usage error,3could not verify (unreachable,401, memory storage, unreadable file). Newcli/verify.ts,VerifyCommandincli/args.ts.verifySignedCheckpoints(server/audit/verifyCheckpoints.ts) gains an optionaltrustedKeyPemparameter for pinning. Newdocs/audit.md(checkpoint file format, manualopenssl/node:cryptoverification without the CLI, key-handling guidance, honest limits);docs/cli.mdgains a "Verify the audit chain" section;docs/integration.mdgains a full/api/v1/audit/*route reference table. Newtests/audit-entries.test.mjs,tests/cli-verify.test.mjs. Split off per.claude/agent-policy.md's size rule from #106; the shared verifier (#106's first sub-issue) and signing/checkpoints (#387) are separate, already-shipped items this one builds on. -
agent-viewer audit export/verifyCLI, release asset,docs/audit-bundle.md(issue #402, third and final sub-issue of #110, v0.8.0 milestone, closes #110):agent-viewer audit export --out <file> [--from-seq n] [--to-seq n] [--gzip] [--url <url>] [--token <token>]downloadsGET /api/v1/audit/bundle(#400) to a temp file, renamed to--outonly after a clean end, so a failed export leaves no file there.agent-viewer audit verify <file> [--trusted-key <fingerprint|pem-file>] [--expect-checkpoint <seq>:<headHash>] [--require-complete] [--json]runs the standalonescripts/verify-audit-bundle.mjs(#401) as a child process and returns its exit code unchanged, never a second implementation of its checks. Newcli/audit.ts,cli/packageInfo.ts'sverifyScriptPath()(resolvesscripts/verify-audit-bundle.mjsin a checkout,dist-cli/verify-audit-bundle.mjsonce installed).scripts/build-cli.mjsnow copies the verifier intodist-cli/after every build, including--skip-viewer, so the packed tarball always ships a working copy next tocli.js..github/workflows/release.ymluploadsverify-audit-bundle.mjsand its SHA-256 as its own downloadable, installable-nothing release assets, with the checksum quoted in the release notes. Newdocs/audit-bundle.md: the full bundle format (every line kind, field tables, the pricing-digest honest-limit note already shipped with #401's code, the verifier's nine checks and exit-code table, the CLI and key-pinning workflow);docs/cli.mdgains an "Audit bundle" section,README.md/docs/README.es.md's existing/api/v1/audit/bundlerows point at it. Newtests/cli-audit.test.mjs: argument parsing plus a genuine end-to-end export+verify round trip against a locally started SQLite server (cli/start.ts'sstartViewer, in-process, no subprocess). This closes the parent epic issue #110 ("offline-verifiable audit bundle"): #400, #401 and #402 are all now shipped. -
Standalone, zero-dependency audit bundle verifier:
scripts/verify-audit-bundle.mjs(issue #401, second sub-issue of #110, v0.8.0 milestone): verifies a bundle from issue #400 without trusting the server that produced it and without installing this package; onlynode:crypto,node:zlib,node:fsandnode:readline, no import fromsrc//server/. Independently reimplements the canonical hash function (#105), the signed-checkpoint format (#106/#387) and the pricing digest/cost-estimate formula (#83/#84), kept honest against drift by parity tests that run both implementations side by side (including thetests/fixtures/chain-vectors.jsonalready shared with issue #105's own tests). Nine checks (FORMAT,CHAIN,CHECKPOINTS,KEY,LEDGER,PRICING,TOTALS,REDACTION,EXPECT), each reporting pass/fail and the first offendingseq/id; streams the bundle line by line withnode:readline(bounded memory, not loaded into an array).node verify-audit-bundle.mjs <bundle> [--trusted-key <fingerprint|pem-file>] [--expect-checkpoint <seq>:<headHash>] [--require-complete] [--json]; exit codes0pass,1fail,2invalid (unreadable/malformed/missing footer/unsupported format version),3unpinned (clean but no--trusted-key). Gzip auto-detected by magic bytes, not only the.gzextension. New committed golden v1 bundle fixture (tests/fixtures/audit-bundle/golden-v1.avbundle.jsonl) both this script and a future verifier must keep accepting. Also fixes a pre-existing off-by-one in #400's own paged event query (server/audit/bundle.ts): a partial-range export (fromSeqgreater than the chain's first row) always included one extra row from just beforefromSeq, found by this issue's own partial-range tests. Newtests/audit-bundle-verifier.test.mjs. Theagent-viewer audit export/verifyCLI, release-asset upload anddocs/audit-bundle.mdare issue #402, not implemented here. -
Offline-verifiable audit bundle: NDJSON format and
GET /api/v1/audit/bundle(issue #400, first sub-issue of #110, v0.8.0 milestone): streams a single file an auditor can save and later check without trusting the server again: aheaderline (format/range metadata), every signingkey(#387), thepricingversions referenced by any exported ledger row, thecheckpoints (#387) covering the range, anytombstone(#109) in range, then every chainedeventimmediately followed by itsledgerrow (#65) when one exists, and afooterwithnull-safe reconciliation totals and acontentSha256digest.fromSeq/toSeqorfrom/to(mutually exclusive),gzip=1. Requires theauditscope and SQLite with the hash chain;409 audit_requires_chainotherwise (400 invalid_rangefor a malformed range,503 audit_export_busywithRetry-AfteraboveAGENT_VIEWER_AUDIT_MAX_CONCURRENT, default 2). Reads inside one snapshot transaction on a dedicated read-only connection, paged with backpressure, so concurrent ingestion/retention/redaction can never change what one export sees; a non-empty range's tail is always covered by a signed checkpoint, forcing a fresh one attoSeq(newAuditCheckpointService.createCheckpointAt,server/audit/checkpoints.ts) when the latest existing one is older. Newserver/audit/bundle.ts. Deferred, documented in the module itself: pricing-version digests are not yet chain-anchored (the pricing table predates the hash chain by two milestones and #110 itself allows deferring this to a follow-up of #83); the standaloneverify-audit-bundle.mjsscript (#401) and theagent-viewer audit export/verifyCLI plusdocs/audit-bundle.md(#402) are separate, not-yet-shipped sub-issues of #110. -
Named API keys and submitter identity (issue #107, v0.8.0 milestone):
AGENT_VIEWER_API_TOKENis now the "bootstrap" credential;POST /api/v1/keyscreates additional named, independently revocable keys (avk_<12 hex id>.<43 char secret>, stored only as a SHA-256 hash, the full token shown exactly once),GET /api/v1/keys//:idlist or inspect them (status,eventCount,firstEventAt,lastEventAt),POST /api/v1/keys/:id/revokerevokes one (idempotent, never deletes the row), andGET /api/v1/whoamireports the identity of the current credential. Every physically stored event (and its usage-ledger row) now recordssubmitter_idwith a real identity (a key's id,bootstrap,webhook, oranonymous), reusing the column issue #105's hash chain already added and hashes, so rewriting an attribution is still detected.GET /api/v1/eventsexposes it assubmittedBy(nullfor a pre-upgrade row, never back-filled), accepts?submittedBy=as a filter, and hides new internalaudit.key.created/audit.key.revokedtrail events by default (includeAudit=trueor an explicittypefilter shows them); these two types are rejected fromPOST /api/v1/events//events/batchlike any other unrecognized type. New CLIagent-viewer keys create|list|revoke|whoami(requiresAGENT_VIEWER_STORAGE=sqlite); new SDKwhoami()in both the TypeScript (packages/sdk) and Python SDKs, surfacingkey_revoked/key_expirederror codes without retrying them. Named keys have the same full read/write access as the bootstrap token in this item; role-based scopes and a read-only auditor role are issue #108. Newdocs/api-keys.md. -
Scoped API keys and a read-only auditor role (issue #108, v0.8.0 milestone): every named key (issue #107) now carries
ingest/read/audit/adminscopes (adminimplies the other three,auditimpliesread); a new default-deny policy table (server/auth/routePolicy.ts) maps every/api/v1route to the scope it needs, enforced by one middleware so a new route can only come out stricter than intended, never more open. The bootstrap token (AGENT_VIEWER_API_TOKEN) and open mode (no token configured) both resolve to an implicitadminprincipal, so nothing changes for a deployment that never creates a named key.403 insufficient_scope(with aWWW-Authenticateheader) is now distinct from401 unauthorized, checked before body validation. NewGET /api/v1/auth/whoami;POST/PATCH /api/v1/keysacceptscopes. Privilege-escalation guards: onlyadmincan create keys or change scopes, a key can never change its own scopes, the bootstrap token's scopes can't be set, and the last active named admin key can't be revoked or demoted while no bootstrap token is configured (409 last_admin). OTLP ingestion (issue #59/#73/#97) requiresingestthe same way. New migration 20 (api-key-scopes); existing #107 keys backfill to["ingest"], least privilege. Newtests/api-key-scopes.test.mjs. Deferred, documented as known gaps: closing an open SSE connection when its key is revoked or expires,submittedByon SSE reconnect-replay frames, andsubmittedByas a rollup/export dimension. -
Chain-compatible retention and redaction: tombstones, legal hold (issue #109, v0.8.0 milestone): a chained row (issue #105) is never deleted, only tombstoned:
summary/payload/event_jsonare replaced together ([removed]/{}), the per-row content salt is destroyed (crypto-shredding), and four new columns record when, why, which rules version and which chained audit event authorized it. Header fields,content_hash,hash,prev_hashandseqare never touched, so the chain keeps verifying after a tombstone. Theevents_immutabletrigger (issue #105) is replaced by one that still rejects every other UPDATE of a chained row, allowing only that exact transition. Newlegal_holdstable (a convenience projection; the evidence is the chainedaudit.legal_hold.changedevents) blocks all destructive retention/redaction while active, re-checked inside the same atomic transaction as every tombstone batch (BEGIN IMMEDIATE), so a hold placed concurrently can't be raced. Three new reserved event types (audit.retention.purged,audit.redaction.applied,audit.legal_hold.changed) and two newCanonicalEventfields (tombstone,redactionRulesVersion), both server-assigned and rejected with400 reserved_type/reserved_fieldif a client tries to send them, on both/api/v1/eventsand/events/batch. NewGET /api/v1/audit/retention(hold/tombstone/redaction status) andPOST /api/v1/audit/retention/purge,/audit/redact,/audit/legal-hold, all under theadminscope (issue #108),423during an active hold. New migration 21 (tombstones-legal-hold). Newtests/audit-retention.test.mjs. -
Audit signing key management and signed checkpoints (issue #387, v0.8.0 milestone): an Ed25519 key (from
AGENT_VIEWER_AUDIT_SIGNING_KEY,AGENT_VIEWER_AUDIT_KEY_FILE, or a generatedaudit-signing-key.pemnext to the database, mode0600; ephemeral with a warning in:memory:SQLite mode) signs periodic checkpoints over the hash chain (issue #105):"agent-viewer/checkpoint/v1\n" + canonicalize(body)(RFC 8785), chained to each other viaprevCheckpointDigest, triggered everyAGENT_VIEWER_CHECKPOINT_EVERY_EVENTS(1000) events, everyAGENT_VIEWER_CHECKPOINT_INTERVAL_MS(3600000, unref'd timer), on graceful shutdown, on demand, and once immediately on a key change (after retiring the previous key). Newaudit_keys/audit_checkpointstables (migration 22,audit-checkpoints), both append-only at the SQL level.GET /api/v1/audit/verify's walk (issue #106) now also verifies signed checkpoints, reporting the first checkpoint failure alongside the first chain break. NewGET /api/v1/audit/checkpoints(json/jsonl),GET /api/v1/audit/checkpoints/latest,POST /api/v1/audit/checkpoints(manual),GET /api/v1/audit/keys;409 audit_unavailablein memory mode. Newserver/audit/signingKey.ts,checkpointFormat.ts,checkpoints.ts,verifyCheckpoints.ts,checkpoint.schema.json. Newtests/audit-checkpoints.test.mjs. Known gaps, documented:?scope=recentstill behaves likefull(pinning a known key is issue #388), and a checkpoint signed while its key was later retired isn't flagged as such (left out to avoid clock-skew false positives). -
Hash chain over stored events (issue #105, v0.8.0 milestone, first item of the formal-audit theme): every SQLite-stored event now joins a SHA-256 hash chain, written inside the same transaction as its insert, so an edit, deletion or reordering of a chained row changes a hash that every later row already committed to. New pure module
server/chain.ts(canonicalize,eventHash,checkpointHash,UnchainedDigest), a JSON Canonicalization Scheme (RFC 8785) implementation with golden test vectors (tests/fixtures/chain-vectors.json) for cross-language verification. New migration 17 (event-hash-chain):events.prev_hash/hash/submitter_idcolumns (reusing the existingseqcolumn as the chain's position), achain_checkpointstable recording a one-timegenesischeckpoint over every pre-chain row (a single digest, not fabricated per-row hashes) and aresumecheckpoint if an older server appends unchained rows after an upgrade, and a defense-in-depthevents_immutabletrigger. New read-onlyGET /api/v1/audit/chainendpoint,?include=chainonGET /api/v1/events, and an optionalchainreceipt on accepted ingestion responses (SQLite mode only).submitter_idis recorded on every physically stored row, derived from how the request authenticated (shared token, open API, webhook HMAC, or the request behind a server-generated event), never from token material. Memory mode is explicitly not tamper-evident (GET /api/v1/audit/chainreportsenabled: false, logged once at startup). Newdocs/audit-chain.mddocuments the format, the genesis/resume checkpoint mechanics, and the honest scope of the guarantee (not a defense against a consistent rewrite by someone with full file write access, without an external anchor; #106 adds signed checkpoints for that). Newscripts/bench-ingest.ts(not part of CI) measures sequential ingestion throughput. Verifying the chain, signed checkpoints, named ingestion keys, scoped keys, chain-compatible retention and the offline audit bundle are separate items (#106 to #111). -
Chain-walk verifier and
GET /api/v1/audit/verify(issue #106, first sub-issue, v0.8.0 milestone): walks the #105 hash chain in one read-only pass (a separateDatabaseSyncconnection from the writer's, paged, yielding between pages) and reports the first broken link. New pure moduleserver/audit/verify.ts(verifyChain), reused unchanged by the CLI (#388) and extended by #387's checkpoint verification. Detectshash_mismatch(edited content),link_mismatch(a row deleted, inserted, reordered, or the first chained row's link to the genesis checkpoint tampered with),unparseable_event,column_mismatch(an indexed column edited directly, bypassingevent_json; the message never names the value) andmissing_hash(a row past every checkpoint's coverage with no hash). Legacy/unchained rows are counted inchain.unchainedwith alegacy_rowswarning, not individually walked. New migration 18 (audit-meta): a generic key/value table whose only row today is a randominstanceId, so a future signed checkpoint (#387) can be pinned to the database it came from.SQLiteEventStoregainsgetDatabasePath()(mirroring the existinggetDatabaseHandle()), used by the new route to open its own connection. In memory mode (and on a database whose schema predates migration 17), the route answers the same{ enabled: false, ... }shapeGET /api/v1/audit/chainalready uses, not a409. There are no signed checkpoints yet, soscope=recentalways behaves likefulland a consistent rewrite is undetectable by design (theno_checkpointswarning says so).docs/audit-chain.mdgains a "Verification" section;README.md/docs/README.es.mdgain the route row. Newtests/audit-verify.test.mjs(17 cases: everyBreakKind, an empty chain, legacy-row upgrades, a truncated-migrations database, page-size independence, and the HTTP route's auth/scope-validation/memory-mode/in-flight-dedup behavior). Split off per.claude/agent-policy.md's size rule: the signing/checkpoint infrastructure (#387) and theagent-viewer verifyCLI command (#388) are separate sub-issues of #106.
Audit bundle verifier
Standalone, zero-dependency: verify an offline audit bundle (agent-viewer audit export, issue #400) without installing this package. SHA-256: b854ac4aa679f545c3c91860ffd52047776af37afbf298f475193b4da1483779