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
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 effectread_onlyarguments: references, resolved from the original call and resultexpect: 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) andbackoff_ms(default 1000, doubled between attempts), because undo is often eventually consistent
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
{{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
reversibleandcompensabletools declare a compensation; the other effects must not.irreversibletools cannot setapproval: never.- A tool cannot require itself or compensate itself.
requireshas no duplicates. - Every
argument_map,verification.argumentsandaudit.redactionentry is a reference such as$args.idor$result.charge_id. kind: inversecannot list residue.retryis only allowed onread_onlyandidempotenttools: retrying anything else could repeat its effect.verification.expectis not empty.approval.quorumis 1 to 10.- Once all manifests are loaded: the compensation tool, every required tool, the verification tool and
replaced_bymust exist, and the verification tool must beread_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.jsonand is checked on load. A file edited outside the workflow is refused. - Signing. With
--key, each version carries an Ed25519 signature overreversi-manifest:<tool>@<version>:<checksum>and the key id. - Trusted key. Set
manifest_public_keyin the project config (orREVERSI_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 anhttps://URL servingindex.jsonandtools/<tool>/v<N>.jsonas 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’segress: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/registryandPOST /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 --statusoverrides astatus:in the file: the flag (defaultdraft) 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
--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
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
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:
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.
