Skip to main content
Every command here was run, in this order, against this repository, in a fresh scratch directory. The expected output is copied from that run and trimmed (... marks cut text; ids, times and your scratch path will differ). For the rest of the manual tests see the main testing guide. Everything runs from one terminal. Servers are started in the background, write to a log file, and are stopped with kill. Ports used: 7700, 7701, 7702 (all on 127.0.0.1).
This page covers what is built. There is no Terraform provider and no Kubernetes operator: reversi apply (scenario 6) and the Helm chart are the supported ways to manage Reversi from files.

0. Setup

You need Node 22.13 or newer (node -v), curl, openssl and shasum, and a repository built with npm install && npm run build.

1. reversi init and reversi doctor

init creates a project: a config file, example manifests, a starter policy, a contract-test file and a data store.
reversi.config.yaml is found from the current folder or any parent, by the CLI, the server and the gateway. Relative paths in it are relative to the file. See what Reversi resolved:
doctor checks the Node version, the config, that the manifests load and lint, that the data store opens and the ledger chain is intact, the policy and its own tests, signing key, notification channels, network rules, that the data directory is writable, and (with --upstream-cmd, --upstream-url or --gateway-config) that an MCP server answers.
The example contract tests and the policy’s own tests run without touching anything:
A failing doctor, and the fix. Break a manifest, run doctor, then put it back:
What this proves: a project is one folder with one config file; doctor finds a broken setup, names the file and the rule, and exits 1; the example contract and policy tests pass.

2. Observe-first: a gateway with no manifests

The aim is to put Reversi in front of an MCP server on day one, change nothing about the agent, and see what it does. The upstream is the real filesystem connector.
manifests/ is empty and the config names no mode, so the gateway starts in observe mode. The startup banner lists every upstream tool and how it is classified:
Now act as an agent. --run obs-1 makes every one-shot call continue the same run, as a real agent session would.
All three calls ran (the delete really deleted). Look at the journal:
Each call is a would_block (what enforce mode would have done), then intended (written before the call is forwarded), then the outcome. The same records as JSON Lines, and a check that the hash chain holds:
What this proves: with no manifests and no changes to the agent, every call is forwarded and journaled with its reason, arguments only as a hash, and a hash-chained record; nothing is blocked; the banner tells you what to classify next.

3. From unclassified to signed, approved manifests

3.1 Draft manifests from the live server

The word after each file is the confidence. Every draft has status: draft, so none can be enforced until a person approves it. Read one:
Tools with no obvious undo are drafted as irreversible with approval: always (the safe default); read-only names are drafted read_only. schema_hash is used in 5.5. Review means editing: name an owner, set a timeout, replace the TODO. Also remove two drafts on purpose, so two tools stay unclassified for scenario 4:
lint finds unsafe, incomplete or contradictory manifests (exit 1 on errors, or on warnings with --strict). With --upstream-cmd, --upstream-url or --gateway-config it also checks the manifests against the server’s real tools.

3.2 Contract tests for manifests

A test file is a list of cases. Each runs in a sandbox: no system is touched. Write three:
Drafts cannot be enforced, so the sandbox refuses them:
Test the content as it will be enforced, using a throwaway copy marked approved:
Break a test on purpose (the wrong backup id) to see a real failure:

3.3 Publish, approve and verify with a signing key

publish makes drafts by default. --four-eyes forbids the author approving their own draft. A different person approves (each approval is a new, signed version):
(manifests deprecate and manifests rollback <tool> <version> complete the set; not run here.)

3.4 Use the registry as the manifests source, then tamper with it

Point the project at the registry and require its signature. The gateway, the server and the CLI all read these two settings:
validate, lint, test and plan read a registry folder too (the current non-draft version of each tool). Now change one registry file behind the registry’s back (grant fs.write_file a weaker permission):
verify, doctor and the gateway all refuse the tampered registry; the gateway does not start: it prints that one-line error (exit code 64) followed by its usage text, not a stack trace. Restore the file:
What this proves: drafts are generated from the real server, tests and lint run before anything is enforceable, approval is a separate signed step by a second person, and any after-the-fact edit to the registry is detected.

4. Switch to enforce

Create keys for an agent, an approver, an operator and an admin (only SHA-256 hashes are stored), and an enforcing config:
The registry’s manifests are enforced, and the observe-mode journal from scenario 2 is still in the same data directory.
Approve from the CLI (it shares the data directory with the gateway), then the agent retries:
The CLI decides through the engine, so the decision is in the ledger (an approved record with the approver as actor), delegations apply, and notification channels in the config are told. The approval is single-use (consumed). ls sandbox prints nothing: the file is gone.

Rollback

Two writes in a new run, then see what a rollback would do:
The gateway holds the connection to the upstream, so the rollback that works against a gateway is the operator endpoint: it calls each compensation on the upstream, newest first. Without confirm it is a dry run.
new.txt is gone and keep.txt has its old text. The agent key cannot even connect to the operator endpoint.
reversi rollback <run> --compensator-cmd '...' is the CLI path for a compensator you supply (a script that undoes a call). It only trusts that command’s exit code, so it is not used here: a command that exits 0 without undoing anything marks the step compensated. For a gateway, use the operator endpoint above (or the console).
What this proves: the unclassified tool is blocked, an allowed call runs, an approval-required call is blocked until a named approver decides and then runs once, and a run is rolled back through the same upstream with every step in the ledger. Stop this gateway:

