Skip to main content
Every command here was run in a scratch folder against the built code, in the order shown. Run them from the repository root in bash or zsh. Scratch files go in try/gov/ (delete the folder to start over). The general guide is Manual testing guide; this page covers the governance features only.
You need Node 22.13 or newer, a built repository (npm install && npm run build), curl, openssl and jq. The server listens on port 7750 here. Ids (apr_...), timestamps and hashes differ on every run; the rest of each output should match.

0. Setup

0.1 Helpers and files

Nine tool manifests. Read them: they show the risk fields, the idempotency key, the timeout and the undo of each tool.
The baseline policy (version 1): a rule that routes big payouts to two people of the finance group, one that routes access grants to the security group and escalates after 3 seconds, a rate limit, a cumulative budget and risk scoring.

0.2 Registry, policy versions, config and keys

The console can only edit manifests that live in a registry folder and a policy that lives in a policy versions folder. Create both with the CLI, and a project config file that points at them.
Expect:
manifests publish stores every manifest with the status you pass (draft by default) and ignores a status: written in the file. A draft is not enforced. Keys: an agent, a second agent that belongs to tenant acme, an auditor, two approvers in the finance group, one in security, an operator and an admin.

0.3 Start the server (terminal A)

Terminal A
Expect reversi-server listening on http://127.0.0.1:7750 (9 tools, files store, console login). The registry holds ten tools but the draft is not loaded.

0.4 Helpers (terminal B)

Expect {"ok":true,"ledger_head":"0000...","halted":null}.

1. Rich manifests in action

What this proves: the extra manifest fields change what the engine does, and the ledger says why.

1.1 The fields are stored

Expect owner payments-team, risk financial / high, timeout_ms:"5s", and the idempotency template.

1.2 A duplicate call is blocked

Expect allow, then the second identical order is refused:
The key is charge-A100, rendered from the arguments, and scope: run means a different run could charge A100 again (use scope: actor or global to widen it).

1.3 A deprecated tool still runs, and is flagged

Expect the call to be allowed ({"decision":"allow","step":1}), and the events to carry the risk score, the duplicate refusal and the deprecation note:

1.4 A draft is not enforced

With a registry, a draft is simply not loaded, so the call is refused as unknown:
With a plain manifest file, status: draft is loaded and refused by name. Try it on a throwaway second server (port 7751):
Expect {"decision":"block","reasons":["the manifest for export_report is a draft: publish it before it can be enforced"]}.

1.5 Timeout

timeout_ms bounds the upstream call made by the gateway: after that many milliseconds the gateway reports upstream did not answer within N ms. The HTTP server does not call tools itself, so there is nothing to time out here; this page does not exercise it (see “Not covered here”).

2. Policy: rules, limits, budgets, risk, windows, freezes, versions

What this proves: policy can refuse or hold a call for reasons other than the manifest, and each refusal names the rule.

2.1 Check and test the file

2.2 A rate limit

Expect three allow lines, then {"decision":"block","reasons":["limit balance-polling: 3 calls per 60s exceeded"]}.

2.3 Risk scoring

Thresholds are approve: 80, deny: 100. A 600 charge scores 30 + 20 + 20 + 15 = 85, so a human is needed. A 9000 charge scores 100 and is refused.
Both reasons are reported: the 9000 charge also breaks the budget of section 2.4. The pending request for R1 is decided in section 3.

2.4 A cumulative budget

The budget is 1000 per actor per day, and m-1 already spent 200. Two charges of 300 fit; the warning at 80 percent is written to the ledger; the third does not fit.
The budget counts only calls that were allowed to start, and is shared by every run of the same actor (per: actor).

2.5 Version 2: a time window, a freeze, and a window that is closed

The next file is version 1 plus the lines marked new. The day names and the freeze period are computed so that one window covers now and one does not. All times are UTC.
A window rule fires only while the time is inside the window (days, between: "09:00-17:00", timezone; outside: true inverts it). A freeze with no match blocks every call that is not read_only. Both read the real clock, so this file is only valid on the day you create it. I dropped the version-1 “big payout” test because payouts are now paused.

2.6 Publish it, watch it enforce, then roll back

The open window blocks the payout, the freeze blocks the deploy, the closed window lets the refund through, and a read is never frozen. Now the history, the diff, and the rollback to version 1 (a policy version is activated, never edited):
(history prints *active* on v2 before the rollback; the last command shows the deploy allowed again once v1 is back.)

3. Approvals

