> ## Documentation Index
> Fetch the complete documentation index at: https://ctrlrun.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# The execution safety layer for AI agents

> The last check before an AI agent does something it can't undo. Autonomy belongs to the action, not the agent.

CTRLRun is a Python library that sits between an agent's decision to act and the call that acts.
A consequential action happens at most once, exactly as approved, and leaves a receipt — and when
the outcome is unknown, CTRLRun says so instead of guessing.

```bash theme={null}
pip install ctrlrun && ctrlrun demo
```

<Columns cols={2}>
  <Card title="Break a protected action in your browser" icon="play" href="/docs/try-it">
    No install. Approve €2,000, execute €5,000, lose a reply, retry: real refusals, in this tab.
  </Card>

  <Card title="Protect your first action" icon="shield" href="/docs/get-started/quickstart">
    One policy file, one decorator, one approval from the shell, three receipts.
  </Card>
</Columns>

**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.

## Protect one function

CTRLRun wraps the call that has the consequence, and a YAML file says how much autonomy that
call gets. This is the whole integration for a function in your own process:

```yaml runnable theme={null}
schema: ctrlrun.policy/v2

actions:
  stripe.refund:
    effect: "refund:{payment_id}"
    rules:
      - when: { amount_gte: 0, amount_lte: 50000 }     # up to €500: autonomous
        decision: allow
      - when: { amount_gte: 0, amount_lte: 500000 }    # up to €5,000: a human decides
        decision: approve
      - decision: deny                                  # above that: never
```

```python runnable theme={null}
import ctrlrun


class Stripe:  # stands in for the real client so this block runs offline
    def refund(self, payment_id: str, amount: int) -> dict:
        return {"status": "succeeded"}


stripe = Stripe()


@ctrlrun.protect("stripe.refund", effect="refund:{payment_id}")
def refund(payment_id: str, amount: int) -> dict:
    return stripe.refund(payment_id, amount)


with ctrlrun.context(agent="refund-agent"):
    refund(payment_id="txn_1", amount=10000)  # €100: runs, and leaves a receipt
    try:
        refund(payment_id="txn_2", amount=200000)  # €2,000: waits for a human
    except ctrlrun.ApprovalRequired as pending:
        print("a human decides:", pending.request_id)
    else:
        raise SystemExit("the €2,000 refund ran without a human; the policy is not in force")
```

What the same function does next, and what stops it:

| The agent                             | CTRLRun                                                                               |
| ------------------------------------- | ------------------------------------------------------------------------------------- |
| refunds €100                          | runs it; one receipt                                                                  |
| refunds €2,000                        | raises `ApprovalRequired`; `ctrlrun approve <id>` from the shell lets it through      |
| has €2,000 approved, executes €5,000  | `ApprovalMismatch`: the approval is bound to the action a human saw                   |
| refunds €20,000                       | `ActionDenied`; no request is created                                                 |
| retries a refund whose reply was lost | `AmbiguousEffect`: the remote may have committed; a human or a reconcile hook decides |
| runs the same refund from two workers | one reserves `refund:txn_1`, the other gets `DuplicateEffect`                         |

The refund is the first example because everyone understands it; the same file protects a
`kubectl delete`, an IAM grant, a record deletion or an outbound email, and the
[cookbook](/docs/cookbook/index) has each of those as a runnable recipe.

## 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.