5. Gateway features

A small upstream of our own, so every feature can be triggered. It has four tools: notes_get and notes_slow and notes_flaky (read-only hint) and notes_add. notes_flaky fails twice per id, then works. Creating drift.flag changes notes_add’s schema.
Here the reviewed drafts were approved in bulk (the page’s point is the gateway, not the review). notes_add is declared idempotent and notes_slow has a 500 ms timeout; notes_flaky allows 3 attempts (retry is only accepted for read_only and idempotent tools, because repeating anything else could repeat its effect). Keys for this gateway, a config in advise mode, and start it:

5.1 Advise mode: an advisory is appended, nothing is blocked

The call ran, the result carries an extra text block saying what enforce mode would have done, and the ledger has a would_block.

5.2 Enforce: schema validation, timeout, retry

Gateway arguments are checked against the upstream tool’s own inputSchema (and results against its outputSchema) unless validate_schemas: false. Restart in enforce mode:
The 2000 ms call hits the manifest’s timeout_ms and is journaled as uncertain (the gateway cannot know whether it finished; see reversi runs show f-2). notes_flaky threw twice; the gateway retried (100 ms, then 200 ms backoff) and the third attempt succeeded: "attempts":3 is the upstream’s own count. Retry never applies to a tool that is not read_only or idempotent.

5.3 Hot reload: a manifest edit, and a broken edit

The gateway polls the manifest files (every 2 s by default; reload.watch and reload.interval in the project config; SIGHUP forces it). Remove one manifest while it runs:
Now a broken edit, then restore the file. The bad file must be rejected and the old configuration kept:
The call still works under the previous, valid configuration.

5.4 unclassified_reads: observe

A tool with no manifest that the server itself annotates read-only can be let through (journaled as unclassified, in a separate observe-mode run named <run>-reads) while every other unclassified tool stays blocked. notes_get has no manifest right now:
(The no such note answer is the upstream’s: it was restarted, so its notes are gone.) The call ran and is marked unverified. Put the manifest back; the running gateway picks it up:

5.5 Schema drift

Each manifest can carry the schema_hash of the tool’s schema as it was when someone reviewed it (suggest-manifest writes it). If the upstream later changes the schema, the gateway refuses the call until the manifest is reviewed again. Change the upstream’s schema, wait out the gateway’s 5 s tool-list cache, and call:
Review, then record the new hash. The new value comes from a fresh draft; the running gateway reloads the manifest:

5.6 reversi.classification, reversi.preview_impact, reversi.suggest_manifest

reversi.preview_impact is on the agent endpoint (an agent can ask “would this be allowed, and could it be undone?” before it tries). reversi.classification and reversi.suggest_manifest are on the operator endpoint only, so they need an operator key. The tiny client is the same mcp.mjs used throughout.
suggestions is empty because every upstream tool already has a manifest (they are skipped). The last line shows the agent cannot list the other two tools. Stop the gateway:
What this proves: advise mode never blocks; enforce mode validates arguments against the upstream’s schema, applies per-tool timeouts and safe retries, and notices schema drift; manifest edits take effect without a restart, and a broken one is rejected while the old set stays in force; the agent can ask what a call would do before making it.

6. GitOps with reversi apply

reversi apply <dir> makes a registry (and policy) match a directory kept in git. Only differences are published; nothing is ever deleted (--prune deprecates tools that left the directory). The directory holds manifests/*.yaml (or manifest files at the top level) and, optionally, policy.yaml.

6.1 Dry run and the drift check

Exit 2 means “there are changes” and nothing was written. --detailed-exitcode sets exit 2 only together with --dry-run (without --dry-run a successful apply exits 0). Use it as a CI drift check.

6.2 Apply, re-apply, edit, prune

Manifests are applied as approved because the directory in git is the reviewed source of truth (--status draft to apply drafts). The second run changes nothing. Edit one file and apply again:
Remove a file from the directory. Without --prune the dry run reports nothing; with it the tool is deprecated, not deleted:

6.3 Against a running server

A server only accepts registry changes if its manifests and policy are versioned folders, so seed one with apply, then start the server on them with an admin key and an operator key:
An operator key can read the registry (so it can compute the plan) but is refused when the plan would publish an approved manifest or a policy: that takes the admin role. The server reloads its configuration after each applied change. Stop everything and clean up:
What this proves: a directory in git is the source of truth; --dry-run --detailed-exitcode detects drift without changing anything; apply is idempotent and signs what it publishes; removal is a deprecation, never a deletion; and a server applies only for an admin key.

Not covered here

  • The console (browser) screens for manifests, policy versions and connector health; see the console.
  • reversi manifests deprecate and manifests rollback, and policy versions on their own.
  • A remote https manifests source and manifest_public_key against it; only a local registry folder was exercised.
  • reversi rollback --compensator-cmd with a real compensator (shown only as a caution in scenario 4), and --verify-cmd and --reader-cmd.
  • Gateway OIDC authentication, allow and deny lists, and several upstreams at once.
  • The Helm chart, Dockerfile and live third-party connectors are written but not exercised against real clusters or services in this repository’s own test pass.
  • OIDC browser-redirect login for the console is not built (login is an API key or an OIDC access token).