Repository navigation
v0.2.0 — Zero-friction deployment
An existing MCP server plus one gateway gets CTRLRun semantics with no agent changes.
pip install "ctrlrun[gateway]"
ctrlrun gateway --upstream http://localhost:8000/mcp --alias acme --principal refund-agentThe reservation, the approval binding and the FAILED/AMBIGUOUS asymmetry hold over a network hop exactly as they do in-process: a lost response blocks the retry, an approval binds to the exact action, and a timeout is never mistaken for a failure.
pip install ctrlrun still installs nothing but pyyaml and click.
Everything below ships. pip install ctrlrun still installs nothing but pyyaml and
click; the gateway, the ACS hook and the OpenTelemetry sink live in extras.
Added
- MCP gateway —
ctrlrun gateway --upstream <url> --alias <name>, inctrlrun[gateway].
An existing MCP tool server gets CTRLRun semantics with no agent changes:tools/call
becomes an Action, everything else is relayed unchanged. The request forwarded upstream is
built from the action's canonical arguments, so what was hashed, reserved and recorded is
byte-for-byte what the tool receives. Serves2026-07-28and2025-03-26–2025-11-25in
passthrough. A fresh connection per intercepted call, because "the connection was never
established" is the only observation that proves non-execution. - Reconciliation hook —
@protect(..., reconcile=...). The second authority permitted to
move a record out ofAMBIGUOUS, and only where its answer points. An exception, a nonsense
return value and a hook that is never called all mean"unknown", which changes nothing. SuspendedandControl.resume— an executor may say "the remote asked for something
before it will finish". The record staysEXECUTING, the lease is extended, the
continuation is held, no receipt is written, and the signal reaches the caller. Built for
MCP elicitation; used by the ACS adapter for the same reason.EventSink— a protocol receiving everyEventandReceipt, called after the
authoritative store write.JSONLEventSinkis the v0.1 file writer under that interface.
A sink that raises is logged and skipped; it can never change a decision or an outcome.OTelEventSink— inctrlrun[otel]. One OpenTelemetry span per action, one span event
per step,ctrlrun.*attributes. Argument values are withheld unless asked for.WebhookApprovalProvider— core, over stdliburllib.request. One signed POST on
APPROVAL_REQUESTED; the gateway serves the signed inbound grant/deny at
POST /ctrlrun/approvals/<request_id>. An undelivered notification is not an approval.ctrlrun inspect <action_id>— one action's whole history: proposal, decision,
approvals, effect, receipt and the event timeline.--jsonemitsctrlrun.inspection/v1.- Policy
schema: ctrlrun.policy/v2— per-actioneffect:,resource:andmcp:
templates, needed because a gateway call has no decorator to carry them. Where a decorator
and the policy disagree, the decorator wins and the mismatch is warned about once. - ACS control hook —
ctrlrun.acs.AcsControlHook, inctrlrun[gateway]. Answers the
OWASP Agent Control Standard'ssteps/toolCallRequestandsteps/toolCallResult. See
docs/ACS.mdfor the mapping and for the four places ACS is silent. No compliance
claim: at the commit read there is no ACS reference implementation and no conformance
suite, so there is nothing to be conformant with. examples/— four standalone failure scenarios, an ACS integration example, and nine
sector policy templates underexamples/policies/.
Changed
StateStore.append_eventreturns the event as stored, where it returnedNone. Sinks must
be called with theevent_idthe store assigned, and the store is the only thing that knows
it. Callers that ignore the return value are unaffected. Recorded inSPEC-v0.1.md§8.SQLiteStateStoreno longer writes JSONL.Controldoes, throughJSONLEventSink, and
Control.from_file()installs one by default — so the two files land exactly where v0.1 put
them and existing evidence directories are unchanged.docs/SPEC-v0.2.md§9 amended: it forbade an ACS adapter on the reading that ACS had no
stable interface. The v0.1.0 schemas say otherwise, so the adapter ships. The no-claim rule
is untouched.
Deprecated
ctrlrun gateway --principal-from-client-info— removed in 0.3. Use
--principal-header. It takes the agent's name from_meta["io.modelcontextprotocol/ clientInfo"], which the MCP revision says is self-reported and "SHOULD NOT" be relied on
for security decisions. It is offerable in 0.2 only because of a fact that stops being true:
a v0.1 policy cannot address the principal at all (SPEC-v0.1.md§3.2 refusesagent_eq
and every other reserved name at load), so an unauthenticated principal misattributes
evidence and cannot widen an outcome. v0.3's authority model makes the principal an
authorization input, at which point a self-reported name cannot be one. The flag warns at
startup and its--helpsays so.
Removed
SQLiteStateStore.journal, and theEventLogclass behind it.JSONLEventSinkis that
class under the sink interface, and it isControl's now.
Fixed
.gitignoreignoredctrlrun.yamlunanchored, so it matched at any depth and silently kept
every example's policy file out of the repository. Anchored to/ctrlrun.yaml.
Compatibility
- A v0.1
ctrlrun.yamlloads unchanged.ctrlrun.policy/v2is opt-in and additive; a
document declaringv1that uses a v2 key is a load-timePolicyErrornaming the key and
the schema it needs, because a v0.1 reader would ignore the template and execute with no
duplicate protection at all. - The receipt schema is unchanged —
ctrlrun.receipt/v1still describes every receipt
v0.2 writes.EFFECT_RESOLVEDgainsdata.resolved_by, and four event types join the set:
RECONCILIATION_STARTED,RECONCILIATION_RESOLVED,EXECUTION_SUSPENDED,
EXECUTION_RESUMED. - A database written by v0.1 is read by v0.2 without migration: the one new table
(continuations) is created on open.
Notes
- The spec is written against MCP revision 2026-07-28, which removed the
initialize
handshake, protocol-level sessions andMcp-Session-Id, and madeMcp-Method/Mcp-Name
required request headers that servers must validate against the body. The gateway will also
serve2025-03-26through2025-11-25in passthrough mode, relaying session ids,GETSSE
streams andDELETEwithout interpreting them; header–body validation applies only where the
headers exist. Decisions come from the parsed body on every revision, so header trust is
never the guarantee. The deprecated2024-11-05HTTP+SSE transport is not served. - An MCP tool call held open across a multi round-trip elicitation keeps its effect reservation
inEXECUTINGwith an extended lease, so concurrent duplicates stay blocked for the whole
exchange and only the final result is mapped to an outcome. This needs the upstream to supply
arequestState, the protocol's only correlator and an optional one; without it the first
leg isAMBIGUOUS, because the alternative would let any client walk past duplicate
protection by inventing aninputResponsesfield. - A policy file using the new
effect:/resource:/mcp:keys must declare
schema: ctrlrun.policy/v2.ctrlrun.policy/v1files keep loading unchanged; av2file
will not load on 0.1.0, which is the point — 0.1.0 would ignore the effect template and
execute with no duplicate protection. - MCP tool arguments that CTRLRun cannot canonicalize — any JSON number with a fraction — will
be refused by the gateway, never rounded or coerced. Tools that move money through the
gateway need integer minor units or decimal strings in their schema.
Full contract: docs/SPEC-v0.2.md. Every claim in the README is mapped to its code and its test in docs/CLAIMS.md.