What this proves: approvals can be routed to groups, need N different people, can be delegated, escalate when nobody answers, and can be forced only by an admin with evidence. The requester can never approve their own call.

3.1 Routing to a group, quorum 2

The list also holds the risk-score request from section 2.3. It shows argument names (amount, to), never values. The approval is bound to the exact arguments and used once.

3.2 The same from the CLI, with --groups

The CLI trusts the host, so you say who you are and which groups you decide as:
(pending by null after the first vote means the quorum is not reached yet.) The CLI decides through the engine, so the decision is in the ledger too, and notification channels in the config would be told:
Deny works the same way, and a denied call stays blocked with the reason:

3.3 Delegation: grant, use, expire

Alice hands her finance authority to dana for a very short time (0.005 hours is 18 seconds; the console’s form takes whole hours). Dana is an operator, not in finance.
A delegate’s vote counts as the delegator’s, so Alice cannot also vote. After 18 seconds the delegation is spent:
Delegation is honoured everywhere a decision is made, including the CLI on the host. Repeat the first half with a fresh request, and let Dana decide with reversi approvals approve instead of the API (no --groups; the delegation supplies finance):
That delegation stays active for an hour; end it with rvg delegate revoke <id> (the id is in rvg delegate list) before the later sections.

3.4 Escalation on timeout

grant_access goes to the security group with a 3 second timeout, then to operator. Approve quickly after the escalation: the approval itself lapses at the same deadline.
After escalation sam (security) is refused and any operator is accepted.

3.5 Break-glass

Only an admin can force a pending approval, with a reason of at least 10 characters and a reference to the evidence. It is recorded as break-glass, not as a normal approval.

3.6 Decide in the console (login mode, key paste)

  1. Create a request:
  1. Open http://localhost:7750/console/login, paste the value of $SAM (print it with echo $SAM), Sign in.
  2. Click Approvals. In the Pending table the row for payout shows Who may decide: finance (0/2), the policy reason, and the argument names. Type a note and click Approve: the page says Could not decide: this request needs an approver from: finance.
  3. Log out, sign in with $ALICE, Approvals, Approve. The row now shows finance (1/2). Alice cannot vote twice.
  4. Log out, sign in with $BOB, Approve. The request moves to Recent decisions as approved by alice, bob.
  5. Sign in with $ROOT (admin): pending rows also have a Break-glass form (reason and ticket). Under Delegation you can hand authority to someone for whole hours and Revoke it.
  6. Sign in with $AUD (auditor): the page has no Approve buttons.
Then let the agent use it:
Expect {"decision":"allow","step":...}.

4. Scoped halts and kill switches

What this proves: a halt can be narrowed to a tool, an actor or a tenant, only an admin can set it, and lifting it restores service.
Scope by actor from the API (admin only), then by tenant from the CLI. The acme-agent key carries tenant: acme:
In the console: sign in as $ROOT, Evidence, Kill switches. Fill reason and one of tool glob / actor / tenant / environment, click Block new calls; the new row shows its scope and a Lift button. A red Kill switch active card appears on every page only for a global halt (no scope) or the kill-switch file; a scoped halt appears in the table only. An operator sees the table but not the form.

5. Capture, simulation, replay, impact and speculation

What this proves: with capture off the history features say so and nothing breaks; with capture on you can test a policy change against real calls before you publish it.

5.1 With capture off (the default)

Create a candidate policy first (it is used again below), then ask for each history feature:
In the browser (sign in as $DANA or $ROOT): open /console/replay/a-1, type path amount, value 9000, Replay: Nothing is captured for that run (capture must be on, and captures expire). Policy, then Simulate against captured history: Capture is off, so there is no history to simulate against. Impact preview and speculation work without capture, because they only read manifests and policy:
The second command prints the same layout for charge_card: decision now: needs_approval, risk 85, the owner, and rollback: but the charge stays visible on the customer's statement (the residue from its manifest). Compare three plans without running anything:
It also shows a real gap: splitting one payout in two avoids the per-call approval rule. A budget on payouts (like charge-budget) is the way to close it. The console equivalent is Approvals, the impact preview link on a pending row, or /console/impact: type a tool, JSON arguments, Preview. The API equivalents are GET /v1/impact?tool=...&args=... and POST /v1/speculate:

5.2 Turn capture on

Stop terminal A with Ctrl-C, add the setting, start it again with the same command as in 0.3. (Browser sessions end when the server restarts.)
Terminal A
Now make a run to analyse. Only calls made from now on are captured, with secrets redacted and a time limit.

5.3 Simulate a policy against that history

