Skip to content

v0.8.0

Latest

Choose a tag to compare

@github-actions github-actions released this 13 Oct 15:37
b9513a2

[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 verify were 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, submittedBy on SSE reconnect-replay
    frames, and submittedBy as 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
    and docs/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.md and
    docs/threat-model.md is deferred; the Spanish README mirrors only the changed Security table rows.

Fixed

  • GET /api/v1/audit/verify/agent-viewer verify are now tombstone-aware (issue #408, v0.8.0 milestone): a row legitimately tombstoned by issue #109's retention/redaction routes rewrites its own event_json by design, so it never recomputes to its own stored hash again; before this fix server/audit/verify.ts's walk had no tombstone handling at all and reported hash_mismatch at 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 with tombstoned_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 chained audit.retention.purged/audit.redaction.applied event its tombstone_audit_seq names: that event must exist, be of the right type, and have committed exactly this row's original hash/content_hash, or the new tombstone_invalid BreakKind is reported (a forged or missing tombstone is not a silent pass). Mirrors the logic scripts/verify-audit-bundle.mjs's REDACTION check (#401) already implemented and tests/audit-retention.test.mjs's tombstoneAwareWalk helper already pinned in tests. docs/audit-chain.md gains 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 in tests/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.md covers 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 Docker api-token file (AGENT_VIEWER_TOKEN_FILE) and AGENT_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 by GET /api/v1/audit/verify/
    agent-viewer verify (chain.unchained count plus a legacy_rows warning, never a separate "pre-chain"
    status, never silently counted as individually verified). Linked from README.md's and
    docs/README.es.md's Security tables and from docs/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 pattern tests/audit-verify.test.mjs's legacy-rows case and
    tests/usage-rollup-migration.test.mjs already 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.yml right after the existing token/api_key doc-content check. Three checks: (1) every relative markdown link and image in README.md, the top-level docs/*.md files (including docs/README.es.md) and .github/SECURITY.md resolves to a real file, and a #anchor resolves 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 from README.md; (3) no U+2014 in any file changed since the v0.7.0 tag (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 in docs/retention.md and one each in docs/crew/evidence/{ambience-148,lamps-149}/README.md (all introduced since v0.7.0), four links to a stale docs/otlp.md#otlp-metrics-post-v1metrics anchor missing the heading's own -issue-73 suffix (README.md, docs/README.es.md, docs/integration.md, docs/otlp.md's own table of contents), two links to a stale docs/integration.md#reconnect-replay-and-resync anchor missing -issue-54 (docs/library.md, docs/library.es.md), and docs/audit.md itself, shipped with issue #388, never having been linked from README.md at all (added to the existing GET /api/v1/audit/verify row). New tests/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 resolves v0.7.0 itself, fetching just that one tag shallowly from origin if 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.md states, 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, unlike GET /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 from README.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.md and docs/api-keys.md. No server, CLI, SDK or library runtime code changed. Also documents, rather than silently assumes, that GET /api/v1/audit/verify/agent-viewer verify are not yet tombstone-aware and report hash_mismatch on 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 in docs/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 verify CLI and GET /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 through GET /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 own audit_keys table reports as active, independently re-checking every checkpoint and setting keyTrust: "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, one seq at a time through the new GET /api/v1/audit/entries/:seq route, without the server ever seeing the file; fills the new VerifyReport.exported field (server/audit/verify.ts's new parseExportedCheckpointsFile/crossCheckExportedCheckpoints). New route: GET /api/v1/audit/entries/:seq returns { seq, eventId, entryHash, prevHash } for one chained row, 404 seq_not_found outside the chain's range, 409 audit_requires_chain in memory mode; needs the audit scope (issue #108). Exit codes: 0 clean, 1 integrity failure, 2 usage error, 3 could not verify (unreachable, 401, memory storage, unreadable file). New cli/verify.ts, VerifyCommand in cli/args.ts. verifySignedCheckpoints (server/audit/verifyCheckpoints.ts) gains an optional trustedKeyPem parameter for pinning. New docs/audit.md (checkpoint file format, manual openssl/node:crypto verification without the CLI, key-handling guidance, honest limits); docs/cli.md gains a "Verify the audit chain" section; docs/integration.md gains a full /api/v1/audit/* route reference table. New tests/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/verify CLI, 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>] downloads GET /api/v1/audit/bundle (#400) to a temp file, renamed to --out only 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 standalone scripts/verify-audit-bundle.mjs (#401) as a child process and returns its exit code unchanged, never a second implementation of its checks. New cli/audit.ts, cli/packageInfo.ts's verifyScriptPath() (resolves scripts/verify-audit-bundle.mjs in a checkout, dist-cli/verify-audit-bundle.mjs once installed). scripts/build-cli.mjs now copies the verifier into dist-cli/ after every build, including --skip-viewer, so the packed tarball always ships a working copy next to cli.js. .github/workflows/release.yml uploads verify-audit-bundle.mjs and its SHA-256 as its own downloadable, installable-nothing release assets, with the checksum quoted in the release notes. New docs/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.md gains an "Audit bundle" section, README.md/docs/README.es.md's existing /api/v1/audit/bundle rows point at it. New tests/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's startViewer, 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; only node:crypto, node:zlib, node:fs and node:readline, no import from src//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 the tests/fixtures/chain-vectors.json already 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 offending seq/id; streams the bundle line by line with node: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 codes 0 pass, 1 fail, 2 invalid (unreadable/malformed/missing footer/unsupported format version), 3 unpinned (clean but no --trusted-key). Gzip auto-detected by magic bytes, not only the .gz extension. 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 (fromSeq greater than the chain's first row) always included one extra row from just before fromSeq, found by this issue's own partial-range tests. New tests/audit-bundle-verifier.test.mjs. The agent-viewer audit export/verify CLI, release-asset upload and docs/audit-bundle.md are 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: a header line (format/range metadata), every signing key (#387), the pricing versions referenced by any exported ledger row, the checkpoints (#387) covering the range, any tombstone (#109) in range, then every chained event immediately followed by its ledger row (#65) when one exists, and a footer with null-safe reconciliation totals and a contentSha256 digest. fromSeq/toSeq or from/to (mutually exclusive), gzip=1. Requires the audit scope and SQLite with the hash chain; 409 audit_requires_chain otherwise (400 invalid_range for a malformed range, 503 audit_export_busy with Retry-After above AGENT_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 at toSeq (new AuditCheckpointService.createCheckpointAt, server/audit/checkpoints.ts) when the latest existing one is older. New server/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 standalone verify-audit-bundle.mjs script (#401) and the agent-viewer audit export/verify CLI plus docs/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_TOKEN is now the "bootstrap" credential; POST /api/v1/keys creates 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//:id list or inspect them (status, eventCount, firstEventAt, lastEventAt), POST /api/v1/keys/:id/revoke revokes one (idempotent, never deletes the row), and GET /api/v1/whoami reports the identity of the current credential. Every physically stored event (and its usage-ledger row) now records submitter_id with a real identity (a key's id, bootstrap, webhook, or anonymous), reusing the column issue #105's hash chain already added and hashes, so rewriting an attribution is still detected. GET /api/v1/events exposes it as submittedBy (null for a pre-upgrade row, never back-filled), accepts ?submittedBy= as a filter, and hides new internal audit.key.created/audit.key.revoked trail events by default (includeAudit=true or an explicit type filter shows them); these two types are rejected from POST /api/v1/events//events/batch like any other unrecognized type. New CLI agent-viewer keys create|list|revoke|whoami (requires AGENT_VIEWER_STORAGE=sqlite); new SDK whoami() in both the TypeScript (packages/sdk) and Python SDKs, surfacing key_revoked/key_expired error 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. New docs/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/admin scopes (admin implies the other three, audit implies read); a new default-deny policy table (server/auth/routePolicy.ts) maps every /api/v1 route 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 implicit admin principal, so nothing changes for a deployment that never creates a named key. 403 insufficient_scope (with a WWW-Authenticate header) is now distinct from 401 unauthorized, checked before body validation. New GET /api/v1/auth/whoami; POST/PATCH /api/v1/keys accept scopes. Privilege-escalation guards: only admin can 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) requires ingest the same way. New migration 20 (api-key-scopes); existing #107 keys backfill to ["ingest"], least privilege. New tests/api-key-scopes.test.mjs. Deferred, documented as known gaps: closing an open SSE connection when its key is revoked or expires, submittedBy on SSE reconnect-replay frames, and submittedBy as 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_json are 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_hash and seq are never touched, so the chain keeps verifying after a tombstone. The events_immutable trigger (issue #105) is replaced by one that still rejects every other UPDATE of a chained row, allowing only that exact transition. New legal_holds table (a convenience projection; the evidence is the chained audit.legal_hold.changed events) 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 new CanonicalEvent fields (tombstone, redactionRulesVersion), both server-assigned and rejected with 400 reserved_type/reserved_field if a client tries to send them, on both /api/v1/events and /events/batch. New GET /api/v1/audit/retention (hold/tombstone/redaction status) and POST /api/v1/audit/retention/purge, /audit/redact, /audit/legal-hold, all under the admin scope (issue #108), 423 during an active hold. New migration 21 (tombstones-legal-hold). New tests/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 generated audit-signing-key.pem next to the database, mode 0600; 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 via prevCheckpointDigest, triggered every AGENT_VIEWER_CHECKPOINT_EVERY_EVENTS (1000) events, every AGENT_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). New audit_keys/audit_checkpoints tables (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. New GET /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_unavailable in memory mode. New server/audit/signingKey.ts, checkpointFormat.ts, checkpoints.ts, verifyCheckpoints.ts, checkpoint.schema.json. New tests/audit-checkpoints.test.mjs. Known gaps, documented: ?scope=recent still behaves like full (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_id columns (reusing the existing seq column as the chain's position), a chain_checkpoints table recording a one-time genesis checkpoint over every pre-chain row (a single digest, not fabricated per-row hashes) and a resume checkpoint if an older server appends unchained rows after an upgrade, and a defense-in-depth events_immutable trigger. New read-only GET /api/v1/audit/chain endpoint, ?include=chain on GET /api/v1/events, and an optional chain receipt on accepted ingestion responses (SQLite mode only). submitter_id is 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/chain reports enabled: false, logged once at startup). New docs/audit-chain.md documents 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). New scripts/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 separate DatabaseSync connection from the writer's, paged, yielding between pages) and reports the first broken link. New pure module server/audit/verify.ts (verifyChain), reused unchanged by the CLI (#388) and extended by #387's checkpoint verification. Detects hash_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, bypassing event_json; the message never names the value) and missing_hash (a row past every checkpoint's coverage with no hash). Legacy/unchained rows are counted in chain.unchained with a legacy_rows warning, not individually walked. New migration 18 (audit-meta): a generic key/value table whose only row today is a random instanceId, so a future signed checkpoint (#387) can be pinned to the database it came from. SQLiteEventStore gains getDatabasePath() (mirroring the existing getDatabaseHandle()), 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, ... } shape GET /api/v1/audit/chain already uses, not a 409. There are no signed checkpoints yet, so scope=recent always behaves like full and a consistent rewrite is undetectable by design (the no_checkpoints warning says so). docs/audit-chain.md gains a "Verification" section; README.md/docs/README.es.md gain the route row. New tests/audit-verify.test.mjs (17 cases: every BreakKind, 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 the agent-viewer verify CLI 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