```console theme={null}
$ ctrlrun demo
CTRLRun demo — five ways an agent action goes wrong, and what stops it.
Policy: refunds up to €1,000 are autonomous, up to €10,000 need a human, above that are denied.

1. Duplicate effect after a lost response

   refund €500  →  remote commits  →  response lost  →  effect: AMBIGUOUS
   agent retries the same refund
   ✗ BLOCKED — effect may already have committed; blind retry refused
   remote refund calls: 1
   only a human moves it on:  ctrlrun resolve refund:txn_1 --committed|--failed
```

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](/docs/try-it) runs
the same demo without an install and lets you break one refund yourself, or read the full transcript in the
[repository README](https://github.com/CTRLRun/ctrlrun#what-ctrlrun-demo-shows).

## What it does

<Columns cols={2}>
  <Card title="Approval binding" href="/docs/concepts/approval-binding">
    An approval is bound to the exact action; a mutated or replayed one is refused. Since v0.1.
  </Card>

  <Card title="One effect, once" href="/docs/concepts/effect-keys">
    One logical effect happens at most once, across threads, processes and hosts. Since v0.1.
  </Card>

  <Card title="Unknown is not failed" href="/docs/concepts/outcomes-and-ambiguous">
    An unknown outcome is AMBIGUOUS, never FAILED, and blocks a blind retry. Since v0.1.
  </Card>

  <Card title="Fail closed" href="/docs/concepts/fail-closed">
    An unknown action, a missing policy or a missing principal is denied. Since v0.1.
  </Card>

  <Card title="Authority and delegation" href="/docs/concepts/authority-and-delegation">
    With authority on, every principal needs a grant, and delegation cannot widen one. Since v0.3.
  </Card>

  <Card title="Receipts" href="/docs/concepts/receipts-and-evidence">
    Every executed action leaves a portable JSON receipt of who, what and outcome. Since v0.1.
  </Card>
</Columns>

<Accordion title="Everything else it does (20 more)">
  <Columns cols={2}>
    <Card title="Per-action policy" href="/docs/reference/policy-yaml">
      One YAML file decides allow, approve or deny per action and argument. Since v0.1.
    </Card>

    <Card title="Operator CLI" href="/docs/reference/cli">
      Approve, deny, resolve, inspect and count from the shell, against any store. Since v0.1.
    </Card>

    <Card title="MCP gateway" href="/docs/guides/gateway-in-front-of-mcp">
      Every guarantee in front of an MCP tool server, with no agent changes. Since v0.2.
    </Card>

    <Card title="Reconciliation" href="/docs/guides/reconcile-automatically">
      A reconcile hook asks the remote what happened and resolves an AMBIGUOUS effect. Since v0.2.
    </Card>

    <Card title="Webhook approvals" href="/docs/guides/approvals-in-slack">
      Approval requests go to a webhook, such as Slack, and the answer comes back. Since v0.2.
    </Card>

    <Card title="OpenTelemetry export" href="/docs/guides/export-to-opentelemetry">
      One span per action, one span event per step; argument values are opt-in. Since v0.2.
    </Card>

    <Card title="Consumed identity" href="/docs/concepts/authority-and-delegation">
      A principal comes from a verified header or JWT; CTRLRun issues nothing. Since v0.3.
    </Card>

    <Card title="Runtime delegation" href="/docs/concepts/authority-and-delegation">
      A principal narrows its own grant at runtime; one revocation cuts the chain. Since v0.3.
    </Card>

    <Card title="Observe mode" href="/docs/concepts/observe-mode">
      Records what enforcement would have blocked, blocks nothing, and counts it. Since v0.3.
    </Card>

    <Card title="Verify" href="/docs/guides/verify-in-ci">
      Runs the guarantee catalogue against your policy and store; N/A is not a pass. Since v0.4.
    </Card>

    <Card title="The verified badge" href="/docs/verify/get-the-badge">
      A GitHub Action and a badge that means the declared guarantees pass. Since v0.4.
    </Card>

    <Card title="Framework adapters" href="/docs/get-started/three-ways-in">
      An approval routed through the framework's own interrupt; never a second path. Since v0.5.
    </Card>

    <Card title="Runs on one host or many" href="/docs/production/index">
      SQLite on one host, Postgres across hosts, the same guarantees either way. Since v0.6.
    </Card>

    <Card title="Postgres store" href="/docs/production/postgres">
      The same store on Postgres, graded by the suite written for SQLite. Since v0.6.
    </Card>

    <Card title="Versioned schema" href="/docs/production/migrations">
      Migrations run at open, forward only, and an unknown schema is refused. Since v0.6.
    </Card>

    <Card title="Recovery on restart" href="/docs/production/recovery">
      A dead worker's effect stays AMBIGUOUS until a human or a hook resolves it. Since v0.6.
    </Card>

    <Card title="Receipt chain" href="/docs/security/receipt-chain">
      Each receipt carries the hash of the one before; alteration is detected and named. Since v0.6.
    </Card>

    <Card title="Policy versioning" href="/docs/concepts/receipts-and-evidence">
      Every receipt names the policy hash and version that decided it. Since v0.6.
    </Card>

    <Card title="Control registry" href="/docs/reference/policy-yaml">
      Name the house controls an action satisfies, and receipts cite them. Since v0.6.
    </Card>

    <Card title="Data scope" href="/docs/reference/policy-yaml">
      Label arguments by data class and condition a rule on the labels present. Since v0.6.
    </Card>
  </Columns>
</Accordion>

## Three ways in

| You have                                                                                   | Use                            | Needs                                               |
| ------------------------------------------------------------------------------------------ | ------------------------------ | --------------------------------------------------- |
| Python in this process: a raw model call, a LangChain tool, a hand-rolled loop, a cron job | the `@protect` decorator       | nothing beyond `pip install ctrlrun`                |
| Tools behind an MCP server, in any language                                                | the gateway, `ctrlrun gateway` | `pip install "ctrlrun[gateway]"`                    |
| A framework with its own approval interrupt, and a place where humans already answer       | an adapter                     | the framework to have a human-in-the-loop primitive |

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](/docs/get-started/choosing) has the decision table.