The 500 payout was allowed; under the candidate (over 400 needs approval) it would have needed one. In the browser: Policy, edit the text (change value: 1000 to value: 400), click Simulate against captured history: Last 7 days of captured calls … Newly needing approval. Nothing is published by simulating.

5.4 Replay one run with a changed argument

In the browser: Runs, open c-1: the page links Replay with a change (only when capture is on). Fill Tool payout, Argument path amount, New value 9000, Replay: a table of As recorded against With your change, ending 1 decision would change. Replay needs the operator role. Equivalent API calls: POST /v1/simulate with {"policy":"<yaml text>"} and POST /v1/runs/c-1/replay with {"changes":[{"tool":"payout","path":"amount","value":9000}]}, both with an operator key.

6. Console editors

What this proves: a manifest or policy change can be authored, validated, reviewed and activated in the browser, with the right role at each step. Requirements: manifests must come from a registry folder (created in 0.2) and the policy from a policy versions folder. If you point the server at plain files the console shows them read-only and says so. Roles: operator can save a manifest as a draft; only admin can approve a draft, publish as approved, and publish or activate a policy.

6.1 A manifest with the form builder

  1. Sign in as $DANA (operator). Manifests lists the 9 enforced tools and, in a box, Drafts waiting for approval: export_report.
  2. Click New manifest. Fill the form: Tool name lookup_customer, Description Look up a customer record, Effect read_only, Permission customers:read, Owner support-team, Data class pii, Blast radius low, Timeout 10s. Leave Undo empty. Click Build the YAML.
  3. Below, The YAML shows the manifest and a green Valid. Lint: no findings. Click Validate to re-check (edit the text to see errors, for example set effect: nope).
  4. Add a note and click Save as draft. You land on the manifest page: status draft, v1, with a History table.
  5. Check the agent cannot use it yet:
  1. As operator you cannot approve: there is no Approve the latest draft button. Log out, sign in as $ROOT (admin), open Manifests, click lookup_customer, tick the author may not approve it, click Approve the latest draft. History now has v2 approved, “by dana, approved by root”. The server reloads itself.
  2. Reload the agent side: the same call is now allowed.
Also on that page: Contract tests (a YAML list, Run the tests), Deprecate (with a replacement tool), and Restore on any older version. A manifest’s approval: object (roles, quorum, timeout) and the Approver groups field are kept intact by save, approve, deprecate and restore, and the registry loads with them. This page routes approvals with policy rules instead; see section 3 for the effect of each.

6.2 The policy editor

  1. Sign in as $ROOT. Click Policy. The top line reads 2 rules, 1 limits, 1 budgets, 0 freezes … active v1 and 2 of 2 of the policy’s own tests pass.
  2. Use the Rule builder if you like (it appends a rule to the text), or edit the text directly: change the big-payouts condition value: 1000 to value: 400.
  3. Click Validate and run its tests: Valid. 2 tests pass and a Changes from the active version diff.
  4. Click Simulate against captured history: the 500 payout is Newly needing approval.
  5. Type a note (lower payout threshold to 400) and click Publish and activate (admin). The Versions table now lists v1, v2 and v3; v3 has the active badge.
  6. Click diff on the v3 row (v2 to v3), then Activate on any other row to roll back. The server reloads on every activation.
What this proves: every run and call can be found and explained afterwards, from the CLI, the API and the console.
Runs with an unresolved step (an intent whose outcome was never reported, like most of these test calls) show as needs_attention. actions show lists the approvals behind a call; a-1 was run before capture was on, so it can only say the arguments were not kept, while c-1 shows the redacted copy. The search API needs an auditor key or better:
Both take actor, tool, since, until, text and limit (runs also status, tenant, mode, after; records also run, event, after_seq). Page through with next. In the console: Runs has filters (actor, tenant, tool, needs attention). Open any run for steps, the rollback plan, approvals and the event trail. Click Compare two runs (or open /console/compare?a=a-1&b=c-1): the two step lists sit side by side.

Not covered here

  • Manifest timeout_ms is enforced by the gateway on the upstream call; it was checked by reading the code, not run.
  • Manifest-level approval: roles, quorum and timeout, and the Approver groups field of the form builder, are not exercised by this guide.
  • Delegation expiry in the console (its form takes whole hours); expiry was tested with the CLI.
  • Policy environment rules, default: deny, content: checks, PII handling in capture and the capture time limit.
  • OIDC login, notifications to Slack or email, reversi apply, the Helm chart, and the gateway.