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
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.
runnable
An action entry
Exactly one ofdecision and rules, plus the optional keys.
A control entry
The registrycontrols: 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.
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
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.
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.
runnable
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.
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.
- Decisions: how a rule is matched.
- Get started · Why.