Skip to main content
The last check before an AI agent does something it can’t undo. Autonomy belongs to the action, not the agent. Every consequential action happens once, exactly as approved, or not at all — and leaves a receipt. Runs in production on a single file, or on Postgres across hosts. SQLite is the default and is production-grade on one host; Postgres is for many. Apache-2.0. CTRLRun is a Python library that sits between an agent’s decision to act and the call that acts. It binds a human’s approval to the exact action they saw, executes one logical effect at most once across processes and hosts, treats an unknown outcome as unknown rather than failed, denies anything it cannot decide, and records what happened.

What the demo shows

Five ways an agent action goes wrong, and what stops each one, in under a second with no network. The first scenario is the one that explains the product: a refund commits at the remote, the reply is lost, the agent retries, and the retry is refused. The customer was refunded once.
The other four are approval mutation, two agents racing for one effect, approval replay, and an agent trying to act outside what was delegated to it. Try it in your browser runs the same demo without an install, or read the full transcript in the repository README.

What it does

Approval binding

An approval is bound to the exact action; a mutated or replayed one is refused. Since v0.1.

One effect, once

One logical effect executes once, across threads, processes and hosts. Since v0.1.

Unknown is not failed

An unknown outcome is AMBIGUOUS, never FAILED, and blocks a blind retry. Since v0.1.

Fail closed

An unknown action, a missing policy or a missing principal is denied. Since v0.1.

Authority and delegation

With authority on, every principal needs a grant, and delegation cannot widen one. Since v0.3.

Receipts

Every executed action leaves a portable JSON receipt of who, what and outcome. Since v0.1.

Per-action policy

One YAML file decides allow, approve or deny per action and argument. Since v0.1.

Operator CLI

Approve, deny, resolve, inspect and count from the shell, against any store. Since v0.1.

MCP gateway

Every guarantee in front of an MCP tool server, with no agent changes. Since v0.2.

Reconciliation

A reconcile hook asks the remote what happened and resolves an AMBIGUOUS effect. Since v0.2.

Webhook approvals

Approval requests go to a webhook, such as Slack, and the answer comes back. Since v0.2.

OpenTelemetry export

One span per action, one span event per step; argument values are opt-in. Since v0.2.

Consumed identity

A principal comes from a verified header or JWT; CTRLRun issues nothing. Since v0.3.

Runtime delegation

A principal narrows its own grant at runtime; one revocation cuts the chain. Since v0.3.

Observe mode

Records what enforcement would have blocked, blocks nothing, and counts it. Since v0.3.

Verify

Runs the guarantee catalogue against your policy and store; N/A is not a pass. Since v0.4.

The verified badge

A GitHub Action and a badge that means the declared guarantees pass. Since v0.4.

Framework adapters

An approval routed through the framework’s own interrupt; never a second path. Since v0.5.

Runs on one host or many

SQLite on one host, Postgres across hosts, the same guarantees either way. Since v0.6.

Postgres store

The same store on Postgres, graded by the suite written for SQLite. Since v0.6.

Versioned schema

Migrations run at open, forward only, and an unknown schema is refused. Since v0.6.

Recovery on restart

A dead worker’s effect stays AMBIGUOUS until a human or a hook resolves it. Since v0.6.

Receipt chain

Each receipt carries the hash of the one before; alteration is detected and named. Since v0.6.

Policy versioning

Every receipt names the policy hash and version that decided it. Since v0.6.

Control registry

Name the house controls an action satisfies, and receipts cite them. Since v0.6.

Data scope

Label arguments by data class and condition a rule on the labels present. Since v0.6.

Three ways in

Most readers need the decorator. An adapter buys exactly one thing, routing an approval through the framework’s own interrupt, and a framework with no such primitive does not need one. Choosing between them has the decision table.

Where it stands

  • Version 0.6.0 is in development; PyPI has 0.5.0. Python 3.11 and later.
  • 3,941 tests, every version specified before it was written and every requirement mutation-tested.
  • 11 guarantees you can check in your own setup, with ctrlrun verify against your policy, on your store’s backend, in a scratch store it creates.
  • One host: a file. SQLite, no server, no ops. Many hosts: Postgres, the same guarantees, graded by the same suite.
  • Soaked for 20m 0s on postgres: 889,735 actions, 0 unattributed ambiguous outcomes, positive control fired. The roadmap asks for a week; that is not met. What it does not establish.
  • Each receipt carries the hash of the one before it, so an alteration is detected and named.
  • Apache-2.0, and the enforcement kernel stays open source. Releases carry PyPI provenance attestations from GitHub Actions.
Not yet:
  • No soak of the length the roadmap asks for. (the criterion is a week of calendar time; the published run is 20m 0s)
  • No external security audit. (planned for v0.8 or v0.9)
  • No third-party review of the kernel. (every review so far was run inside this project)
  • No sector packs. (the policy templates are starting points, not a product)

Start here

Run it for real

Which store, what a lost COMMIT does, what survives a crash, and what to watch.

MCP

The gateway in front of any MCP server, this site as an MCP server, and what is planned.

Three ways in

Decorator, gateway, adapter: what each covers and what each needs.

60-second quickstart

Protect one function end to end and read the receipt.

Why

The five principles, in 700 words. The page people link to.

Outcomes and AMBIGUOUS

The idea that explains the product: a timeout is not a failure.

Ask your coding tool

This site is an MCP server. Add it to Cursor or any MCP client that takes an mcpServers entry, and the assistant answers from these pages rather than from memory:
The server exposes one tool, a search across this documentation. When the site moves to its own domain the URL moves with it; the current one is always in this block.

Next

  • Why: what CTRLRun believes and why.
  • Install: what pip install ctrlrun puts on your machine, and what it does not.
  • How this is built: the discipline behind the guarantees.