Skip to main content
Every command here was run against this repository. Run them from the repository root, in bash or zsh. Scratch files go in try/ (git-ignored); delete it any time. Use separate terminals where it says so.
This guide covers what is built. Things listed under “Not built” in the roadmap have nothing to test yet.

0. Prerequisites and build

You need Node 22.13 or newer (node -v), Python 3.9+ for the SDK, curl, and openssl. Docker is optional (sections 8 and 12).
If node -v shows an older version (nvm: nvm use 22), nothing else will work. The one-line helpers used below:

1. The automated suites

Expect Test Files … passed and OK. PostgreSQL tests are skipped unless you point them at a throwaway database (section 8): REVERSI_TEST_PG=postgres://… npm test.

2. The built-in demo and the viewer

Expect: an order and a charge are created, an email is sent, delete_customer is blocked (“permission customers:admin is not in the run’s scope”), the plan lists the email as unrecoverable, rollback compensates steps 2 and 1, and the last line says chain intact. Then open viewer/index.html in a browser and choose examples/order-agent/out/ledger.jsonl. You should see the timeline and a green “chain verified”. Open the file in an editor, change one letter inside a record, reload it in the viewer: it must flag the first bad record.

3. Manifests

4. Policy as code

Break it: change expect: require_approval to expect: allow and run policy test again. You should see FAIL … expected allow, got require_approval and exit code 1.

5. The server, end to end

5.1 Create keys and start it

Terminal A:
Expect reversi-server listening on http://127.0.0.1:7421 (7 tools, files store). Terminal B (all the rest of section 5):

5.2 An agent works; policy, approvals, content checks, scope

Approval flow (a charge over 100 needs a human, by the policy above):
Also check that a different amount is not covered: ask for "amount":900 after a fresh approval for 500 and see it blocked again. Content check and scope:
An unknown outcome:

5.3 Reconcile, roll back, verify

Look at the event trail: blocked, approved (by alice, from the approval decision), approved (the engine consuming it), intended, succeeded, uncertain, failed (resolved by dana: …), compensating/compensated (actor cli).

5.4 Evidence

5.5 Metrics, kill switch, logs

In terminal A you should see one JSON line per request (method, path, status, ms, principal) and no bodies or keys.

5.6 The console (browser)

Open http://127.0.0.1:7421/console/login, paste $APPR (print it with echo $APPR), sign in. Then:
  1. Runs: try-1 is listed. Use the filter “needs attention”.
  2. Run detail: steps with statuses, the rollback plan, the event trail, the report link and signed-bundle link.
  3. Approvals: ask for another big charge in a new run from the agent, then approve it with the form (add a note). Try Deny on another.
  4. Tools: manifests with effect, permission and compensation; “called without a manifest”.
  5. Sign out, sign in with $AUD (the auditor): the approve buttons are gone, and a forged POST is refused.
  6. Sign in with $ROOT and use Evidence → Block all new calls, then resume. The red banner shows while halted.
Security spot checks: the cookie is HttpOnly; SameSite=Strict; a form POST without the CSRF field returns 403. Stop the server (Ctrl-C in terminal A) when done.

6. The MCP gateway

The gateway fronts an MCP server; here the upstream is the built-in filesystem connector. Create the config and manifests:
Terminal C:
Terminal B:
Agent calls (--run gw-run-1 makes every one-shot invocation continue the same run, as a real agent’s session would):
Approve from the CLI (it shares the data directory with the gateway):
Operator endpoint and rollback:
Also try: reversi.halt (admin token: $ROOT) then an agent call; reversi.export_evidence; and deny_action instead of approve. Observe mode: stop the gateway, change mode: observe in try/gateway.yaml, start it again, and call nope: it is no longer blocked, so the call goes on to the upstream (which has no such tool, so it fails), and the ledger shows would_block then intended then failed: rv export -d try/gw-data | grep -E 'would_block|failed'. With a tool the upstream does have but that has no manifest, the call runs. stdio mode (no listen:, no auth:: one unauthenticated session, as an MCP client like Claude Code would launch it):
Stop the gateway with Ctrl-C.

7. Connectors

The filesystem connector is covered above. For the others you need sandbox credentials; use throwaway accounts, never production. List and compensate by hand:
GitHub (a throwaway repository, token with Issues write): add an upstream to the gateway config and call it:
(Give the agent key github:* permissions in max_permissions; see each tool’s permission in the manifests.) Slack: slack.post_message {"channel":"C…","text":"hi"} then rollback deletes the message (SLACK_BOT_TOKEN). Stripe: stripe.create_customer {"email":"t@example.com"} then rollback deletes it (STRIPE_SECRET_KEY = a sk_test_… key); stripe.create_refund needs approval and cannot be undone: check that the plan says so. The connectors are tested against fakes of these APIs; this is the first time they meet the real ones.

8. SQLite and PostgreSQL

SQLite (a single file, Node 22.13+): restart the server from 5.1 with REVERSI_STORE=sqlite REVERSI_DATA=try/sqldata, repeat a few calls, then:
Expect rejected: ledger is append-only. PostgreSQL with Docker:
In another terminal create a run through localhost:7423 as in 5.2, then:
Two writers, one ledger: start a second server on port 7424 against the same database, make calls through both, and run rv verify --store postgres: the chain stays intact.

9. Authentication

Roles. With the keys from 5.1, repeat these and check the status codes: an agent key on /v1/approvals (403), an approver key on /v1/runs/try-1/plan (200) but on /v1/runs/try-1/steps/0/compensation (403), an operator key on /v1/halt (403), the admin key on it (200). Key rotation. Add "expires_at": "2000-01-01T00:00:00Z" to one key in try/keys.json and wait two seconds (or kill -HUP the server): that key now returns 401, others keep working. Set "disabled": true for the same effect. Break the JSON on purpose: the server logs key reload failed, keeping the old keys and nothing changes. OIDC with the bundled mock provider. Terminal D: node examples/oidc/mock-idp.mjs. Then:
The ledger actor for the bot’s runs is its sub (bot-1). You can also paste a token into the console login.

10. Approval notifications

Terminal D: node examples/webhook-sink.mjs. Start any server or gateway with REVERSI_APPROVAL_WEBHOOK=http://127.0.0.1:9100/hook REVERSI_CONSOLE_URL=http://127.0.0.1:7421, trigger an approval (5.2 or 6) and watch the sink print a Slack-style text plus the structured request. It never contains argument values. Stop the sink and trigger another: the call is still blocked normally and the server logs approval webhook failed.

11. The Python SDK

Expect created …, a charge, and blocked: … for the out-of-scope email. See the SDK page for approvals and rollback calls (Blocked.approval_id, run.rollback(...)).

12. Docker

(The first line of output should be the listening message; docker ps shows healthy after ~10 s.) The Compose stack on PostgreSQL: follow the comments at the top of deploy/docker-compose.yml.

13. Docs and CI

Every page in the left navigation should open and render, including this one. CI (.github/workflows/ci.yml) runs the test suites with a PostgreSQL service, the fixture check, npm audit, an SBOM and a secret scan, and builds the Docker image.

Cleanup