> ## Documentation Index
> Fetch the complete documentation index at: https://ctrlrun-release-0-10-0.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Policy YAML reference

> Every key of ctrlrun.yaml, from ctrlrun.policy/v1 to v6: actions, decision, rules, max_attempts, approver_role, approvals_required and the when operators.

`ctrlrun.yaml` is one document: a `schema`, an `actions` map, and from `ctrlrun.policy/v3` the
`mode`, `environment` and `authority` keys, and from `v4` `version` and `controls`. Every key
set is closed: an unknown key anywhere is a load error, never ignored, because a key an older
reader silently dropped would be a typo that never surfaced. A test asserts this page names
every key and operator the loader accepts.

## Top level

| Key           | Type                                        | Since | Default when omitted                                                                        | Notes                                                                                                                                                                                                |
| ------------- | ------------------------------------------- | ----- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schema`      | string                                      | v1    | **error**: a document with no schema is an unknown schema, never "assume v1"                | one of `ctrlrun.policy/v1`, `ctrlrun.policy/v2`, `ctrlrun.policy/v3`, `ctrlrun.policy/v4`, `ctrlrun.policy/v5`; each accepts the keys below marked with its version or earlier, and refuses the rest |
| `actions`     | map of action name → entry                  | v1    | **error**                                                                                   | the whole list of what an agent may do; an action not in it is denied                                                                                                                                |
| `mode`        | `enforce` or `observe`                      | v3    | `enforce`                                                                                   | top level only; refused anywhere else                                                                                                                                                                |
| `environment` | string                                      | v3    | `production`, unless `$CTRLRUN_ENVIRONMENT` or the gateway's `--environment` says otherwise | part of every action's hash and of authority matching; never read off the wire                                                                                                                       |
| `authority`   | section                                     | v3    | none: the document behaves as v2, with no authority check                                   | the moment it exists, every principal needs a grant. See the [authority reference](/docs/reference/authority-yaml)                                                                                   |
| `version`     | string                                      | v4    | none recorded                                                                               | your label for this document, carried on every receipt as `policy_version`; never authoritative, the hash is                                                                                         |
| `controls`    | map of id → entry with `title` and `source` | v4    | empty                                                                                       | the registry an action or a rule may cite; `source` is cited on receipts and never interpreted; a cited id the registry does not define is a load error                                              |

A file that cannot be read, does not parse, or fails any check above raises `PolicyError`, and
no `Control` is built. There is no empty permissive policy.

```yaml runnable theme={null}
schema: ctrlrun.policy/v4
version: "2026-09-refunds-3"
mode: enforce
environment: production

controls:
  maker-checker-refunds:
    title: "A refund over the desk limit is approved by a second person"
    source: "House policy FIN-4.2"

actions:
  stripe.refund:
    effect: "refund:{payment_id}"
    resource: "payment:{payment_id}"
    controls: [maker-checker-refunds]
    rules:
      - when: { amount_gte: 0, amount_lte: 50000 }
        decision: allow
      - when: { amount_gte: 0, amount_lte: 500000 }
        decision: approve
      - decision: deny
