Skip to main content
The ledger is append-only and holds hashes, reasons and details, never argument values, so it is never deleted. Retention removes the working data around a run: its run state, its approval requests and its captured (redacted) arguments. Without a retention: section in the policy, nothing is ever deleted.

The retention policy

policy.yaml
  • default_days is required (a positive integer).
  • Each rule has a match and days. match fields, all optional and all must hold: tool (glob, matched against the tools the run called), tenant, environment, data_class (from the manifest’s risk.data_class).
  • A run is kept for the longest of its default, every matching rule, and audit.retention_days on the manifest of any tool it called.
  • A manifest with audit.evidence_export: required means the run cannot be deleted until its evidence has been archived as a signed bundle (see below).
tools.yaml
Retention runs when you ask it to: there is no built-in timer, so run 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-dir and a signing key (--key, or the configured signing_key) every deleted run first gets a signed bundle at <archive-dir>/<run_id>.json, verifiable with reversi 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 purged record to the ledger (retention: older than N days, with the archive path) and sends a retention_purged notification.
The same actions exist on the server and in the console: 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. 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. --tool is 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 --all shows it as released) and makes the run due again.
  • HTTP: GET /v1/holds (auditor, ?all=1 for released ones), POST /v1/holds with run_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. The egress 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
Everything is redacted on the way in, before it is stored:
  • Credentials (API keys, tokens, private keys and similar patterns) are always replaced, in any field.
  • Personal data, while redact_pii is 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. Set redact_pii: false only 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 purge deletes them, and reversi capture purge --run <id> deletes everything captured for a run. Retention also purges a run’s captures when it deletes the run.