Skip to main content
A manifest is one JSON or YAML object per tool (or an array of them). The machine-readable schema is schemas/manifest.schema.json. Unknown fields are rejected at every level, so a typo is an error, not a silently ignored setting.
manifests/payments.yaml
reversi validate manifests accepts this file (ok: 3 tool manifests valid).

Fields

Durations are a number of milliseconds or a string with a unit: 500ms, 30s, 15m, 2h, 1d. That applies to timeout_ms, retry.base_ms, retry.max_ms, approval.timeout, compensation.timeout_ms and verification.backoff_ms.

Effects

  • read_only: changes nothing. Skipped on rollback.
  • idempotent: safe to repeat, no undo needed (often the compensation tools themselves).
  • reversible: a true inverse exists (delete the draft you created).
  • compensable: a counteraction exists but leaves residue (a refund still appears on a statement).
  • irreversible: no safe undo (an email). Always needs approval; rollback reports it as not undone.

Compensation

argument_map values are references:
  • $args.path: from the original call’s arguments
  • $result.path: from what the tool returned
Arguments are not stored in the ledger. Reversi keeps only the values named by argument_map, in run state, so it can build the undo later. If a referenced value is missing when the tool returns, the step is marked unrecoverable rather than guessed. The plan reports kind and residue per step, so a reviewer sees what a rollback will leave behind before running it.

Verification

An API can answer “ok” and do nothing. verification calls a read-only tool after the undo and compares the answer:
  • tool: must be registered with effect read_only
  • arguments: references, resolved from the original call and result
  • expect: dotted path to expected value, deep equal. At least one entry. A path into an array uses the index (items.0.id)
  • retries (1 to 10, default 3) and backoff_ms (default 1000, doubled between attempts), because undo is often eventually consistent
If every attempt mismatches, the compensation fails with the first mismatch in the ledger. See Rollback for what happens when nothing can run the read-back.

Approval

approval: always makes a human approve each call. The object form says who and how many:
required: false in the object form means never. Internally the manifest keeps approval: always plus the rules. Rules from the manifest and from policy are merged for one call: any listed role may approve, the largest quorum wins, the shortest timeout wins. See approvals.
approval is deliberately separate from effect. A reversible tool can still need a human (large amounts), and irreversible tools always do.

Duplicate calls

The key is a template over call arguments and needs at least one {{args.field}}. Two calls of the same tool with the same rendered key are duplicates: the second is refused while the first still stands (in flight, succeeded, uncertain or being compensated). scope is run (default), actor or global. If an argument is missing or not a plain value the call is refused rather than guessed. Once the first call is undone or failed, the key is free again. This is separate from the idempotency key Reversi gives each compensation, which stays the same across retries so the downstream API can deduplicate.

Lifecycle

What the validator enforces

  • reversible and compensable tools declare a compensation; the other effects must not.
  • irreversible tools cannot set approval: never.
  • A tool cannot require itself or compensate itself. requires has no duplicates.
  • Every argument_map, verification.arguments and audit.redaction entry is a reference such as $args.id or $result.charge_id.
  • kind: inverse cannot list residue.
  • retry is only allowed on read_only and idempotent tools: retrying anything else could repeat its effect.
  • verification.expect is not empty. approval.quorum is 1 to 10.
  • Once all manifests are loaded: the compensation tool, every required tool, the verification tool and replaced_by must exist, and the verification tool must be read_only. A duplicate tool name is an error.

Schema drift

schema_hash is a short hash of the upstream tool’s input schema. When the gateway sees a different hash it adds a violation (the tool's input schema changed since its manifest was approved: review the manifest again), so in enforce mode the call is blocked until someone reviews and re-approves the manifest. reversi suggest-manifest fills it in.

Registry: versions, signing, trust

A registry is a folder of immutable versions. Every publish makes a new version; a draft becomes enforceable only when someone approves it, and the approval is itself a new version.
All writing commands take --registry <dir> (default: manifests: in reversi.config.yaml), --by, --key and --note.
  • Checksums. Each version’s checksum is in index.json and is checked on load. A file edited outside the workflow is refused.
  • Signing. With --key, each version carries an Ed25519 signature over reversi-manifest:<tool>@<version>:<checksum> and the key id.
  • Trusted key. Set manifest_public_key in the project config (or REVERSI_MANIFEST_PUBLIC_KEY) to a PEM public key. Every version must then be signed by it, or the registry does not load. Setting it for manifests that are not in a registry is an error.
  • Remote registry. manifests: may be an https:// URL serving index.json and tools/<tool>/v<N>.json as static files. The newest non-draft version of each tool is fetched, its checksum (and signature, with a trusted key) is checked, and the project’s egress: rules apply (see the gateway page). Tested against a local static server; not run against a hosted one.
  • HTTP. A server with a registry folder exposes GET /v1/registry and POST /v1/registry/manifests, .../{tool}/approve, .../{tool}/deprecate. Drafts need the operator role; approving or publishing as approved needs admin. A change reloads the engine.
approve, deprecate and rollback keep a manifest’s approval: object (roles, quorum, timeout, escalate_to) as you wrote it, and so do reversi apply and the console editor; a registry holding such a manifest loads everywhere. Two things to know:
  • manifests publish --status overrides a status: in the file: the flag (default draft) decides.
  • A draft is not enforced and is simply absent from the loaded registry, so a call to a tool that has only a draft reports “no manifest”, not “is a draft”.

Lint

Lint finds manifests that are valid but unsafe, incomplete or contradictory. It exits 1 on errors, and on warnings with --strict. --json prints the findings. With --upstream-cmd, --upstream-url or --gateway-config it also checks the manifests against the real tools’ input schemas.

Contract tests

reversi test runs manifest contract tests without touching any system: for a fake call and a fake result, does the manifest produce the undo arguments, verification and plan you expect? It reads every *.yaml, *.test.yaml or *.json under tests/ next to the config, or the files you pass, and also runs the policy’s own tests.
tests/payments.test.yaml
Case fields: tool, args, result (the fake success), fail (a message: the call fails), after (tools to run first; default is the manifest’s requires), expect_plan (compensate, skip or unrecoverable), expect_handle (undo arguments), expect_verification (verification arguments), verify_with plus expect_verified (would this read-back be accepted), expect_duplicate, and expect_blocked (a substring of the reason the call must be refused for). Exit 1 on any failure. Tools without a test are listed as not covered.

Draft manifests from a running server

It connects to an MCP server and drafts a manifest for each tool that has none, from names, descriptions, input schemas and the server’s annotations (readOnlyHint, destructiveHint, idempotentHint) and the mcp-compensate convention. Every draft has status: draft, a confidence (low or medium in the run above), comments saying what was guessed, a schema_hash, and a TODO residue where the undo needs describing. Annotations are the server’s own claims, so a draft grants nothing until a person reviews and approves it. Over the gateway the same thing is reversi.suggest_manifest.

Python and JSON Schema

The JSON Schema and the Python SDK mirror the TypeScript validator. reversi_sdk.validate_manifest returns the normalized manifest and a list of errors, and is tested against the TypeScript implementation on shared fixtures:
Optional pydantic v2 models (reversi_sdk.pydantic_models.ToolManifest) run the same validator first, so they cannot disagree with it. Manifests are read from JSON in Python; convert YAML first.