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.
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.