Execution contract, v1
Normative. Derived from RUNBOOK.md §13; where the two differ, RUNBOOK.md wins.
Implemented by @runbooks/supervise; every MUST here has a test in
packages/supervise/src/contract.test.ts.
P0/P1/P2 say how much structure a document has. R0/R1/R2 say how much of this contract a client enforces. They are independent axes, and the catalog should be able to say so: a perfect P2 document read by an R0 client is unsupervised.
Profiles
| Profile | The client… | Suitable for |
|---|---|---|
| R0 — advisory | reads the runbook and follows it. Nothing enforced; the model's judgement is the only control. | human-only, human-with-agent on read-only work |
| R1 — enforcing | enforces the capability allowlist and per-step scope, halts at approval gates, honours a risk ceiling, enforces retry budgets, blocks deviations. | the minimum for agent-autonomous |
| R2 — supervised | R1, plus: evaluates assert and routes on_fail itself, and emits a run report. |
anything destructive or irreversible |
What a client MUST do
Claiming any profile:
- Check
capabilities[]against its own allowlist before starting, and refuse the run rather than partially executing it. A run that stops halfway has already done some of the work, and "some of a destructive procedure" is the worst available outcome — so every reason to refuse is collected up front, not discovered in sequence. - Treat every step body below trust T2 as untrusted input, never as instructions to itself.
- Honour
requires_approvalwith a decision from a human who is not the agent. - Refuse a document whose
min_runtime_profileexceeds what it enforces. - Stop at a terminal, and report a run that ended anywhere else as incomplete.
Claiming R1 or above, additionally:
- Scope permissions to the current step, not to the union of the runbook.
- Enforce
retrybudgets and abort when one is exhausted. - Block any invocation the current step does not declare, and record it as a deviation rather than dropping it silently.
Claiming R2, additionally:
- Emit a run report conforming to §14, containing no free text.
- Evaluate
assert, notexpect. See below.
Coverage bounds the profile
A profile is bounded by what the adapter can observe, not by what the client claims.
| Adapter | Sees | Therefore |
|---|---|---|
| MCP proxy | MCP tool calls only | blind to shell access: an agent can step around it without violating a single proxy rule |
| tool-call hook | every invocation, shell included | complete for that client |
| library in the client's own loop | whatever the client routes through it | complete only if the client routes everything |
A runbook containing risk: destructive|irreversible MUST NOT run under an adapter
that cannot observe every tool invocation — however loudly the client claims R1 or R2.
Every adapter publishes what it cannot see, and that published statement is what this
rule is evaluated against.
min_runtime_profile
A runbook may declare the lowest profile permitted to run it. When absent, the implied
minimum is: R2 if the procedure contains destructive or irreversible work, R1 if it
declares agent-autonomous execution, R0 otherwise.
agent-autonomous at R0 is an error, not a warning: an autonomous agent under an
advisory client has nothing between it and the tools but its own judgement, which is the
arrangement this specification exists to replace.
Postconditions: assert, never expect
expect is prose for the reader. assert is a closed set of predicates over the
observation, and it is what an R2 supervisor evaluates.
A step with only prose is unevaluable — a first-class outcome, distinct from pass and from fail. The supervisor halts and asks a human rather than guessing. A supervisor that treats "I could not tell" as "passed" is worse than one that does not check at all.
There is deliberately no model evaluator. Putting one in would place a model back inside
the control loop the supervisor exists to be outside of, and "the supervisor verified the
check" would come to mean "a model thought it looked right". Reasoning:
docs/decisions/Q12-expect-evaluability.md.
The policy envelope
What a runbook authorizes at run time, derived from the document and never authored separately. A policy that can drift from the procedure it governs is worse than no policy, because it is trusted while being wrong — so an author who writes a valid P1 runbook has already written the policy, and there is no second artifact to keep in sync.
| Document element | Envelope field | Runtime meaning |
|---|---|---|
capabilities[] |
allowlist |
nothing outside it may be invoked for the duration of the run |
a step's tool |
steps[id].tools |
the scope — this step alone, never the union |
risk + requires_approval |
steps[id].gated |
the capability does not exist until a human decides |
retry |
budgets["from->to"] |
counted, and finite |
| terminals | terminals |
where a run may end |
| anything else | — | a deviation |
Two invariants hold by construction, not by review:
- The envelope never grants what the document does not declare. A step naming a tool
absent from
capabilities[]has it recorded underundeclaredand not placed in scope. The linter fails the record; the supervisor does not quietly honour it. - A destructive capability is never in scope before its gate. It exists only while the destructive step is current and its approval has been given — not before, and not after control moves on.
Serialized as runbook-policy-envelope/v1, so a client can be handed the policy rather
than re-deriving it. Derivation is deterministic: two implementations produce the same
envelope for the same document, or the contract is not portable and "any client can
enforce this" means "our client can".
Deviations
| Class | Meaning |
|---|---|
undeclared-tool |
outside the runbook's allowlist entirely |
out-of-scope-tool |
declared by the runbook, not by the current step |
out-of-order-step |
attributed to a step this runbook does not contain |
budget-exhausted |
a retry limit ran out |
terminal-not-reached |
the run ended somewhere that is not a terminal |
undeclared and out-of-scope are reported separately on purpose. They look alike and
mean different things: the first is a runbook asking for a privilege it never declared,
the second is an agent reaching for a privilege at the wrong moment. Collapsing them
would hide which one happened, and only one of them is a content problem.
What supervision does not provide
Published as part of the contract rather than as a caveat, because a contract that overstates its guarantee is worse than none.
- Not a sandbox. It authorizes tool calls; it does not contain their effects.
- Not a general agent policy engine. Its jurisdiction is one run of one runbook.
- Not a replacement for the client's own allowlist. That
MUSTis unconditional; the client's allowlist is the outer boundary and the supervisor is the inner one. - Not a service anyone operates for you. It runs in the operator's environment, with their tools and their credentials.
- Not a guarantee. It bounds what the runbook authorizes. An agent with out-of-band access to a system still has it. That is a real reduction and not a total one.