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:

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:

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.