Signed bundles
A bundle holds ledger records (the whole ledger, or one run), the ledger head at export time and an Ed25519 signature over a canonical summary.verify-bundle recomputes every record hash, checks the chain where it applies and checks the signature.
- Whole-ledger bundle: the full hash chain from genesis to the recorded head is verified.
- Single-run bundle: each record’s own hash and its link to its neighbour are verified. It is a subset, so it cannot prove nothing else happened; keep the whole-ledger bundle for that.
--pubchecks the signature against a key you already trust. Without it, a valid signature only proves the bundle is self-consistent: anyone could re-sign an edited bundle with their own key.
GET /v1/runs/{id}/bundle, /v1/ledger/bundle) when started with REVERSI_SIGNING_KEY
(or signing_key: in the project file), and publishes the public key at /v1/evidence/key. Give auditors the public key
out of band. The same key signs anchors and retention archives.
Anchoring
A hash chain detects edits, deletions and reordering. It cannot detect truncation of the tail or a rewrite of the whole file by someone who controls it. Anchoring publishes the ledger head somewhere the operator cannot quietly rewrite, so that later you can prove the history is unchanged.What is anchored
One JSON line per anchor: the hash of the last record (head), how many records the ledger had (records), a timestamp,
the data region if set under egress, and, when a signing key is configured, the key_id and
an Ed25519 signature.
Sinks and schedule
reversi.config.yaml
- File sinks append one line per anchor. Webhook sinks POST the JSON; with
secretthe body is signed in thex-reversi-signature: sha256=<hmac>header, which you check like a notification webhook. Webhooks go through the egress guard and time out after 10 s. - The server and gateway anchor once at start and then every
interval(default one hour). A failing sink is logged and does not lose the anchor for the other sinks. - On demand:
reversi anchor publish [--file <path>] [--webhook <url>] [--secret <s>] [--key <file>](it uses the sinks in the config when none are given, and exits 1 if any sink failed), orPOST /v1/anchor(admin).
reversi report and
reversi bundle from cron if you want files on a timetable.
Verifying
--pub; an unsigned anchor is then rejected), the ledger must still have at least that many records, and the
record at that position must still have the anchored hash. A shortened ledger fails with
the ledger has 5 records but was anchored at 9: it has been truncated; a rewritten one with
history was rewritten. Exit code 1 if any anchor fails. Keep the anchor file where the ledger’s operator cannot edit it,
otherwise it proves nothing.
Run report
GET /v1/runs/{id}/report (HTML) and GET /v1/runs/{id}/report.pdf; the console links both.
Period report
What the agents did over a window, for a weekly review or an auditor.GET /v1/report?since=7d&format=md|html|json (auditor and up).
Retention archives
When retention deletes a run’s working data, it can first write a signed bundle of that run’s ledger records to an archive folder (reversi retention apply --archive-dir ./arch --key signing.key). A manifest with
audit.evidence_export: required makes that mandatory. The archive is an ordinary single-run bundle:
purged event.
Offline verification without Node
The ledger format uses SHA-256 so anyone can verify it:python -m reversi_sdk verify ledger.jsonl, the browser viewer, or
sha256sum and a short script. Bundle signatures need an Ed25519 verifier (OpenSSL 3, any crypto library).