```

## An action entry

Exactly one of `decision` and `rules`, plus the optional keys.

| Key                  | Type                                                                                                                                                                                                                                                                                  | Since | When omitted                                                                                                      | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `decision`           | `allow`, `approve` or `deny`                                                                                                                                                                                                                                                          | v1    | required unless `rules` is present                                                                                | the same answer for every argument                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `rules`              | non-empty list of rules                                                                                                                                                                                                                                                               | v1    | required unless `decision` is present                                                                             | tried in order; the first match wins; **no match is `deny`**                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `effect`             | template                                                                                                                                                                                                                                                                              | v2    | no reservation: the action runs unguarded against duplicates, and the gateway names it on the line that starts it | `"refund:{payment_id}"`; every placeholder must resolve from the arguments or the action is refused before it runs                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `resource`           | template                                                                                                                                                                                                                                                                              | v2    | no resource in the hash, and a grant's `resources:` cannot match                                                  | `"payment:{payment_id}"`; the decorator's `resource=` wins where both exist                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `mcp`                | mapping with one key, `not_executed_on_error` (bool)                                                                                                                                                                                                                                  | v2    | `false`                                                                                                           | gateway only: whether an upstream's in-band error means the tool did nothing (`FAILED`) or nobody knows (`AMBIGUOUS`). Say `true` only for an upstream that rejects before acting                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `upstream`           | mapping with up to three keys: `tls_cert_sha256` (list of `sha256:…` strings, the upstream's leaf certificate in DER form), `tls_cert_file` (path to a PEM file, one or more certificates), `tool_schema_sha256` (one `sha256:…` string, the tool's `tools/list` entry canonicalised) | v8    | no pin: the action is authorised against whatever answers at the upstream's name                                  | gateway only. Checked in three places for one rule: at startup, where a mismatch refuses to start and prints the observed hash beside the pinned one; at decision time, where the action is denied `upstream_mismatch` when what this process last observed differs from the pin and `upstream_unverified` when nothing has been observed yet, so a pin never switches itself off by being unobserved; and at the handshake, where `tls_cert_file`'s certificates are the connection's only trust anchors and a swapped server fails before the first request byte. A digest-only pin gets the first two and not the third, and the startup line says so. The in-process decorator and the ACS hook hold no connection and cannot pin: the hook refuses at construction, the decorator denies `upstream_unverified` |
| `controls`           | list of ids                                                                                                                                                                                                                                                                           | v4    | none                                                                                                              | cited on every receipt for this action, in registry order                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `data`               | map of argument name → label, either a bare string or a mapping with the one key `label`                                                                                                                                                                                              | v4    | no labels; `data_scope` is empty                                                                                  | labels the arguments carry, for the `data_scope` condition below                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `approvals_required` | integer, at least 1                                                                                                                                                                                                                                                                   | v6    | one approval, exactly as before v6                                                                                | how many **distinct verified principals** must answer before the approval is consumable. A second yes from a principal that already answered is recorded, moves that entry's timestamp, and does not move the count. The count is decided inside the store's own write on all three backends, never by a read followed by a write. Above `1` in a deployment that configures no approver identity is a **denial**, not a silent downgrade: the kernel cannot tell two anonymous yeses apart. `ctrlrun approve` records no verified approver, so it never counts toward one, and says so when it is used. Inside the policy hash                                                                                                                                                                                     |
| `max_attempts`       | integer, at least 1                                                                                                                                                                                                                                                                   | v5    | no ceiling: a renewal after `FAILED` is unbounded, exactly as before v5                                           | the most **attempts** that may execute on one effect key, the first included: `3` is the first attempt and two renewals. Above it the executor is not called, the record is released as `FAILED`, a `blocked` receipt names the ceiling, and `ActionDenied(reason="attempt_ceiling")` is raised; the refused number is spent. `0`, a negative, a bool, a float, a string and a mapping are each a load error naming the key, the action and the line. An attempt is not an executor invocation: a suspended executor's resumes all run on one attempt. Inside the policy hash                                                                                                                                                                                                                                       |

## A control entry

The registry `controls:` at the top level. v4 gave a control a `title` and a `source`, which name
a requirement and decide nothing. v6 gives it one key that decides something.

| Key             | Type             | Since | When omitted                  | Notes                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| --------------- | ---------------- | ----- | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title`         | string           | v4    | required                      | what the control requires, in a sentence                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `source`        | string           | v4    | none                          | where the requirement comes from. CTRLRun does not interpret it                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `approver_role` | non-empty string | v6    | **this control gates nobody** | the role a principal must hold for their approval of an action citing this control to be consumable. Matched **byte for byte** against the claim the deployment names: no case folding, no trimming, no prefix matching and no pattern grammar, because a wildcard in a role would be an entitlement nobody wrote. Leading or trailing whitespace is a load error rather than a role that matches nothing for ever. Where an action cites several controls, **every** required role must be held, since any-of would let the weakest control in a set decide who may answer. Inside the policy hash |

**Omission is not entitlement, and it is not refusal either.** A control with no `approver_role`
gates nobody; a principal whose claims lack the role is not entitled. The two look alike in a diff
and mean opposite things: the reading that merges them either refuses every approval in a
deployment with one unroled control, or admits every approver in a deployment with one unclaimed
principal.

**What the kernel refuses is an approval whose *recorded* entitlement does not cover the role.**
What entitled it was decided where the credential was verified — the operator MCP server, or an
embedding application. CTRLRun does not interpret the role, does not check that such a role exists
anywhere, and claims nothing about a standard or an audit on the strength of one.

## A rule

