... 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.
doctor, then put it back:
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:
--run obs-1 makes every one-shot call continue the same run, as a real agent session would.
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:
3. From unclassified to signed, approved manifests
3.1 Draft manifests from the live server
status: draft, so none can be enforced until a person approves it.
Read one:
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: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:
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: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: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).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.
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
would_block.
5.2 Enforce: schema validation, timeout, retry
Gateway arguments are checked against the upstream tool’s owninputSchema (and results against its outputSchema) unless
validate_schemas: false. Restart in enforce mode:
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:
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:
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 theschema_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:
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:
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
--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
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:
--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 withapply, then start the
server on them with an admin key and an operator key:
--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 deprecateandmanifests rollback, and policy versions on their own.- A remote
httpsmanifests source andmanifest_public_keyagainst it; only a local registry folder was exercised. reversi rollback --compensator-cmdwith a real compensator (shown only as a caution in scenario 4), and--verify-cmdand--reader-cmd.- Gateway OIDC authentication,
allowanddenylists, 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).