## Where it stands

* **Version 0.6.1**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later.
* **4,404 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. Nothing here establishes what only accumulates over days. [What it does not establish](https://ctrlrun.dev/docs/production/soak).
* **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 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

<Columns cols={3}>
  <Card title="Protect your first action" icon="play" href="/docs/get-started/quickstart">
    Protect one function end to end and read the receipt. Ten minutes.
  </Card>

  <Card title="Try it in your browser" icon="flask" href="/docs/try-it">
    One refund you can break six ways, and the five demo scenarios, on the released wheel.
  </Card>

  <Card title="Cookbook" icon="book" href="/docs/cookbook/index">
    Refunds, deploys, IAM, deletions, email, MCP, LangGraph: each a recipe that runs.
  </Card>
</Columns>

<Columns cols={3}>
  <Card title="Three ways in" icon="signpost" href="/docs/get-started/three-ways-in">
    Decorator, gateway, adapter: what each covers and what each needs.
  </Card>

  <Card title="MCP" icon="plug" href="/docs/mcp/overview">
    The gateway in front of any MCP server, and this site as an MCP server.
  </Card>

  <Card title="Run it for real" icon="server" href="/docs/production/index">
    Which store, what a lost `COMMIT` does, what survives a crash, and what to watch.
  </Card>
</Columns>

<Columns cols={2}>
  <Card title="Why" icon="book-open" href="/docs/why">
    The five principles, in 700 words. The page people link to.
  </Card>

  <Card title="Outcomes and AMBIGUOUS" icon="circle-help" href="/docs/concepts/outcomes-and-ambiguous">
    The idea that explains the product: a timeout is not a failure.
  </Card>
</Columns>

## 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:

```json theme={null}
{
  "mcpServers": {
    "ctrlrun-docs": { "type": "http", "url": "https://ctrlrun.dev/mcp" }
  }
}
```

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](/docs/why): what CTRLRun believes and why.
* [Install](/docs/get-started/install): what `pip install ctrlrun` puts on your machine, and what it does not.
* [How this is built](/docs/how-this-is-built): the discipline behind the guarantees.


## Related topics

- [Execution safety for AI agents](/index.md)
- [Protect my agent](/protect-my-agent.md)
- [Agent Execution Risk Check](/risk-check.md)
- [How this is built](/docs/how-this-is-built.md)
- [Running on Postgres](/docs/postgres.md)
