ctrlrun 0.5.0
v0.4 asked does it hold in my setup? v0.5 asks a narrower and harder question: can somebody
else implement this? The adapter contract is one of the six things v1.0 freezes, so it is
written to be lived with rather than revised once somebody tries it.
The milestone's own answer is yes, with caveats. A session that could read the five
specifications and nothing else — no kernel source, no reference adapter, no test — wrote a third
adapter against the contract and returned fourteen questions it could not answer. Every one
became an edit, and one of them was a defect in a shipping adapter that no test caught. That
exercise, not the two adapters, is what v0.5 is for.
Added
-
Two reference adapters, on their own version line and in their own distributions:
ctrlrun-langgraph(adapters-langgraph-1.0) reusesinterrupt(),Command(resume=...)and
the checkpointer;ctrlrun-openai-agents(adapters-openai-agents-1.0) reuses the SDK's
tool-approval interruption. Neither is in thectrlrunwheel or sdist (T136). LangGraph
passes the conformance kit 6/6; the Agents SDK 4/4 with two suitesnot_applicable, each with
its reason on the report and in the README.Prevention or attribution, in that word, is the sentence each README leads with.
LangGraph's resumption carries the arguments a human answered against and core re-checks them;
the Agents SDK records that a call was approved and not what its arguments were. -
ctrlrun.adapter— the surface:FrameworkInterrupt,PendingApproval,
ApprovalAnswer,InterruptApprovalProvider,needs_approval,banner, and
Control.resolve_principalpromoted from private. Core, stdlib, in the action path. An
adapter returns an answer and one core provider writes the grant, through the same calls
ctrlrun approvemakes — which is what makes "never a second approval path" structural. -
ctrlrun.conformance— this repository's ownv0.1 §7andv0.3 §10acceptance tests,
runnable against any adapter, in core. It is not a certification and passing it is not a
claim about quality: it answers one question, does an action driven through this adapter get
the same refusals as one driven through@protect? Fifteen deliberately broken fixtures were
written first, and each fails the suite named for it and no other. -
docs/adapters.md, and a README section that opens by saying when you do not need an
adapter (T139) —@protectcovers anything in this process and the gateway anything over MCP. -
The framework probe was run against LangGraph 1.2.11 and openai-agents 0.22.0, five
repetitions each, and the results are published. Read theapproval-mutationcolumn carefully:
executed_oncethere does not mean the scenario went well.
Changed
ctrlrun demo --helpsaid four scenarios and ran five, stale since v0.3 added the
authority escalation.
Security
Three independent reviews and item 6 found five authorization defects in
ctrlrun-openai-agents before it shipped. All are fixed, mutation-tested, and recorded here
because the pattern matters more than any one of them: the SDK's approval record is keyed to a
tool call, and a CTRLRun grant binds to an action hash, so every defect was the same shape —
reading a coarser answer as though it answered a finer question.
interrupt()returnedgranted=Trueunconditionally.- It then accepted a sticky per-tool decision, so
always_approve=Trueanswered for later
calls no human saw. - It then answered for every action raised under one tool call: a human approving a $5 refund
authorized a $1,000,000 wire raised beside it, with a receipt naming the channel as approver. unwrap()returned the firstCTRLRunErroranywhere in the chain, so a nestedNotExecuted
masked an AMBIGUOUS refund — safe to retry reported for an effect that may have landed.- Observe mode interrupted and blocked the action. Found by item 6 without reading the
adapter. §3.6's rule followed for one framework shape and had to be required of the other;
a deployment evaluating CTRLRun in the mode built for evaluating it would have had its agent
halted.
Added
-
docs/SPEC-v0.5.md— the v0.5 contract, a delta over v0.1, v0.2, v0.3 and v0.4. It fixes
the adapter surface (§2), the approval round trip (§3), theSPEC-v0.3.md§4.3.1 rows an
adapter adds (§4), the conformance kit (§5), packaging and versioning (§6), what an adapter
must document (§7), the acceptance tests T126–T139 (§8) and the public names v1.0 will freeze
(§9). No implementation lands with it.Five decisions are written into it with their arguments, because a decision whose
reasoning lives only in a build note is one the next session re-litigates:
ApprovalRequired+with_approvalrather thanSuspended+Control.resume; a Protocol
rather than a base class; the conformance kit in this repository rather than a third distribution;
one repository with separate distributions; and an adapter that sees the principal and
never supplies one.A sixth was open and is settled in §3.6: an adapter never interrupts in observe mode,
never prints — it is inside somebody else's loop and may have nowhere to print — and
logs theSPEC-v0.3.md§6.5 banner once perControl, whichctrlrun.adapter.banner
does so no two adapters word it differently. The conformance kit refuses an observing
Control— a refused report with no suites, which is not a success — rather than producing
an all-not_applicablereport with a zero denominator, because0/0reported as a pass is
the false greenSPEC-v0.4.md§3.8 refuses by name.An independent review in a session that did not write the document found five defects that
would each have produced an insecure or unimplementable adapter, and §9.1 records them. The
worst was--principal-from-client-info's third costume: §3.5 told an adapter to answer its
framework's pre-invocation predicate withControl.evaluate(action), andAction.principal
has no default — so the only way to obey was to build a principal from the framework's
session.ctrlrun.adapter.needs_approvalis core's because of that, and
Control.resolve_principalis promoted from private for it: the identity seam an adapter may
read and may not supply. -
ctrlrun.adapter— the adapter surface: theFrameworkInterruptProtocol,
PendingApproval,ApprovalAnswer,InterruptApprovalProvider,needs_approvaland
banner. Core and stdlib, re-exported fromctrlrun, andControl.resolve_principalis
promoted from private for it — the identity seam an adapter may read and may not supply. -
ctrlrun.conformance— the adapter conformance kit: theSPEC-v0.1.md§7 and
SPEC-v0.3.md§10 acceptance suites runnable against any adapter through that surface, and
eleven adapters broken in one named way each, written before the reference adapters because
"two adapters pass the suites" means nothing until the suite can fail.It is core and stdlib-only, not the extra
SPEC-v0.5.md§5.1 originally specified. The
premise there was that a kit needspytest; building it showed otherwise, and an extra with
no dependency behind it is an install line that installs nothing. §12.1 records the change.
dependenciesis unchanged:pyyamlandclick.
Changed
-
docs/ROADMAP.md's v0.5 bullet was wrong and is corrected here, not silently. It said
the reference adapters map their frameworks' interrupts ontoSuspended/Control.resume,
"which v0.2 already ships for exactly this shape". It does not:Suspendedexists for the
remote asking a question mid-execution, where the reservation is already taken and must
stay taken, and an approval gate has none to hold — v0.1 consumes the approval in the same
transaction as the reservation, so a human deliberating for an hour pins nothing.
SPEC-v0.5.md§3.1 argues it in full. This is the treatmentSPEC-v0.4.md§9.4 gave the
threat model's sentence about a check verify could not deliver. -
Version is
0.5.0.dev0.