retention: section in the policy, nothing is ever deleted.
The retention policy
policy.yaml
default_daysis required (a positive integer).- Each rule has a
matchanddays.matchfields, all optional and all must hold:tool(glob, matched against the tools the run called),tenant,environment,data_class(from the manifest’srisk.data_class). - A run is kept for the longest of its default, every matching rule, and
audit.retention_dayson the manifest of any tool it called. - A manifest with
audit.evidence_export: requiredmeans the run cannot be deleted until its evidence has been archived as a signed bundle (see below).
tools.yaml
reversi retention apply from cron or a job.
Plan and apply
plan lists every run with its age, how long it is kept and what would happen: keep, due, due; needs an evidence bundle first, or HELD (<reason>). --days <n> replaces the policy’s setting for a one-off run. apply deletes the
due runs, prints deleted: … (would delete: … with --dry-run) and a kept <run>: <reason> line for each run it left
alone.
- Holds win. A held run is skipped with
legal hold hold_…: <reason>. - Evidence export. With
--archive-dirand a signing key (--key, or the configuredsigning_key) every deleted run first gets a signed bundle at<archive-dir>/<run_id>.json, verifiable withreversi verify-bundle. A run whose manifest requires export and that has no archive folder or key is skipped:a manifest requires an evidence bundle before deletion; give an archive directory and a signing key. - Recorded. Each deletion appends a
purgedrecord to the ledger (retention: older than N days, with the archive path) and sends aretention_purgednotification.
Both answer
404 no retention settings when the policy has no retention: section. The server archives with its signing
key (REVERSI_SIGNING_KEY) and writes to the archive_dir you give it, on the server’s disk.
Legal holds
A hold stops retention for the runs it covers, for as long as it is active.- Scope. Any of
--run,--tenant,--actor,--tool; all given fields must match.--toolis an exact tool name and matches a run that called that tool at least once. A hold with no scope is refused (a legal hold needs a scope: a run, tenant, actor or tool), and the reason must be at least 5 characters (a case or matter reference). - Effect. Retention skips covered runs. A hold does not touch the ledger, which is never deleted anyway, and it does not extend the time-to-live of captured arguments.
- Release keeps the record (
hold list --allshows it as released) and makes the run due again. - HTTP:
GET /v1/holds(auditor,?all=1for released ones),POST /v1/holdswithrun_id,tenant,actor,tool,reason(admin),DELETE /v1/holds/{id}(admin). Console: Settings, Legal holds (admin).
Data residency and egress controls
Reversi itself sends data out in a few places: notification channels, anchor webhooks, OpenTelemetry export, remote manifest registries and the gateway’s upstream servers. Theegress settings control where.
reversi.config.yaml
Only
http and https are allowed. Hostnames are resolved before connecting, so a name that points inside the network is
caught too, and every redirect (at most three) is checked the same way. Refusals read, for example:
reversi doctor checks each configured notification, trace and webhook URL against these rules. The single
REVERSI_APPROVAL_WEBHOOK goes through the guard too. The server’s own Settings page
says whether egress rules are in force.
Personal data in capture
The ledger never stores arguments. Replay, simulating a policy against history and impact previews need them, so there is an opt-in capture store, off by default.reversi.config.yaml
- Credentials (API keys, tokens, private keys and similar patterns) are always replaced, in any field.
- Personal data, while
redact_piiis on: emails, phone numbers, card numbers (must pass the Luhn check), US social security numbers and IBANs (must pass the checksum). Patterns are conservative on purpose, so an ordinary number is not masked. Setredact_pii: falseonly if you accept storing them. - Named fields. A manifest can list more paths:
audit.redaction: ["$args.client_secret", "$result.token"]. They become[REDACTED]. - Size. Strings are cut at 20,000 characters, and long lists and objects at 200 entries.
- Expiry. Records past their time-to-live are no longer returned;
reversi capture purgedeletes them, andreversi capture purge --run <id>deletes everything captured for a run. Retention also purges a run’s captures when it deletes the run.