| Key        | Type                         | When omitted                               | Notes                                                           |
| ---------- | ---------------------------- | ------------------------------------------ | --------------------------------------------------------------- |
| `when`     | map of condition → operand   | the rule matches every action; put it last | all conditions must hold                                        |
| `decision` | `allow`, `approve` or `deny` | **error**                                  |                                                                 |
| `controls` | list of ids                  | none                                       | added to the action's own on the receipt when this rule matches |

## Conditions

A condition key is `<subject>_<operator>`. The subject is an argument name, or the derived
`data_scope`; the operator is one of seven, and there is no other syntax. Amounts and every
other numeric operand are integers: `float` is refused everywhere in CTRLRun.

| Operator | Meaning            | Example                      |
| -------- | ------------------ | ---------------------------- |
| `eq`     | equal              | `role_eq: admin`             |
| `neq`    | not equal          | `to_domain_neq: example.com` |
| `in`     | member of the list | `role_in: [reader, viewer]`  |
| `lt`     | less than          | `amount_lt: 100`             |
| `lte`    | at most            | `amount_lte: 50000`          |
| `gt`     | greater than       | `amount_gt: 0`               |
| `gte`    | at least           | `amount_gte: 0`              |

Bound both ends of a numeric band. `amount_lte: 50000` alone matches a negative amount, and a
refund of a negative amount is a charge.

**Reserved subjects.** Ten names are refused as condition subjects at load, in a document of
every schema version. Five are the principal — `agent`, `user`, `claims`, `issuer`,
`expires_at` — because the policy cannot see who is asking. Five more name the action itself
rather than an argument of it: `action_id`, `environment`, `principal`, `resource` and
`data_scope`. So `when: { resource_eq: "payment:1" }` is a load error, not a rule that never
matches. `data_scope` is the one derived subject: it is the set of
labels the supplied arguments carry, `data_scope_in: [phi]` means the set intersects the list,
and `data_scope_eq` / `data_scope_neq` compare the whole set. `data_scope` may not be an
argument's name, an effect placeholder, or a protected function's parameter.

```yaml runnable theme={null}
schema: ctrlrun.policy/v4

actions:
  patient.record.update:
    effect: "record:{patient_id}"
    data:
      diagnosis: phi
      patient_id: phi
      note: internal
    rules:
      - when: { data_scope_in: [phi] }
        decision: approve
      - decision: allow
```

## What a version adds

**Declare the lowest version that has every key you use.** `v2` for `effect:`, `v3` for `mode:` or
`authority:`, `v4` for `controls` or `data`. A newer number changes nothing until one of its keys
appears in the file, so a `ctrlrun.policy/v2` document on this site is not behind; it uses no key
that `v3` or `v4` added. What a version buys is the refusal in the third column: the key an older
reader would have silently misread is one it refuses instead.

| Schema              | Adds                                                                                                                | An older reader would                                                                                                                                              |
| ------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `ctrlrun.policy/v1` | `schema`, `actions`, `decision`, `rules`, `when`                                                                    |                                                                                                                                                                    |
| `ctrlrun.policy/v2` | `effect`, `resource`, `mcp` on an entry                                                                             | run a gateway tool with no reservation                                                                                                                             |
| `ctrlrun.policy/v3` | `mode`, `environment`, `authority` at the top                                                                       | enforce an observed deployment, or run with no authority check                                                                                                     |
| `ctrlrun.policy/v4` | `version`, `controls` at the top; `controls`, `data` on an entry and a rule                                         | cite ids nothing defines, or evaluate a `data_scope` rule against nothing                                                                                          |
| `ctrlrun.policy/v5` | `max_attempts` on an entry                                                                                          | bound renewal after `FAILED` on one effect key                                                                                                                     |
| `ctrlrun.policy/v6` | `approver_role` on a control entry; `approvals_required` on an action entry; `break_glass` in the authority section | gate nobody while reporting a deployment as checking entitlement, consume on the first grant where several were required, or read an envelope as an ordinary grant |
| `ctrlrun.policy/v7` | `tasks`, `budgets` on a grant in the authority section                                                              | authorise a task the grant does not cover, or spend with no ceiling                                                                                                |
| `ctrlrun.policy/v8` | `upstream` on an entry                                                                                              | authorise the action against any server at all, which is the whole of what the key restricts                                                                       |

That is why the key sets are closed by version: each key is refused by every reader that would
misread it.

## Next

* [Authority YAML reference](/docs/reference/authority-yaml).
* [Decisions](/docs/concepts/decisions): how a rule is matched.
* [Get started](/docs/get-started/quickstart) · [Why](/docs/why).
