Skip to main content
A policy file adds rules to what manifests and scopes already enforce. It can deny a call, require approval for it, or (with default: deny) turn the rules into an allowlist. It can never grant a permission the run’s scope lacks.
policy.yaml
The top-level keys are version, default, rules, limits, budgets, freezes, risk, retention, content and tests. Anything else is an error. Rule and limit ids must be unique.

Matching

match fields (all optional, all must hold): tool, permission, actor, environment, tenant, data_class, blast_radius (globs: * any run of characters, ? one; each may be a list), and effect (an exact effect class or a list). data_class and blast_radius come from the tool’s manifest risk; a call to a tool without them does not match a rule that names them. when conditions then look at the call arguments with $args.path.to.value: eq, ne, gt, gte, lt, lte, in, not_in, matches (regular expression), exists, contains. Numbers and numeric strings both count as numbers, so an upstream that coerces "5000" cannot slip under gt 1000. A comparison on a missing value is false. For deny and require_approval rules a value that is present but of the wrong type for the operator (text under gt, a number under matches) counts as a match, so a restriction cannot be dodged by sending the wrong type. allow rules do not do this.

Tenants

tenant matches the tenant of the run. The server sets it from the tenant on the API key that opens the run. Runs created by the gateway carry no tenant, so a rule with tenant: never matches there.

Precedence

deny beats require_approval beats allow. Every matching rule is evaluated and reported. With default: deny, a call that no allow rule matches is denied (no allow rule matches this call (default deny)).

Limits

Sliding windows counted per run, actor, tenant or globally. Only calls that actually started count: a call that was blocked or is waiting for approval does not use up the window. Counters live in the data store, so every process sharing it shares the counts.

Budgets

A limit counts calls. A budget can also add up an argument:
A call that would take the total above max is denied or needs approval, and the reason says how much was used. Between warn_at and max the call goes through and the ledger notes budget daily-charge-total at 85% (...). A call whose argument is not a number adds 0.

Time windows

A rule applies only inside its window:
days is any of sun..sat (default every day), between is HH:MM-HH:MM and may wrap midnight (22:00-06:00), timezone is an IANA name (default UTC), and outside: true inverts the window. Checked with a fixed clock, the first rule denies on a Saturday and allows on a Friday; the second asks for approval at 19:00 New York time and not at 11:00.

Freezes

During the period a matching call is denied: freeze quarter-end: quarter-end freeze (until 2027-01-02T00:00:00Z). Without match, every call that is not read_only is frozen, which includes tools with no manifest. to must be after from.

Risk scoring

A transparent 0 to 100 score from the call’s effect, the manifest’s blast_radius and data_class, the environment, an argument and the actor. Every factor is listed, so a reviewer sees why a call scored what it did. Add a risk section to turn it on:
At or above approve the call needs approval; at or above deny it is refused. approve cannot exceed deny. The score is clamped to 0..100 and recorded with the step and in the intent record: risk 60 (effect compensable +35, amount over 100000 +25). A $args.amount call of 200000 by new-agent in prod scores effect compensable +35, environment prod +10, amount over 100000 +25, actor new-agent +20 = 90, which is denied. Default weights, used for anything you do not override:
A map you give under weights replaces the default map for that key. If you set only effect: { compensable: 35 }, irreversible scores 0. List every class you care about, as above.

Approvers and quorum

A require_approval rule can say who may approve and how many must:
approvers, quorum and approval_timeout are only valid on require_approval rules. When several rules and the manifest’s own approval settings apply to one call, they are merged: any listed role may approve, the largest quorum wins, the shortest timeout wins. See approvals.

Retention and content

Two more sections live in the same file.
retention says how long run state, approval requests and captured arguments are kept; a run is kept for the longest of the default, the matching rules and each tool’s manifest audit.retention_days. The ledger itself is never purged. See retention. content configures the checks in content checks (secrets, encoded_payloads, injection, pii: flag, require_approval or deny; plus cumulative and max_string_bytes).

Modes

Policy decisions are the same in every mode; the mode decides what happens to a violation. The gateway sets the mode with --mode or mode: in its config. Use observe or advise first, read the would_block records or reversi report (with no run id, the period report), then switch to enforce. The gateway turns a block into an MCP error result the agent can read.

More than one policy source

The engine merges the policy file with the gateway’s allow and deny lists, which it compiles into rules (gateway-deny-0, gateway-allow). Rules, limits, budgets, freezes and tests are concatenated, so ids must stay unique (a clash is an error). If any source says default: deny, the result is deny: a gateway allow list therefore makes the merged policy an allowlist. risk, retention and content come from the first source that has one, which is the policy file.

Test it

policy test runs the inline tests: and any extra file of cases, and exits 1 on a failure. Put it in CI. A case may name the rule that must have fired, which also covers limits, budgets and freezes by id. input takes tool, effect, permission, actor, environment, tenant, data_class, blast_radius and args; the defaults are actor agent, environment dev and empty args. Each case starts with fresh counters, so limits and budgets are tested on a single call, and windows and freezes use the real clock. An extra file is a list of the same cases:
more-cases.yaml

Versioned policies

Point policy: at a folder and each change becomes a version with an author, a note, a checksum and a record of how many of its own tests passed. Publishing validates the policy and runs its tests; a policy whose tests fail is refused.
--dir defaults to policy: in reversi.config.yaml. The engine loads the active version, and a running engine picks up a new one on reload (POST /v1/reload; the server and gateway also watch the files unless reload: { watch: false } is set). Each version’s file is checked against its checksum when read, so a policy edited outside the history is refused. Over HTTP an admin can POST /v1/registry/policy (text, note, activate) and POST /v1/registry/policy/{version}/activate. The console policy page validates, runs the tests and can simulate the draft against captured history before you publish.