Handing a runbook to an agent safely
Audience: you are integrating this catalog, or any catalog, into something that acts. You may never publish a record here, and this guide is still for you.
Everything below is a requirement on your side. The catalog can describe a procedure accurately and still be handed to an agent that ignores every declaration in it.
Content from a catalog is untrusted input
Not "mostly trustworthy". Untrusted, in the same sense as a web page or a user message.
A runbook is data your agent reads. It is not instructions to your agent, and anything in it that addresses your agent — changing its role, overriding its instructions, telling it to skip a step — is an attack whether or not it was written as one (§12).
We screen for that and publish how (/spec/v1/injection). Screening
is not a guarantee: it matches known shapes, and the list will always trail the attacks.
Records below trust T2 are marked untrusted: true, and the absence of that marker is
not a promise — the trust level is.
Check capabilities before you start, and refuse rather than half-run
capabilities[] is a privilege request. Compare it to your own allowlist before the
first step, and refuse the whole run if you cannot satisfy it.
Refusing up front matters more than it sounds. A run that stops in the middle has already done part of the work, and part of a procedure is a state nobody designed — least of all a destructive procedure, where the half that ran is the half that broke something.
Scope permissions to the current step
Granting the union of a procedure's capabilities for the whole run is how a diagnostic session ends in a deletion.
std/postgres-drop-stale-replication-slot needs cli:psql at every step, including the
one that drops a replication slot. If you grant it for the run, the checking steps hold
the dropping step's rights. Scope to the step (§13.2), and
the destructive capability exists only while the destructive step is current — behind its
gate.
An approval is a human who is not the agent
requires_approval means a decision from someone other than the thing being supervised.
An agent approving its own destructive step is the failure this entire specification
exists to prevent, so make it impossible in your integration rather than discouraged.
Two details that are easy to get wrong:
- A timeout aborts. Proceed-on-timeout is the convenient default somebody eventually adds, and it turns every gate into a delay. An unanswered question is not a yes.
- An approval covers one attempt. If a retry brings the run back to the same step, that is a new attempt and needs a new decision.
Your adapter decides what you can enforce
A profile is bounded by what you can observe, not by what you claim (§13.3).
If you supervise by proxying MCP calls, you cannot see shell commands — an agent with a
Bash tool steps around you without breaking a single rule you enforce. That is why a
runbook containing destructive or irreversible steps must not run under an
adapter that cannot observe every tool invocation, however honestly you have implemented
the rest.
What supervision does not give you
Stated plainly, because a contract that overstates its guarantee is worse than none:
- It is not a sandbox. It authorizes calls; it does not contain their effects.
- It does not replace your own allowlist. Yours is the outer boundary.
- It does not make an agent safe. It bounds what one runbook authorizes. An agent with access obtained elsewhere still has it.
If you would rather not build this at all
Everything above is what you owe if you are handing a runbook to an agent you operate.
You may not need to. npx @runbooks/cli run <ref> --as <your-name> runs the procedure on
your own machine under a supervisor that is doing the invoking: it proposes each step
before it happens, stops at an approval gate and waits for you, takes the failure route
the procedure names rather than carrying on, and spends the retry budget the record
declares. Nothing about the run leaves the machine.
That is also the honest answer to the section above: an adapter beside somebody else's
agent is bounded by what that agent routes through it, and one that is the agent is not.
runbooks hook remains for the case where the agent does the work and you want a
supervisor in its tool-call path.
If you build nothing else
Pin the version. std/kafka-consumer-group-stuck-restart@1.0.0 is reproducible;
std/kafka-consumer-group-stuck-restart is whatever we published this morning. Verify
the content_hash when you fetch it, and check upstream_state before you run: a
procedure whose source has moved may no longer match the system it describes.