actions: answers.
It is not the only question. “May a €50,000 refund run without a human?” and “may this agent
propose a €50,000 refund at all?” are different, and a system that can only ask the first will
eventually answer the second by accident.
Authority is the second axis. This page is the model in plain language; the contract is
SPEC-v0.3.md §4 and §5.
Opt in, and then it is closed
A document with noauthority: section behaves exactly as v0.2 did. Nothing is evaluated,
no AUTHORITY_* event is written, and no decision changes.
The moment the section exists, every principal needs a grant, and no grant means denied —
including for actions the policy allows outright, including reads, including actions with no
effect key. There is no default: allow, no per-action opt-out, and no flag that makes a
missing grant permissive. Half-configured authority is the failure mode this rule exists to
prevent: it is the state in which nobody can say whether an action was permitted or merely
unlisted.
grants: [] is valid and permits nothing. A missing grants key is a load error, because
inferring “nothing” from an absent key would make a truncated edit look deliberate.
A grant carries no decision
There is nodecision: in a grant, and this is the design rather than an omission.
How much autonomy stripe.refund has is the same for the head of support and for the newest
agent in the fleet: it is a property of the action and of the amount, and it lives in
actions:. What differs between principals is whether they may propose it at all.
The two axes are evaluated separately — authority first — and combine as the stricter of the
pair. Authority cannot make a denied action allowed. Policy cannot make an unauthorized
action permitted. Neither can loosen the other, which is what lets you read either one on its
own and be right about what it does.
And policy still cannot see the principal. agent_eq and every other principal-addressing
condition is refused at load, exactly as in v0.1. Authority is a separate vocabulary, on
purpose.
What a grant matches on
Patterns are deliberately small, because containment between two of them has to be
decidable: a literal, a
prefix* that cannot cross a separator, and a final **. So
stripe.* matches stripe.refund and not stripe.refund.partial. There is no ? and no
character class. Granting the whole surface of a system is spelled ** — one token, greppable
in review, and impossible to write by accident.
The environment is not the caller’s to state. It is set once on the Control and stamped
on every Action, so a grant scoped to ["staging"] cannot be satisfied by a call that
describes itself as staging. An authorization dimension the subject can set is not one.
How much, not just whether
Every key above except one answers may this principal do this? A budget answers a different question, and it is the one a grant could not ask before v0.9: how much, in total?constraints bounds one action. budgets bounds the aggregate. A grant that answers only the
first permits a thousand actions that each pass it, which is how an agent acting entirely within
its permissions empties an account one permitted refund at a time.
Four things worth knowing, because each one is a decision that could have gone the other way.
It is consumed when the effect is reserved, in the same transaction, not checked on one line
and consumed on another. Two processes cannot both pass a check and then both spend.
It is released only when the effect reaches FAILED — the one state in which the executor
proved nothing happened. A committed spend is a spend, permanently.
Ambiguity is not a refund. An AMBIGUOUS effect holds its charge until a human or a
reconcile hook says what happened, because otherwise an agent that can manufacture ambiguity
can manufacture authority. This is the one that surprises people, and it is why
ctrlrun inspect --grant exists: a budget refusing while it looks nowhere near its limit is
almost always one unresolved effect, and the view names it and the command that clears it.
Every grant in the chain is charged. A delegation spends its own budget and its parent’s, so
a holder cannot mint children to spend the same budget over again.
A daily budget smaller than one action the same grant permits is legal and almost always a
mistake: the first action of the day exhausts it, and any rule band above it becomes unreachable.
ctrlrun verify reports that rather than failing.
Delegation, and the rule that makes it safe
A principal holding adelegable grant can create a narrower one at runtime:
delegable: true requires expires_at. Authority that can mint more authority and never
lapses is the one shape this model refuses to write down.
The omission rule
This is the part that surprises people, so it gets its own section. A child that drops a dimension its parent constrains is rejected. Not inherited. Certainly not unconstrained.resources places no resource limit, so the child would authorize payment:US-*, which the
parent never could. Silence widened it.
Most permission systems read an omitted field as “inherit the parent’s”. That is a reasonable
convention and it is not this one, because it makes the safe reading of a document depend on a
document you are not looking at. Here, a delegated grant means exactly what it says, and
saying less is refused rather than resolved. The cost is that every link states every
dimension; the benefit is that a chain can be reviewed one file at a time.
The same rule applies to the subject: a delegation may not carry a wildcard grantee, and may
not drop its parent’s user. Both hand the grant to a wider population than the parent
covered.
Revocation
unrevoke: the operation whose safety matters is the one taken in
a hurry.
Two limits worth knowing before you need them:
Authorityis built when the document is loaded, and v0.3 does not hot-reload. Revocation and expiry are live — they are read from the store and the clock on every evaluation. An edit to the file is not: narrowing a ceiling, bringing an expiry forward or removingdelegabletakes effect when the process next loads the document, which forctrlrun gatewaymeans a restart.- There is no way to list delegations in v0.3, so there is no way to sweep a subtree.
ctrlrun revokeworks one id at a time, and the ids are in the events file. The operation that cuts a chain of unknown width is settingdelegable: falseon the root grant and restarting: every descendant is then denied on the next evaluation.
Reading the evidence
Every action that passes authority appendsAUTHORITY_RESOLVED — not only delegated ones,
because a deployment with a permissive grant has to be distinguishable from one with no
section at all. A denial appends AUTHORITY_DENIED and never POLICY_EVALUATED: policy is
not evaluated, so no approval request is created and no human is left holding a request for an
action that could never run.
The reason is one of a closed set — no_authority, authority_constraint,
authority_expired, authority_escalation, authority_revoked, authority_unreadable — and
never a grant id. A grant may legally be named no_authority, and evidence that can be spoofed
by naming a grant is not evidence. The ids travel in data.grant_id and
data.delegation_id.
Try it
ctrlrun demo, scenario 5 — the chain, the escalation, and the refusal, in process.examples/authority-escalation/— the same story as a standalone script, including the delegation that is refused at creation.examples/authority/— a payments chain and a DevOps chain, as complete documents to read rather than run.