Skip to content

v0.2.0 — Zero-friction deployment

Choose a tag to compare

@arpanghoshal arpanghoshal released this 03 Sep 21:50
· 586 commits to main since this release
87bc56f

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-agent

The 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>, in ctrlrun[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. Serves 2026-07-28 and 2025-03-26–2025-11-25 in
    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 of AMBIGUOUS, 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.
  • Suspended and Control.resume — an executor may say "the remote asked for something
    before it will finish". The record stays EXECUTING, 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 every Event and Receipt, called after the
    authoritative store write. JSONLEventSink is 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 — in ctrlrun[otel]. One OpenTelemetry span per action, one span event
    per step, ctrlrun.* attributes. Argument values are withheld unless asked for.
  • WebhookApprovalProvider — core, over stdlib urllib.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. --json emits ctrlrun.inspection/v1.
  • Policy schema: ctrlrun.policy/v2 — per-action effect:, resource: and mcp:
    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, in ctrlrun[gateway]. Answers the
    OWASP Agent Control Standard's steps/toolCallRequest and steps/toolCallResult. See
    docs/ACS.md for 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 under examples/policies/.

Changed

  • StateStore.append_event returns the event as stored, where it returned None. Sinks must
    be called with the event_id the store assigned, and the store is the only thing that knows
    it. Callers that ignore the return value are unaffected. Recorded in SPEC-v0.1.md §8.
  • SQLiteStateStore no longer writes JSONL. Control does, through JSONLEventSink, 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 refuses agent_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 --help says so.

Removed

  • SQLiteStateStore.journal, and the EventLog class behind it. JSONLEventSink is that
    class under the sink interface, and it is Control's now.

Fixed

  • .gitignore ignored ctrlrun.yaml unanchored, 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.yaml loads unchanged. ctrlrun.policy/v2 is opt-in and additive; a
    document declaring v1 that uses a v2 key is a load-time PolicyError naming 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/v1 still describes every receipt
    v0.2 writes. EFFECT_RESOLVED gains data.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 and Mcp-Session-Id, and made Mcp-Method / Mcp-Name
    required request headers that servers must validate against the body. The gateway will also
    serve 2025-03-26 through 2025-11-25 in passthrough mode, relaying session ids, GET SSE
    streams and DELETE without 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 deprecated 2024-11-05 HTTP+SSE transport is not served.
  • An MCP tool call held open across a multi round-trip elicitation keeps its effect reservation
    in EXECUTING with 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
    a requestState, the protocol's only correlator and an optional one; without it the first
    leg is AMBIGUOUS, because the alternative would let any client walk past duplicate
    protection by inventing an inputResponses field.
  • A policy file using the new effect: / resource: / mcp: keys must declare
    schema: ctrlrun.policy/v2. ctrlrun.policy/v1 files keep loading unchanged; a v2 file
    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.