From e30e1df09d34e9dba5e852d297635757189866b2 Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 16 Sep 2026 18:05:06 +0530 Subject: [PATCH 1/2] One Overview, in the documentation's own layout `/` and `/docs` were both titled Overview and sat one above the other in the Start group, opening on the same claim. `/` was also still a landing page: its breakpoints were written against the viewport, which does not account for the sidebar, so at 1440px the hero split a 705px column into two 310px ones, the lede ran under the assistant widget, and Mintlify's frontmatter title gave the page a second H1 saying what the hero already said. docs.mdx is merged into index.mdx and `/docs` redirects to `/`. The hand-built hero goes with it: the page is now frontmatter title, a lede and `##` sections, the same shape as every other page. The two cards at the top of the old `/docs` repeated the hero's buttons and the Start here grid, and the hallucinated-refund chain is the refund already worked through in Protect one function and the demo, so neither survives the merge. Everything else does. The description was the body lede word for word; `/docs`'s own description takes that slot. The diagram carries the tokens the `.cr-site` wrapper used to give it. The navbar's Docs link pointed at the merged page and is removed: the wordmark and How it works already go to `/`. Signed-off-by: arpan --- .mintignore | 2 +- README.md | 6 +- docs.json | 10 +- docs.mdx | 291 ----------------------- execution-boundary.mdx | 2 +- index.mdx | 350 +++++++++++++++++++++++----- style.css | 8 +- tests/test_docs_production.py | 4 +- tests/test_docs_site.py | 9 +- tests/test_home_and_readme_agree.py | 22 +- tools/docs_audit/lint-allowlist.txt | 2 +- 11 files changed, 334 insertions(+), 372 deletions(-) delete mode 100644 docs.mdx diff --git a/.mintignore b/.mintignore index e3ad45d..5c91c74 100644 --- a/.mintignore +++ b/.mintignore @@ -13,7 +13,7 @@ SEO.md BUILD-PROMPTS-*.md # The capability source and the rendered fragments pages embed. A render is a fragment, not a -# page: `docs.mdx` and `docs/production/index.mdx` include them. +# page: `index.mdx` and `docs/production/index.mdx` include them. capabilities.yaml generated/ diff --git a/README.md b/README.md index 45e9c8c..9c50d6c 100644 --- a/README.md +++ b/README.md @@ -80,13 +80,13 @@ Apache-2.0, the same as the library. See [LICENSE](LICENSE). ## The site, file by file -- `index.mdx` serves `/`: the homepage, the hallucinated-refund example, the execution - boundary and its four rules, and the integration entry point. +- `index.mdx` serves `/`: the one Overview. The hero and the hallucinated-refund example, the + seven-step diagram, and below them the technical overview `docs.mdx` used to carry at + `/docs` until the two were merged on 2026-09-16. `/docs` redirects here. - `execution-boundary.mdx` serves `/execution-boundary`: the boundary in three sections -- the drawing, the three ways it goes into a codebase, and the four steps by which autonomy widens. The drawing lives in `snippets/execution-boundary.jsx` and is chosen by one control, the domain: it carries the action, the five checks and the refusal each one raises, so the prose beside it stays short. -- `docs.mdx` serves `/docs`: the technical overview. - `docs/` is every technical page, published under `/docs/...`. - `execution-boundary.mdx` is a custom-mode page. The two commercial pages it sat beside, `risk-check.mdx` and `protect-my-agent.mdx`, were removed when the site became technical only. diff --git a/docs.json b/docs.json index 906318c..77e0c98 100644 --- a/docs.json +++ b/docs.json @@ -20,10 +20,6 @@ "href": "/#how-it-works", "label": "How it works" }, - { - "href": "/docs", - "label": "Docs" - }, { "href": "https://github.com/CTRLRun/ctrlrun", "label": "GitHub" @@ -44,7 +40,6 @@ "group": "Start", "pages": [ "index", - "docs", "docs/why", "docs/not-only-agents", "docs/agents-you-cant-modify", @@ -372,6 +367,11 @@ "indexing": "navigable" }, "redirects": [ + { + "source": "/docs", + "destination": "/", + "permanent": true + }, { "source": "/why", "destination": "/docs/why", diff --git a/docs.mdx b/docs.mdx deleted file mode 100644 index ed0736e..0000000 --- a/docs.mdx +++ /dev/null @@ -1,291 +0,0 @@ ---- -title: "The execution safety layer for AI agents" -sidebarTitle: "Overview" -description: "The last check before an AI agent does something it can't undo. Autonomy belongs to the action, not the agent." -mode: "wide" -"og:title": "CTRLRun: the execution safety layer for AI agents" -"twitter:title": "CTRLRun: the execution safety layer for AI agents" -canonical: "https://ctrlrun.dev/docs" ---- - -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 -pip install ctrlrun && ctrlrun demo -``` - - - - No install. The path an action takes, the five checks on it, and the refusal each one raises. - - - One policy file, one decorator, one approval from the shell, three receipts. - - - -**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 -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 -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 ` 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 -$ 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. [The execution boundary](/execution-boundary) draws -the same decisions without an install, or read the full transcript in the -[repository README](https://github.com/CTRLRun/ctrlrun#what-ctrlrun-demo-shows). - -## What it does - -{/* generated from capabilities.yaml (mdx) — edit the YAML, never this grid */} - - - An approval is bound to the exact action; a mutated or replayed one is refused. Since v0.1. - - - One logical effect happens at most once, across threads, processes and hosts. Since v0.1. - - - An unknown outcome is AMBIGUOUS, never FAILED, and blocks a blind retry. Since v0.1. - - - An unknown action, a missing policy or a missing principal is denied. Since v0.1. - - - Every principal needs a grant, delegation cannot widen one, and a grant bounds the total. Since v0.3. - - - Every executed action leaves a portable JSON receipt of who, what and outcome. Since v0.1. - - - - - - One YAML file decides allow, approve or deny per action and argument. Since v0.1. - - - Approve, deny, resolve, inspect and count from the shell, against any store. Since v0.1. - - - Every guarantee in front of an MCP tool server, with no agent changes. Since v0.2. - - - A reconcile hook asks the remote what happened and resolves an AMBIGUOUS effect. Since v0.2. - - - Approval requests go to a webhook, such as Slack, and the answer comes back. Since v0.2. - - - One span per action, one span event per step; argument values are opt-in. Since v0.2. - - - A principal comes from a verified header or JWT; CTRLRun issues nothing. Since v0.3. - - - A principal narrows its own grant at runtime; one revocation cuts the chain. Since v0.3. - - - Records what enforcement would have blocked, blocks nothing, and counts it. Since v0.3. - - - Runs the guarantee catalogue against your policy and store; N/A is not a pass. Since v0.4. - - - A GitHub Action and a badge that means the declared guarantees pass. Since v0.4. - - - An approval routed through the framework's own interrupt; never a second path. Since v0.5. - - - SQLite on one host, Postgres across hosts, the same guarantees either way. Since v0.6. - - - The same store on Postgres, graded by the suite written for SQLite. Since v0.6. - - - Migrations run at open, forward only, and an unknown schema is refused. Since v0.6. - - - A dead worker's effect stays AMBIGUOUS until a human or a hook resolves it. Since v0.6. - - - Each receipt carries the hash of the one before; alteration is detected and named. Since v0.6. - - - Every receipt names the policy hash and version that decided it. Since v0.6. - - - Name the house controls an action satisfies, and receipts cite them. Since v0.6. - - - Label arguments by data class and condition a rule on the labels present. Since v0.6. - - - -{/* end generated */} - -## 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 - -{/* generated from the suite, pyproject and the soak (mdx) — run the generator */} -- **Version 0.12.2**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14. -- **6,248 tests**, every version specified before it was written and every requirement mutation-tested. -- **32 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. (optional, and no release waits for one) -- 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) -{/* end generated */} - -## Start here - - - - Protect one function end to end and read the receipt. Ten minutes. - - - One action, five ways to stop it, drawn for any of twelve domains. - - - Refunds, deploys, IAM, deletions, email, MCP, LangGraph: each a recipe that runs. - - - - - - Decorator, gateway, adapter: what each covers and what each needs. - - - The gateway in front of any MCP server, and this site as an MCP server. - - - Which store, what a lost `COMMIT` does, what survives a crash, and what to watch. - - - - - - The five principles, in 700 words. The page people link to. - - - 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: - -```json -{ - "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. diff --git a/execution-boundary.mdx b/execution-boundary.mdx index 3b8d0d9..364f14b 100644 --- a/execution-boundary.mdx +++ b/execution-boundary.mdx @@ -19,5 +19,5 @@ import { ExecutionBoundary } from "/snippets/execution-boundary.jsx"; -
Give agents autonomy.
Keep control of their actions.
+
Give agents autonomy.
Keep control of their actions.
diff --git a/index.mdx b/index.mdx index 0e8175d..834c2e3 100644 --- a/index.mdx +++ b/index.mdx @@ -1,8 +1,7 @@ --- title: "Stop wrong, restricted, or malicious AI agent actions" sidebarTitle: "Overview" -description: "Every AI agent action is checked against your rules before it runs. Allowed ones go through, sensitive ones wait for a person, forbidden ones are blocked." -mode: "wide" +description: "The last check before an AI agent does something it can't undo. Autonomy belongs to the action, not the agent." "og:title": "CTRLRun: stop wrong, restricted, or malicious agent actions" "twitter:title": "CTRLRun: stop wrong, restricted, or malicious agent actions" canonical: "https://ctrlrun.dev/" @@ -11,67 +10,302 @@ canonical: "https://ctrlrun.dev/" import { HowDiagram } from "/snippets/how-diagram.jsx"; -
-
-
-

THE AGENT IS PROBABILISTIC. THE ACTION IS NOT.

-

CTRLRun stops AI agents from taking wrong, restricted, or malicious actions in your workflows.

-

Every action is checked against your rules before it runs. Allowed actions go through. Sensitive ones wait for a person. Forbidden ones are blocked.

- -

Open source · one line, any action{'@ctrlrun.protect("your.action")'}

-
-
-

ONE EXAMPLE / THE MODEL HALLUCINATES AN AMOUNT

-
-

The model guesses.
CTRLRun does not.

-
-

The ticket saysrefund $500

-

The agent asks for$5,000

-
-
-
- Without CTRLRun -
    -
  1. Nothing checks the amount
  2. The call goes through
  3. $4,500 too much
  4. -
-
-
- With CTRLRun -
    -
  1. Your rule checks the amount
  2. The call never leaves
  3. $0 wrongly paid
  4. -
-
-
-
-
-
-

Works with agents you can and can't modify.

-

WhatsApp, Slack, Teams, Claude Code, Cursor, Codex, ChatGPT, OpenAI Agents. Any AI agent you have. If it acts through your systems, it is checked.

- How it works -
-
- -
-
-

HOW CTRLRUN WORKS

-

Let it run. Ask a human. Or stop it cold.

-

CTRLRun stops an agent from taking an action your rules do not allow. Every action that leaves your agents, tools and workflows is normalized into one action, decided against your policy, held for a person where you require it, reserved so it cannot run twice, executed, resolved and recorded. An action with no rule is blocked, arguments changed after sign-off void the approval, and an outcome nobody knows is never retried on a guess.

-
- -

Interactive walkthrough: follow one agent action through every check →

-
- -
+**CTRLRun stops AI agents from taking wrong, restricted, or malicious actions in your workflows.** + +Every action is checked against your rules before it runs. Allowed actions go through. Sensitive +ones wait for a person. Forbidden ones are blocked. + +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 +pip install ctrlrun && ctrlrun demo +``` + +**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. + +## How it works + +CTRLRun stops an agent from taking an action your rules do not allow. Every action that leaves +your agents, tools and workflows is normalized into one action, decided against your policy, +held for a person where you require it, reserved so it cannot run twice, executed, resolved and +recorded. An action with no rule is blocked, arguments changed after sign-off void the approval, +and an outcome nobody knows is never retried on a guess. + + + +[Interactive walkthrough: follow one agent action through every check](/execution-boundary) + +## Works with agents you can and can't modify + +WhatsApp, Slack, Teams, Claude Code, Cursor, Codex, ChatGPT, OpenAI Agents. **Any AI agent you +have.** If it acts through your systems, it is checked. +[How it works](/docs/agents-you-cant-modify). + +## 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 +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 +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 ` 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 +$ 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. [The execution boundary](/execution-boundary) draws +the same decisions without an install, or read the full transcript in the +[repository README](https://github.com/CTRLRun/ctrlrun#what-ctrlrun-demo-shows). + +## What it does + +{/* generated from capabilities.yaml (mdx) — edit the YAML, never this grid */} + + + An approval is bound to the exact action; a mutated or replayed one is refused. Since v0.1. + + + One logical effect happens at most once, across threads, processes and hosts. Since v0.1. + + + An unknown outcome is AMBIGUOUS, never FAILED, and blocks a blind retry. Since v0.1. + + + An unknown action, a missing policy or a missing principal is denied. Since v0.1. + + + Every principal needs a grant, delegation cannot widen one, and a grant bounds the total. Since v0.3. + + + Every executed action leaves a portable JSON receipt of who, what and outcome. Since v0.1. + + + + + + One YAML file decides allow, approve or deny per action and argument. Since v0.1. + + + Approve, deny, resolve, inspect and count from the shell, against any store. Since v0.1. + + + Every guarantee in front of an MCP tool server, with no agent changes. Since v0.2. + + + A reconcile hook asks the remote what happened and resolves an AMBIGUOUS effect. Since v0.2. + + + Approval requests go to a webhook, such as Slack, and the answer comes back. Since v0.2. + + + One span per action, one span event per step; argument values are opt-in. Since v0.2. + + + A principal comes from a verified header or JWT; CTRLRun issues nothing. Since v0.3. + + + A principal narrows its own grant at runtime; one revocation cuts the chain. Since v0.3. + + + Records what enforcement would have blocked, blocks nothing, and counts it. Since v0.3. + + + Runs the guarantee catalogue against your policy and store; N/A is not a pass. Since v0.4. + + + A GitHub Action and a badge that means the declared guarantees pass. Since v0.4. + + + An approval routed through the framework's own interrupt; never a second path. Since v0.5. + + + SQLite on one host, Postgres across hosts, the same guarantees either way. Since v0.6. + + + The same store on Postgres, graded by the suite written for SQLite. Since v0.6. + + + Migrations run at open, forward only, and an unknown schema is refused. Since v0.6. + + + A dead worker's effect stays AMBIGUOUS until a human or a hook resolves it. Since v0.6. + + + Each receipt carries the hash of the one before; alteration is detected and named. Since v0.6. + + + Every receipt names the policy hash and version that decided it. Since v0.6. + + + Name the house controls an action satisfies, and receipts cite them. Since v0.6. + + + Label arguments by data class and condition a rule on the labels present. Since v0.6. + + + +{/* end generated */} + +## 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 + +{/* generated from the suite, pyproject and the soak (mdx) — run the generator */} +- **Version 0.12.2**, on [PyPI](https://pypi.org/project/ctrlrun/), Python 3.11 and later, tested on 3.11 to 3.14. +- **6,248 tests**, every version specified before it was written and every requirement mutation-tested. +- **32 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. (optional, and no release waits for one) +- 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) +{/* end generated */} ## Built on this kernel Two products run on CTRLRun and credit it on every page. [ctrl ai agents](https://ctrlaiagents.com), the hosted product for a person or a team, puts this boundary under any agent you buy or build, with the inbox, the receipts and the analysis in one dashboard. [ctrl payments](https://ctrlpayments.com) is the same boundary for money: every payment an agent attempts is allowed, held for a person, or refused before it leaves, and there is a receipt either way. The kernel that decides and refuses is this one, Apache-2.0, and the receipt format they write is the one documented here. +## Start here + + + + Protect one function end to end and read the receipt. Ten minutes. + + + One action, five ways to stop it, drawn for any of twelve domains. + + + Refunds, deploys, IAM, deletions, email, MCP, LangGraph: each a recipe that runs. + + + + + + Decorator, gateway, adapter: what each covers and what each needs. + + + The gateway in front of any MCP server, and this site as an MCP server. + + + Which store, what a lost `COMMIT` does, what survives a crash, and what to watch. + + + + + + The five principles, in 700 words. The page people link to. + + + 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: + +```json +{ + "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. -- [Quickstart](/docs/get-started/quickstart): protect your first action. -- [The execution boundary](/execution-boundary): follow one agent action through every check. +- [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. diff --git a/style.css b/style.css index c0e5945..29d1a04 100644 --- a/style.css +++ b/style.css @@ -373,7 +373,13 @@ body:has(.cr-subpage) #search-bar-entry-mobile,body:has(.cr-subpage) #assistant- /* How it works: the boundary, drawn. Two ends we do not own, one box we do. */ /* How it works, drawn. Two variants: the wide one keeps seven stages on one spine, the narrow one stacks them -- one SVG scaled down to a phone renders its labels at 4px. */ -.cr-diagram { display:block; width:100%; height:auto; } +/* The overview took the documentation's own layout on 2026-09-16 and the `.cr-site` wrapper + went with the hero, so the diagram carries the tokens it reads rather than inheriting them. + Inside `.cr-site`, on `/execution-boundary`, the wrapper's values still win: these are set + on the SVG, and the wrapper sets them on an ancestor of it only when there is one. */ +.cr-diagram { display:block; width:100%; height:auto; --cr-ink:#20221f; --cr-muted:#656963; --cr-line:#e0e3dc; --cr-paper:#fafbf8; --cr-panel:#f2f4ee; --cr-accent:#a96308; } +.dark .cr-diagram { --cr-ink:#f0f1e9; --cr-muted:#b0b5a9; --cr-line:#343b30; --cr-paper:#171b15; --cr-panel:#20261e; --cr-accent:#efb752; } +.cr-site .cr-diagram { --cr-ink:inherit; --cr-muted:inherit; --cr-line:inherit; --cr-paper:inherit; --cr-panel:inherit; --cr-accent:inherit; } .cr-dia-narrow { display:none; } .cr-diagram text { font-family:inherit; } .cr-dia-label,.cr-dia-num { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; fill:var(--cr-accent); } diff --git a/tests/test_docs_production.py b/tests/test_docs_production.py index d226198..dc640e2 100644 --- a/tests/test_docs_production.py +++ b/tests/test_docs_production.py @@ -422,7 +422,7 @@ def test_every_production_page_is_in_the_production_group(): #: CTRLRun does, how to use it and how it works. The block has two homes on the site, where a #: reader who wants the numbers goes, and the generator still refuses a shrunken suite: what #: was dropped is one embedding, not the guard. -READINESS_HOMES = ("docs.mdx", "docs/production/index.mdx") +READINESS_HOMES = ("index.mdx", "docs/production/index.mdx") @pytest.mark.parametrize("home", READINESS_HOMES) @@ -709,7 +709,7 @@ def test_the_readme_says_where_it_runs_before_the_badges(): def test_the_home_page_offers_to_run_it_for_real(): - home = (DOCS / "docs.mdx").read_text(encoding="utf-8") + home = (DOCS / "index.mdx").read_text(encoding="utf-8") assert "Run it for real" in home assert "/docs/production/index" in home diff --git a/tests/test_docs_site.py b/tests/test_docs_site.py index b537ef6..31b44ff 100644 --- a/tests/test_docs_site.py +++ b/tests/test_docs_site.py @@ -166,7 +166,7 @@ def test_every_page_links_to_why_and_to_get_started_or_is_one_of_them(page: Path text = _body(page) if slug != "docs/why": assert "](/docs/why)" in text, f"{page.name} does not link to Why" - if not slug.startswith("docs/get-started/") and slug != "docs": + if not slug.startswith("docs/get-started/"): assert "](/docs/get-started/" in text, f"{page.name} does not link to Get started" @@ -181,7 +181,7 @@ def test_every_page_but_a_reference_page_fits_the_word_budget(page: Path): slug = page.relative_to(DOCS).with_suffix("").as_posix() if ( slug.startswith("docs/reference/") - or slug in {"index", "docs"} + or slug == "index" or slug.removeprefix("docs/") in LONG_FORM ): return @@ -205,7 +205,10 @@ def test_every_concepts_page_says_what_it_does_not_do(page: Path): def test_the_documentation_root_preserves_the_technical_overview(): - text = (DOCS / "docs.mdx").read_text(encoding="utf-8") + """`/docs` was a second page titled Overview, one entry below this one in the same + sidebar group and opening on the same claim. It was merged into the root page on + 2026-09-16; what it carried that the hero did not has to survive that merge.""" + text = (DOCS / "index.mdx").read_text(encoding="utf-8") assert "The last check before an AI agent does something it can't undo." in text assert "Autonomy belongs to the action, not the agent." in text assert "generated from capabilities.yaml (mdx)" in text diff --git a/tests/test_home_and_readme_agree.py b/tests/test_home_and_readme_agree.py index 0f59280..d3f158c 100644 --- a/tests/test_home_and_readme_agree.py +++ b/tests/test_home_and_readme_agree.py @@ -20,6 +20,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] HOME = REPO_ROOT / "index.mdx" +_FRONTMATTER = re.compile(r"\A---\n.*?\n---\n", re.S) DIAGRAM = REPO_ROOT / "snippets" / "how-diagram.jsx" README = CORE_ROOT / "README.md" @@ -31,10 +32,20 @@ def _prose(markup: str) -> str: return " ".join(re.sub(r"<[^>]+>", "", text).split()) -def _homepage(pattern: str) -> str: - match = re.search(pattern, HOME.read_text(encoding="utf-8"), re.S) - assert match, f"index.mdx no longer carries {pattern!r}" - return _prose(match.group(1)) +def _opening() -> tuple[str, str]: + """The two sentences the overview opens with: the claim, then how it is met. + + They were an `

` and a `

` in a hand-built hero until 2026-09-16, when the page was + merged with `/docs` and took the documentation's own layout. They are the page's first two + paragraphs now, the claim in bold, and the README still has to open with them. + """ + body = _FRONTMATTER.sub("", HOME.read_text(encoding="utf-8"), count=1) + body = re.sub(r"^import .*$", "", body, flags=re.M).strip() + paragraphs = [block.strip() for block in body.split("\n\n") if block.strip()] + assert len(paragraphs) >= 2, "index.mdx no longer opens with two paragraphs" + claim = re.fullmatch(r"\*\*(.+?)\*\*", paragraphs[0], re.S) + assert claim, f"index.mdx no longer opens with the claim in bold: {paragraphs[0][:80]!r}" + return _prose(claim.group(1)), _prose(paragraphs[1]) def _readme() -> str: @@ -45,8 +56,7 @@ def test_the_readme_opens_with_the_homepage_h1_and_lede(): """In sequence, not merely present: the README's prose opens with the H1 and the lede follows it directly. Prose before the H1, or the lede ahead of it, fails here.""" head = _prose(_readme().split("\n## ", 1)[0]) - h1 = _homepage(r'

(.*?)

') - lede = _homepage(r'

(.*?)

') + h1, lede = _opening() assert h1.startswith("CTRLRun ") and h1.endswith("."), h1 assert head.startswith(h1), f"the README does not open with the homepage H1: {head[:120]!r}" diff --git a/tools/docs_audit/lint-allowlist.txt b/tools/docs_audit/lint-allowlist.txt index a7f15e8..6984c71 100644 --- a/tools/docs_audit/lint-allowlist.txt +++ b/tools/docs_audit/lint-allowlist.txt @@ -85,7 +85,7 @@ allow docs/compare/governance-toolkits.mdx oversight toolkits # neighbours**, so under `*` a real sector-pack claim written directly above or below it would # be permitted anywhere. An independent review found the wider glob. allow README.md No sector packs\. \(the policy templates are starting points -allow docs.mdx No sector packs\. \(the policy templates are starting points +allow index.mdx No sector packs\. \(the policy templates are starting points allow docs/production/index.mdx No sector packs\. \(the policy templates are starting points allow generated/readiness.* No sector packs\. \(the policy templates are starting points allow docs/CLAIMS.md no external security audit, no third-party review of the kernel, no sector packs From 2cb2b01d7fe544137ad5751c4ce11fba6418e837 Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 16 Sep 2026 18:22:09 +0530 Subject: [PATCH 2/2] The name is lowercase ctrlrun across the site The wordmark has been the lowercase ctrlrun since #53 and every sentence beside it still said CTRLRun. Pages, frontmatter titles and descriptions, the social titles, the quoted CLI transcripts and the tests that pin them are one spelling now, lowercase at the start of a sentence too. The GitHub owner keeps its own case wherever it appears, in repository links, badge URLs and the MCP registry namespace, which is built from the owner and compared case-sensitively; so do CTRLRunError, whose reference page is named after it, and the CTRLRun-Signature header the approvals guide tells a reader to verify. Paired with the kernel's lowercase-ctrlrun branch: the README opens with this site's first two sentences and each side pins the other's, so the two land together. Signed-off-by: arpan --- IA.md | 18 +++---- README.md | 4 +- SEO.md | 34 ++++++------ STYLE.md | 4 +- capabilities.yaml | 4 +- docs.json | 6 +-- docs/ACS.md | 26 ++++----- docs/ARCHITECTURE.md | 12 ++--- docs/CLAIMS.md | 18 +++---- docs/OWASP-AGENTIC-TOP10.md | 26 ++++----- docs/OWASP-SOLUTIONS-LANDSCAPE.md | 54 +++++++++---------- docs/ROADMAP.md | 22 ++++---- docs/THREAT_MODEL.md | 34 ++++++------ docs/adapters.md | 6 +-- docs/agents-you-cant-modify.mdx | 12 ++--- docs/architecture/specifications.mdx | 2 +- docs/authority.md | 2 +- docs/compare/durable-workflows.mdx | 16 +++--- docs/compare/framework-hitl.mdx | 12 ++--- docs/compare/governance-toolkits.mdx | 18 +++---- docs/compare/guardrail-libraries.mdx | 18 +++---- docs/compare/idempotency-keys.mdx | 4 +- docs/concepts/approval-binding.mdx | 6 +-- docs/concepts/authority-and-delegation.mdx | 2 +- docs/concepts/fail-closed.mdx | 4 +- docs/concepts/observe-mode.mdx | 2 +- docs/concepts/outcomes-and-ambiguous.mdx | 4 +- docs/concepts/receipts-and-evidence.mdx | 2 +- docs/cookbook/openai-agents-tool-approval.mdx | 4 +- docs/cookbook/receipts-to-opentelemetry.mdx | 4 +- docs/cookbook/verify-in-github-actions.mdx | 6 +-- docs/faq.mdx | 26 ++++----- docs/get-started/quickstart.mdx | 4 +- docs/get-started/three-ways-in.mdx | 4 +- docs/guides/export-to-opentelemetry.mdx | 4 +- docs/guides/gateway-in-front-of-mcp.mdx | 4 +- docs/guides/langchain-middleware.mdx | 12 ++--- docs/guides/langgraph-adapter.mdx | 2 +- docs/guides/observe-to-enforce.mdx | 2 +- docs/guides/openai-agents-adapter.mdx | 8 +-- docs/guides/resolve-an-ambiguous-effect.mdx | 2 +- docs/guides/run-on-postgres.mdx | 4 +- docs/guides/verify-in-ci.mdx | 6 +-- docs/how-this-is-built.md | 2 +- docs/mcp/approve-from-your-assistant.mdx | 2 +- docs/mcp/gateway-in-5-minutes.mdx | 8 +-- docs/mcp/overview.mdx | 8 +-- docs/mcp/use-the-docs-from-your-editor.mdx | 6 +-- docs/not-only-agents.mdx | 4 +- docs/postgres.md | 8 +-- docs/production/anchoring.mdx | 4 +- docs/production/index.mdx | 2 +- docs/production/migrations.mdx | 4 +- docs/production/operations.mdx | 2 +- docs/production/recovery.mdx | 2 +- docs/reference/api/CTRLRunError.mdx | 4 +- docs/reference/api/Suspended.mdx | 2 +- docs/reference/api/acs-AcsControlHook.mdx | 4 +- docs/reference/api/index.mdx | 4 +- docs/reference/cli.mdx | 4 +- docs/reference/errors.mdx | 8 +-- docs/reference/exit-codes.mdx | 4 +- docs/reference/policy-yaml.mdx | 6 +-- docs/reference/receipt-and-event-schemas.mdx | 4 +- docs/security/assurance-case.mdx | 6 +-- docs/security/disclosure.mdx | 6 +-- docs/security/receipt-chain.mdx | 2 +- docs/verify.md | 18 +++---- docs/verify/get-the-badge.mdx | 4 +- docs/why.mdx | 10 ++-- execution-boundary.mdx | 8 +-- generated/badges.readme.md | 2 +- generated/capabilities.mdx | 2 +- generated/capabilities.txt | 2 +- images/wordmark.svg | 2 +- index.mdx | 24 ++++----- pyproject.toml | 2 +- scripts/render-how-diagram.py | 8 +-- snippets/architecture-review.jsx | 6 +-- snippets/execution-boundary.jsx | 2 +- snippets/how-diagram.jsx | 12 ++--- tests/test_cookbook_pages.py | 2 +- tests/test_docs_production.py | 8 +-- tests/test_docs_reference.py | 2 +- tests/test_docs_seo.py | 4 +- tests/test_home_and_readme_agree.py | 2 +- tests/test_owasp_landscape.py | 6 +-- tests/test_owasp_mapping.py | 10 ++-- tests/test_release_documents.py | 8 +-- tests/test_verify_page.py | 2 +- tools/docs_audit/lint-allowlist.txt | 6 +-- tools/docs_audit/lint.py | 6 +-- tools/docs_audit/render_badges.py | 2 +- tools/docs_audit/render_cookbook.py | 2 +- tools/docs_audit/render_readiness.py | 2 +- tools/docs_audit/render_schemas.py | 6 +-- website-form/README.md | 4 +- website-form/api/interest.mjs | 8 +-- website-form/api/review.mjs | 4 +- website-form/interest.test.mjs | 8 +-- website-form/review.test.mjs | 2 +- 101 files changed, 383 insertions(+), 383 deletions(-) diff --git a/IA.md b/IA.md index 0a1ba29..107df85 100644 --- a/IA.md +++ b/IA.md @@ -12,7 +12,7 @@ answer. Later sessions write the pages; this file is what they write against, an | Principle | Autonomy belongs to the action, not the agent. | Second sentence everywhere; the line people quote | | Category | The execution safety layer for AI agents. | GitHub About, PyPI summary, site ``, directory listings | | Opener (long-form only) | Everyone is rushing to ship AI agents without thinking about consequences. | First line of Why and of launch posts; never a heading | -| Promise | 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. | Hero subline, README paragraph 2 | +| Promise | 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. | Hero subline, README paragraph 2 | | Hook (posts) | Agents can retry. The real world can't. | Social, talk titles; not the README header | The rules every page is held to are in `STYLE.md`. The tools that hold them are in @@ -157,7 +157,7 @@ row landed with `docs/SPEC-mcp-operator.md`, which is when it stopped being plan | Path | Purpose | Query | |---|---|---| -| `docs/mcp/overview` | CTRLRun works with MCP in four ways: enforcement (the gateway in front of any MCP server), answering (the operator server, for approvers), learning (this site is an MCP server), discovery (the registries, once listed). | *MCP gateway* · *MCP server human approval* | +| `docs/mcp/overview` | ctrlrun works with MCP in four ways: enforcement (the gateway in front of any MCP server), answering (the operator server, for approvers), learning (this site is an MCP server), discovery (the registries, once listed). | *MCP gateway* · *MCP server human approval* | | `docs/mcp/gateway-in-5-minutes` | For a reader who already runs an MCP server: before/after, the two commands, what the agent sees on deny and on approval-required, the supported revisions, the principal-flag choice and its security note. | *protect MCP server* · *MCP tool call approval gateway* | | `docs/mcp/approve-from-your-assistant` | For the person who answers approvals rather than the one who deploys: what `ctrlrun mcp-operator` is, the two flags, a client configuration, a real transcript ending in the receipt that names the approver, and the five things it will not do. | *approve MCP tool call from an assistant* · *MCP human approval server* | | `docs/mcp/use-the-docs-from-your-editor` | The exact configuration for this site's MCP server, three questions an assistant can then answer, a screenshot spec. | *ctrlrun mcp docs* | @@ -199,7 +199,7 @@ turn readers into users. | `docs/guides/verify-in-ci` | The GitHub Action, the two shapes of report, the N/A line and what it means, the badge. | *verify AI agent safety configuration CI* | | `docs/guides/export-to-opentelemetry` | `OTelEventSink`: one span per action, one event per step, argument values opt-in. | *opentelemetry AI agent actions* | | `docs/guides/langgraph-adapter` | Route an approval through `interrupt()`: the operator builds the `Control`, `wait=True`, `Command(resume=...)`, and prevention versus attribution. | *langgraph interrupt human approval tool call* | -| `docs/guides/openai-agents-adapter` | Route an approval through the SDK's tool-approval interruption: `protected_tool`, `gate.run`, why a rejection leaves no CTRLRun evidence. | *openai agents sdk tool approval* | +| `docs/guides/openai-agents-adapter` | Route an approval through the SDK's tool-approval interruption: `protected_tool`, `gate.run`, why a rejection leaves no ctrlrun evidence. | *openai agents sdk tool approval* | ## Cookbook (session 4) @@ -256,9 +256,9 @@ short table. No vendor name in a heading. | Path | Purpose | Query | |---|---|---| | `docs/compare/framework-hitl` | A framework's interrupt lets a human say yes; it does not bind the yes to the arguments that execute, refuse a retry after a lost response, or leave a receipt. Use both: the adapter routes through the interrupt. | *langgraph human in the loop vs* · *agent framework approval limitations* | -| `docs/compare/guardrail-libraries` | Guardrails inspect inputs and outputs; CTRLRun sits at the boundary between intention and effect. Different layer; use both. | *AI guardrails vs execution control* | -| `docs/compare/governance-toolkits` | Governance toolkits catalogue, monitor and report; CTRLRun refuses, in the execution path, per action. | *AI agent governance vs runtime enforcement* | -| `docs/compare/durable-workflows` | Durable workflow engines retry until success and make that safe with idempotent activities; CTRLRun refuses to retry an unknown outcome and binds approvals. Complementary. | *temporal vs ctrlrun* · *durable execution AI agents idempotency* | +| `docs/compare/guardrail-libraries` | Guardrails inspect inputs and outputs; ctrlrun sits at the boundary between intention and effect. Different layer; use both. | *AI guardrails vs execution control* | +| `docs/compare/governance-toolkits` | Governance toolkits catalogue, monitor and report; ctrlrun refuses, in the execution path, per action. | *AI agent governance vs runtime enforcement* | +| `docs/compare/durable-workflows` | Durable workflow engines retry until success and make that safe with idempotent activities; ctrlrun refuses to retry an unknown outcome and binds approvals. Complementary. | *temporal vs ctrlrun* · *durable execution AI agents idempotency* | | `docs/compare/idempotency-keys` | An idempotency key deduplicates at one remote that supports it; an effect key deduplicates at the agent side across remotes, refuses on unknown, and is bound to an approval. The page that says *idempotency* precisely. | *idempotency keys AI agents* · *stripe idempotency key vs* | ## FAQ @@ -273,7 +273,7 @@ signature · what is not covered. Query: *ctrlrun faq* and each question verbati | Path | Purpose | Query | |---|---|---| -| `security/threat-model` | What CTRLRun defends against, what it does not, and the fail-closed rules that follow; renders `docs/THREAT_MODEL.md`. | *ctrlrun threat model* | +| `security/threat-model` | What ctrlrun defends against, what it does not, and the fail-closed rules that follow; renders `docs/THREAT_MODEL.md`. | *ctrlrun threat model* | | `docs/security/verify-guarantees` | The guarantee catalogue G1–G11, what each exercises, what N/A means, what verify cannot see. | *ctrlrun verify guarantees* | | `docs/security/receipt-chain` | Each receipt carries the hash of the one before; what the chain detects, what it does not prove, and the two statements that erase the end of the log. Alteration, not authorship. | *tamper evident audit log AI agent* | | `security/how-this-is-built` | Spec-first, every MUST mutation-tested with the real numbers, independent review sessions, CLAIMS.md, N/A is not a pass, AI coding agents used throughout with the constraints that make that safe, and what has not been done: no external audit yet. Session 1b. | *is ctrlrun trustworthy* · *how ctrlrun is tested* | @@ -283,7 +283,7 @@ signature · what is not covered. Query: *ctrlrun faq* and each question verbati | Path | Purpose | Query | |---|---|---| -| `architecture/overview` | The boundary CTRLRun owns, the canonical flow (normalize · decide · approve · reserve · execute · record), the module map; renders `docs/ARCHITECTURE.md`. | *ctrlrun architecture* | +| `architecture/overview` | The boundary ctrlrun owns, the canonical flow (normalize · decide · approve · reserve · execute · record), the module map; renders `docs/ARCHITECTURE.md`. | *ctrlrun architecture* | | `docs/architecture/specifications` | The six specifications, unchanged, with one line each on what the version asked; plus the OWASP and ACS readings. | *ctrlrun spec* | ## Changelog @@ -402,7 +402,7 @@ Two sources with opposite emphases, and the tree above follows both: What this means concretely for these pages, and what `STYLE.md` enforces: **answer-first first paragraphs** and one **definitional sentence** per Concepts page, written for a human; -**consistent entity naming** (CTRLRun, effect key, action hash, AMBIGUOUS); **comparison +**consistent entity naming** (ctrlrun, effect key, action hash, AMBIGUOUS); **comparison tables** on every Compare page; **FAQ structured data** on the FAQ page; quotable, plain claims with a `CLAIMS.md` row behind each; and the generated `llms.txt` left to Mintlify. What it does not mean: keyword density, chunked pages, or a second writing style for machines. diff --git a/README.md b/README.md index 9c50d6c..a6251a8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CTRLRun documentation +# ctrlrun documentation The source of [ctrlrun.dev](https://ctrlrun.dev): the pages, the tools that render them from the library's own source, and the tests that check them. @@ -124,7 +124,7 @@ sources to their destinations, which makes them self-redirects. The Medical Affairs workbench uses `medical-workbench.js`, styles scoped to `#cr-medical-workbench`, and the library's `examples/medical_workbench.py`. Keep its embedded `MODULE` equivalent to that Python source; `tests/test_medical_workbench.py` checks the copy -against the library checkout. The browser loads CTRLRun on demand. Evidence and synthesis are +against the library checkout. The browser loads ctrlrun on demand. Evidence and synthesis are synthetic; release decisions and receipts execute in Python. `assets/verify-medical-workbench.cjs` exercises browser Python, both downloads, error recovery and all six stages at three viewport widths. diff --git a/SEO.md b/SEO.md index f449404..a597001 100644 --- a/SEO.md +++ b/SEO.md @@ -17,7 +17,7 @@ by tests lives in `tests/test_docs_site.py` and `tests/test_docs_seo.py`. | Every page links to Why and to Get started | `test_every_page_links_to_why_and_to_get_started_or_is_one_of_them` | | Every page ends with Next links | `test_every_page_ends_with_next_links` | | The FAQ carries FAQ structured data whose questions match the page | `test_the_faq_structured_data_matches_the_page` | -| Consistent entity naming: CTRLRun, effect key, action hash, AMBIGUOUS | the forbidden-words lint and review | +| Consistent entity naming: ctrlrun, effect key, action hash, AMBIGUOUS | the forbidden-words lint and review | `llms.txt`, `llms-full.txt`, `sitemap.xml` and `robots.txt` are generated by Mintlify for every site; nothing here writes them. The Open Graph defaults and the social image are set once in @@ -38,10 +38,10 @@ that page's frontmatter, never here. | `docs` | ctrlrun · AI agent safety layer | The last check before an AI agent does something it can't undo. | | `docs/why` | why do AI agents double execute · AI agent consequential actions | Everyone is rushing to ship AI agents without thinking about consequences. | | `docs/not-only-agents` | celery task retried twice · webhook delivered twice duplicate · retry safe background job python | Every page on this site says agent, and the failure underneath them does not require one. | -| `docs/agents-you-cant-modify` | control AI agents you can't modify · whatsapp slack teams bot approval · claude code cursor codex mcp approval · chatgpt connector approval | CTRLRun works with agents you can't modify as well as the ones you can, because it checks the action, not the agent. | +| `docs/agents-you-cant-modify` | control AI agents you can't modify · whatsapp slack teams bot approval · claude code cursor codex mcp approval · chatgpt connector approval | ctrlrun works with agents you can't modify as well as the ones you can, because it checks the action, not the agent. | | `docs/get-started/install` | install ctrlrun | `pip install ctrlrun` installs the kernel and exactly two dependencies, `pyyaml` and `click`. | | `docs/get-started/quickstart` | protect an AI agent action python · ctrlrun quickstart | In sixty seconds you will write a policy, protect a refund function, and watch a mutated approval be refused. | -| `docs/get-started/three-ways-in` | do I need a ctrlrun adapter · ctrlrun langgraph | There are three ways to put CTRLRun in front of a consequential action, and only one of them is an adapter. | +| `docs/get-started/three-ways-in` | do I need a ctrlrun adapter · ctrlrun langgraph | There are three ways to put ctrlrun in front of a consequential action, and only one of them is an adapter. | | `docs/get-started/choosing` | ctrlrun decorator vs gateway | In-process Python takes the decorator, tools behind MCP take the gateway, and a framework with its own approval UI takes an adapter. | | `docs/production/index` | is ctrlrun production ready | SQLite is the default and is production-grade on one host; Postgres is for many hosts. | | `docs/production/postgres` | ctrlrun sqlite vs postgres | Choose by how many machines write to the store, not by how serious you are. | @@ -53,7 +53,7 @@ that page's frontmatter, never here. | `docs/production/retention` | delete old receipts hash chain | Prune a prefix of the receipt chain and still verify across the gap, or be refused. | | `docs/production/soak` | ctrlrun soak test results | One published run, its measured duration, and the exit criterion it does not meet. | | `docs/production/operations` | ctrlrun monitoring | Watch how many effects are sitting in an unknown outcome that nobody has answered. | -| `docs/mcp/overview` | MCP gateway human approval | CTRLRun works with MCP in four ways. | +| `docs/mcp/overview` | MCP gateway human approval | ctrlrun works with MCP in four ways. | | `docs/mcp/gateway-in-5-minutes` | protect MCP server · MCP tool call approval | Point the MCP client at `ctrlrun gateway` instead of the tool server. | | `docs/mcp/approve-from-your-assistant` | approve MCP tool call from an assistant · MCP human approval server | Run `ctrlrun mcp-operator` and answer a pending approval from an MCP client, under your own name. | | `docs/mcp/use-the-docs-from-your-editor` | ctrlrun docs mcp server | This documentation is an MCP server, hosted with the site. | @@ -61,11 +61,11 @@ that page's frontmatter, never here. | `docs/concepts/decisions` | AI agent action policy allow approve deny | A decision is what the policy says may happen to one action. | | `docs/concepts/approval-binding` | approval bound to action · approval mutation AI agent | An approval is a human's yes to one exact action, bound to that action's hash. | | `docs/concepts/effect-keys` | idempotency key AI agent · prevent duplicate tool execution | An effect key is the name of a consequence in the real world. | -| `docs/concepts/outcomes-and-ambiguous` | agent tool call timeout · double execution AI agent retry | An outcome is what CTRLRun knows about the consequence, and there are three. | +| `docs/concepts/outcomes-and-ambiguous` | agent tool call timeout · double execution AI agent retry | An outcome is what ctrlrun knows about the consequence, and there are three. | | `docs/concepts/receipts-and-evidence` | AI agent audit trail receipts | A receipt is the portable JSON record of one action that reached the executor. | | `docs/concepts/authority-and-delegation` | AI agent authorization delegation · least privilege AI agents | Authority answers the question the policy cannot: may this principal propose this action at all? | | `docs/concepts/observe-mode` | AI agent policy shadow mode | Observe mode is one top-level line that evaluates every action and executes it regardless. | -| `docs/concepts/fail-closed` | fail closed AI agent | Fail closed means that anything CTRLRun cannot decide, it denies. | +| `docs/concepts/fail-closed` | fail closed AI agent | Fail closed means that anything ctrlrun cannot decide, it denies. | | `docs/guides/protect-a-function` | protect python function approval | Decorate the function that acts, name the action and the consequence. | | `docs/guides/gateway-in-front-of-mcp` | MCP gateway policy | Point the MCP client at `ctrlrun gateway` instead of the tool server. | | `docs/guides/approvals-in-slack` | slack approval AI agent | `WebhookApprovalProvider` sends one signed POST to a URL you own for every approval request. | @@ -75,7 +75,7 @@ that page's frontmatter, never here. | `docs/guides/run-on-postgres` | ctrlrun postgres | Use Postgres when workers on more than one host must share one store. | | `docs/guides/verify-in-ci` | verify agent safety configuration CI | `ctrlrun verify` runs the kernel's own failure scenarios against your policy. | | `docs/guides/export-to-opentelemetry` | opentelemetry AI agent actions | `OTelEventSink` turns every action into one OpenTelemetry span. | -| `docs/guides/langchain-middleware` | langchain middleware tool call policy | CTRLRun checks every tool call your agent makes against a policy you write, before the call runs, and records what happened after. | +| `docs/guides/langchain-middleware` | langchain middleware tool call policy | ctrlrun checks every tool call your agent makes against a policy you write, before the call runs, and records what happened after. | | `docs/guides/langgraph-adapter` | langgraph interrupt human approval | `ctrlrun-langgraph` makes an `approve` decision surface as a LangGraph `interrupt()`. | | `docs/guides/openai-agents-adapter` | openai agents sdk tool approval | `ctrlrun-openai-agents` makes an `approve` decision stop the run with the SDK's own `ToolApprovalItem`. | | `docs/cookbook/index` | AI agent policy examples | Each recipe is a situation an agent is put in. | @@ -100,9 +100,9 @@ that page's frontmatter, never here. | `docs/cookbook/reconcile-against-the-remote` | reconcile agent effect stripe kubernetes | A hook asks the remote and moves the record only the way the answer points. | | `docs/cookbook/verify-in-github-actions` | verify agent policy CI | One workflow step proves the declared guarantees still hold against your policy. | | `docs/cookbook/sqlite-to-postgres` | share agent state across hosts | One line changes: the store, and the guarantee now holds across hosts. | -| `docs/compare/framework-hitl` | langgraph human in the loop limitations | A framework's human-in-the-loop primitive is the right place for a human to answer, and CTRLRun uses it rather than replacing it. | -| `docs/compare/guardrail-libraries` | AI guardrails vs execution control | A guardrail library reads text; CTRLRun sits one layer down, where a decision becomes an effect. | -| `docs/compare/governance-toolkits` | AI agent oversight vs enforcement | A toolkit describes; CTRLRun refuses. | +| `docs/compare/framework-hitl` | langgraph human in the loop limitations | A framework's human-in-the-loop primitive is the right place for a human to answer, and ctrlrun uses it rather than replacing it. | +| `docs/compare/guardrail-libraries` | AI guardrails vs execution control | A guardrail library reads text; ctrlrun sits one layer down, where a decision becomes an effect. | +| `docs/compare/governance-toolkits` | AI agent oversight vs enforcement | A toolkit describes; ctrlrun refuses. | | `docs/compare/durable-workflows` | temporal vs ctrlrun · durable execution agents | One drives work forward; the other decides whether the work may happen. | | `docs/compare/idempotency-keys` | idempotency keys AI agents · stripe idempotency vs | An idempotency key deduplicates at one API; an effect key deduplicates at the agent, across every API it touches. | | `docs/verify/get-the-badge` | ctrlrun verified badge | Two minutes, three steps: verify on every push, publish the badge JSON, point Shields at it. | @@ -112,17 +112,17 @@ that page's frontmatter, never here. | `docs/security/verify-guarantees` | ctrlrun verify guarantees | `ctrlrun verify` runs eleven guarantees against the configuration in front of it. | | `docs/security/assurance-case` | ctrlrun assurance case security requirements | An assurance case is the argument, with its evidence, that a system meets its security requirements. | | `docs/security/disclosure` | ctrlrun security report | Report vulnerabilities privately to contact@arpanghoshal.com. | -| `docs/how-this-is-built` | is ctrlrun trustworthy · how ctrlrun is tested | CTRLRun is built specification-first, every requirement in it is mutation-tested. | +| `docs/how-this-is-built` | is ctrlrun trustworthy · how ctrlrun is tested | ctrlrun is built specification-first, every requirement in it is mutation-tested. | | `docs/reference/policy-yaml` | ctrlrun.yaml reference · ctrlrun policy schema | `ctrlrun.yaml` is one document: a `schema`, an `actions` map, and from v3 the `mode`, `environment` and `docs/authority` keys. | | `docs/reference/authority-yaml` | ctrlrun authority grants yaml | The `authority:` section says which principal may propose which action. | | `docs/reference/cli` | ctrlrun cli | The `ctrlrun` command reads the policy in the working directory and the store beside it. | | `docs/reference/errors` | ctrlrun ApprovalMismatch · ctrlrun AmbiguousEffect | Every refusal is an exception of its own, raised as itself before the executor runs. | -| `docs/reference/exit-codes` | ctrlrun verify exit code | Every `ctrlrun` command exits 0 when it did what it was asked, 1 when CTRLRun refused, 2 on a usage error. | +| `docs/reference/exit-codes` | ctrlrun verify exit code | Every `ctrlrun` command exits 0 when it did what it was asked, 1 when ctrlrun refused, 2 on a usage error. | | `docs/reference/receipt-and-event-schemas` | ctrlrun receipt json schema | A receipt is one executed action; an event is one step on the way. | | `docs/reference/api/index` | ctrlrun Control · ctrlrun protect decorator | Every frozen public name of the package and its extras, one page each. | -| `docs/architecture/specifications` | ctrlrun specification | Every version of CTRLRun was a specification before it was code. | -| `docs/ARCHITECTURE` | ctrlrun architecture | The boundary CTRLRun owns, and the six steps every protected call takes. | -| `docs/THREAT_MODEL` | ctrlrun threat model | What CTRLRun defends against, and what it deliberately does not. | +| `docs/architecture/specifications` | ctrlrun specification | Every version of ctrlrun was a specification before it was code. | +| `docs/ARCHITECTURE` | ctrlrun architecture | The boundary ctrlrun owns, and the six steps every protected call takes. | +| `docs/THREAT_MODEL` | ctrlrun threat model | What ctrlrun defends against, and what it deliberately does not. | | `docs/how-this-is-built` | is ctrlrun trustworthy · how ctrlrun is tested | Specification first, every requirement mutation-tested, every claim mapped to a test. | | `docs/verify` | ctrlrun verify guarantees badge | Running the guarantee catalogue against your own configuration. | | `docs/adapters` | ctrlrun adapter langgraph openai | The three ways in, and when you do not need an adapter. | @@ -131,8 +131,8 @@ that page's frontmatter, never here. | `docs/CLAIMS` | ctrlrun claims tests | Every README sentence mapped to the code and the test that proves it. | | `docs/ROADMAP` | ctrlrun roadmap v1.0 | What each version asked and answered, and what is not on the list. | | `docs/ACS` | agent control standard ctrlrun | What was read, what maps, and where the standard is silent. | -| `docs/OWASP-AGENTIC-TOP10` | OWASP agentic top 10 mapping | A reading of somebody else's taxonomy against the guarantees CTRLRun tests. | -| `docs/OWASP-SOLUTIONS-LANDSCAPE` | OWASP agentic solutions landscape | Which boxes on somebody else's checklist CTRLRun ticks, and which it does not. | +| `docs/OWASP-AGENTIC-TOP10` | OWASP agentic top 10 mapping | A reading of somebody else's taxonomy against the guarantees ctrlrun tests. | +| `docs/OWASP-SOLUTIONS-LANDSCAPE` | OWASP agentic solutions landscape | Which boxes on somebody else's checklist ctrlrun ticks, and which it does not. | ## The words that appear once diff --git a/STYLE.md b/STYLE.md index 72334e7..40c9a2c 100644 --- a/STYLE.md +++ b/STYLE.md @@ -32,7 +32,7 @@ reviewer reads for. ## The words -- **CTRLRun**, always in that capitalisation. Never *Ctrlrun*, *ctrlrun* in prose, or *CTRL Run*. +- **ctrlrun**, always in that capitalisation. Never *Ctrlrun*, *ctrlrun* in prose, or *CTRL Run*. In code, the package and command are `ctrlrun`. - **The fixed copy** is fixed. The tagline, the principle, the category line, the promise and the opener are quoted from `IA.md` and are not paraphrased. @@ -54,7 +54,7 @@ reviewer reads for. understands it. It is never the only one. - **Real names, invented values.** `stripe.refund`, `k8s.delete_namespace`, `iam.grant_role`, `crm.update_record`, `email.send`. Amounts, ids and addresses are obviously invented. -- **The share unit is a failure.** An example shows an agent doing something wrong and CTRLRun +- **The share unit is a failure.** An example shows an agent doing something wrong and ctrlrun refusing. A list of features is not an example. ## The code blocks diff --git a/capabilities.yaml b/capabilities.yaml index 9c05641..97df3ec 100644 --- a/capabilities.yaml +++ b/capabilities.yaml @@ -167,7 +167,7 @@ capabilities: - id: identity name: Consumed identity - description: A principal comes from a verified header or JWT; CTRLRun issues nothing. + description: A principal comes from a verified header or JWT; ctrlrun issues nothing. guarantee: false ways_in: decorator: true @@ -175,7 +175,7 @@ capabilities: adapter: true since: v0.3 page: docs/concepts/authority-and-delegation - claim: CTRLRun issues no credential and defines no identity format + claim: ctrlrun issues no credential and defines no identity format - id: delegation name: Runtime delegation diff --git a/docs.json b/docs.json index 77e0c98..87e26ed 100644 --- a/docs.json +++ b/docs.json @@ -1,6 +1,6 @@ { "$schema": "https://mintlify.com/docs.json", - "name": "CTRLRun", + "name": "ctrlrun", "theme": "mint", "colors": { "primary": "#B8730A", @@ -13,7 +13,7 @@ "href": "/" }, "favicon": "/images/favicon.svg", - "description": "CTRLRun stops AI agents from taking wrong, restricted, or malicious actions in your workflows. Every action is checked against your rules before it runs.", + "description": "ctrlrun stops AI agents from taking wrong, restricted, or malicious actions in your workflows. Every action is checked against your rules before it runs.", "navbar": { "links": [ { @@ -359,7 +359,7 @@ }, "seo": { "metatags": { - "og:site_name": "CTRLRun", + "og:site_name": "ctrlrun", "og:image": "https://ctrlrun.dev/images/social-preview.png", "twitter:card": "summary_large_image", "twitter:image": "https://ctrlrun.dev/images/social-preview.png" diff --git a/docs/ACS.md b/docs/ACS.md index 86e7361..81bd70a 100644 --- a/docs/ACS.md +++ b/docs/ACS.md @@ -1,6 +1,6 @@ --- title: "The Agent Control Standard" -description: "What was read, what maps onto CTRLRun's guarantees, where the standard is silent, and how the adapter is built." +description: "What was read, what maps onto ctrlrun's guarantees, where the standard is silent, and how the adapter is built." --- What was read, what maps, what does not, and how the adapter is built. @@ -34,7 +34,7 @@ Two things the repository does **not** have at that commit, both of which shaped version-sync tooling. So there is nothing to conform *to* except the schemas, and this adapter is written against them directly. - **No `examples/` directory**, and so no house format for a community example. `examples/acs/` - therefore follows CTRLRun's own convention. + therefore follows ctrlrun's own convention. ## What ACS defines @@ -52,12 +52,12 @@ allow · deny · modify · ask · defer `deny` requires `reasoning`. `modify` requires `reasoning` and `modifications`. `ask` requires `reasoning` and `ask_details`. `defer` requires `reasoning` and `defer_details`. -Of the 22 hooks, CTRLRun answers **two**, and it is worth being explicit that it declines the +Of the 22 hooks, ctrlrun answers **two**, and it is worth being explicit that it declines the other twenty: `SessionStart`, `SessionEnd`, `AgentTrigger`, `TurnStart`, `TurnEnd`, `UserMessage`, `AgentResponse`, `KnowledgeRetrieval`, `MemoryContextRetrieval`, `MemoryStore`, `PreCompact`, `PostCompact`, `SubagentStart`, `SubagentStop`, `SkillRegister`, `SkillLoad`, `SkillUnload`, `SystemPing`, `AgbomSnapshot`, `AgbomChanged`. Those are checkpoints about what -the model is thinking, remembering or composed of. CTRLRun's product rule is that it decides +the model is thinking, remembering or composed of. ctrlrun's product rule is that it decides actions that can affect the real world, and nothing else — so an unanswered method returns a JSON-RPC error in ACS's reserved range rather than an opinion. @@ -65,7 +65,7 @@ JSON-RPC error in ACS's reserved range rather than an opinion. ### `steps/toolCallRequest` → build the Action, decide it, take the reservation -| ACS field | CTRLRun | +| ACS field | ctrlrun | |---|---| | `params.metadata.agent_id` | `Principal.agent` — **only where no `identity` provider is configured**. With one it is **ignored**: not merged, not a fallback, not compared (SPEC-v0.3 §8.4) | | `params.metadata.user_context.user_id` | `Principal.user`, under the same rule | @@ -114,7 +114,7 @@ to be able to say different things about `create` and `void`. The decision maps out: -| CTRLRun | ACS | +| ctrlrun | ACS | |---|---| | `ALLOW` | `allow` | | `DENY` | `deny` + `reasoning` + `reason_codes` | @@ -130,13 +130,13 @@ human answers with `ctrlrun approve`, and `timeout_seconds`. All three are requi `ask-details.json`. `ask_details.intent_extension` is **not** used. It grants capabilities for `this_request` or -`session`, which is an authority model — CTRLRun has none until v0.3, and a grant it cannot +`session`, which is an authority model — ctrlrun has none until v0.3, and a grant it cannot represent is one it must not claim to honour. ### `steps/toolCallResult` → close the reservation ACS describes this hook as *"fires after tool execution, before results reach the agent, -serving as an output redaction checkpoint"*. CTRLRun redacts nothing, so it always answers +serving as an output redaction checkpoint"*. ctrlrun redacts nothing, so it always answers `allow`; the work is the outcome it records. `exit_status` is `success | failure | timeout | blocked`. **ACS does not say what any of them @@ -165,7 +165,7 @@ what the Instrument layer was built for. `request_id_ref` links a result to its request. Neither identifies the *effect*: two calls that would refund the same payment get two unrelated UUIDs. There is no idempotency key, no `capability`-plus-argument identity, nothing an implementer could use to recognise that a retry -is a repeat. CTRLRun supplies one from the policy's `effect:` template. +is a repeat. ctrlrun supplies one from the policy's `effect:` template. **2. `exit_status` is a status of the call, not an outcome of the effect.** Four values, and the vocabulary itself carries the confusion: `timeout` sits alongside `failure` as though both @@ -177,13 +177,13 @@ retry is unsafe until a human resolves it". approver. What comes back is a decision on *the request*, and `intent_extension` can widen a capability for the session. Neither pins the approval to a canonical form of the arguments, so nothing in ACS prevents an agent from getting `refund(2000)` approved and then calling -`refund(5000)`. CTRLRun binds to `action_hash`, which covers the principal, the arguments, the +`refund(5000)`. ctrlrun binds to `action_hash`, which covers the principal, the arguments, the resource and the environment. **4. No terminal unknown state.** ACS's decisions are about the future of a call. There is no way for a Guardian to record that an effect's outcome is unresolved and that *no* further call on that effect may proceed until a human says which way it went. `AMBIGUOUS` has no ACS -counterpart, and it is the state most of CTRLRun's design exists to protect. +counterpart, and it is the state most of ctrlrun's design exists to protect. **5. The Guardian does not execute.** ACS is advisory by construction — the platform runs the tool. That is a reasonable separation, but it means a Guardian cannot make reserve-and-execute @@ -196,7 +196,7 @@ to lease-expire into `AMBIGUOUS` by the ordinary path of v0.1 §5.3 E3. `ctrlrun.acs.AcsControlHook`, in `ctrlrun[gateway]` — it needs no new dependency, and it is kept out of core for the same reason the gateway is: `import ctrlrun` must not grow. -The shape is forced by the seam above. ACS is advisory and CTRLRun is executing, so one action +The shape is forced by the seam above. ACS is advisory and ctrlrun is executing, so one action is split across two hooks and the reservation is held between them. That is exactly the shape `Suspended` and `Control.resume` were built for in SPEC-v0.2 §6.9 — a reservation held across a round trip the kernel does not control — so the adapter reuses them rather than reaching for @@ -220,7 +220,7 @@ steps/toolCallResult `Control` remains the only module that composes the others (ARCHITECTURE §6). The adapter translates two vocabularies and decides nothing. -**What it does not do.** It does not use `modify` — CTRLRun refuses or permits an action as +**What it does not do.** It does not use `modify` — ctrlrun refuses or permits an action as proposed, and rewriting an agent's arguments is a different product. It does not use `defer`. It does not answer the other twenty hooks. It makes no claim of conformance: the schemas are `v0.1.0`, the repository is a public preview, there is no reference implementation to test diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 9636190..40c32ec 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,20 +1,20 @@ --- title: "Architecture" -description: "The boundary CTRLRun owns, the six steps every protected call takes, the data model, and the key design decisions with their trade-offs." +description: "The boundary ctrlrun owns, the six steps every protected call takes, the data model, and the key design decisions with their trade-offs." --- -Context: agent frameworks model work as `model → tool call → response`. That is fine for reads. For writes it is missing the semantics every serious system has around consequential operations: authorization bound to the exact operation, identity of the effect (not the request), atomic reservation, and an honest distinction between *failed* and *unknown*. CTRLRun adds those semantics around the dangerous part and nothing else. +Context: agent frameworks model work as `model → tool call → response`. That is fine for reads. For writes it is missing the semantics every serious system has around consequential operations: authorization bound to the exact operation, identity of the effect (not the request), atomic reservation, and an honest distinction between *failed* and *unknown*. ctrlrun adds those semantics around the dangerous part and nothing else. The contract is in [`SPEC-v0.1.md`](https://github.com/CTRLRun/ctrlrun/blob/main/docs/SPEC-v0.1.md). This document explains the shape and the reasoning. -## 1. The boundary CTRLRun owns +## 1. The boundary ctrlrun owns ``` Agent reasoning (not ours) │ "I want to do X" ▼ ┌──────────────────────────────┐ -│ CTRLRun │ +│ ctrlrun │ │ normalize → decide → │ │ approve → reserve → │ │ execute → resolve → record │ @@ -24,7 +24,7 @@ Agent reasoning (not ours) Real-world effect (not ours either) ``` -CTRLRun sits between *intention* and *consequence*. It does not sit between prompt and model. Everything upstream (planning, prompting, retrieval, memory) and everything downstream (the remote system's own semantics) is out of scope. +ctrlrun sits between *intention* and *consequence*. It does not sit between prompt and model. Everything upstream (planning, prompting, retrieval, memory) and everything downstream (the remote system's own semantics) is out of scope. ## 2. Canonical flow @@ -87,7 +87,7 @@ A retry is a new proposal (`action_id`) for the same logical effect (`effect_key *Trade-off:* the developer has to declare the key. We make that one decorator argument and fail loudly on a bad template rather than silently degrading. ### 4.4 AMBIGUOUS is a first-class terminal state -A timeout after a request was sent is not a failure. The remote may have committed. Frameworks that map timeout → failed → retry are how double refunds happen. CTRLRun refuses to guess: `AMBIGUOUS` blocks retries until a human resolves it. +A timeout after a request was sent is not a failure. The remote may have committed. Frameworks that map timeout → failed → retry are how double refunds happen. ctrlrun refuses to guess: `AMBIGUOUS` blocks retries until a human resolves it. *Trade-off:* this creates operational work (someone must run `ctrlrun resolve`). That is the correct place for the work to land. v0.2 adds a `reconcile` hook (`SPEC-v0.2.md` §2) for executors that can ask the remote what happened: it is the second — and only other — authority permitted to move a record out of `AMBIGUOUS`, and only where its answer points. An answer it cannot give is `"unknown"`, which changes nothing. diff --git a/docs/CLAIMS.md b/docs/CLAIMS.md index b6cb5df..c034a9d 100644 --- a/docs/CLAIMS.md +++ b/docs/CLAIMS.md @@ -20,15 +20,15 @@ by its quoted claim, and `tests/test_docs_audit.py` fails if a named row is not > The last check before an AI agent does something it can't undo. Autonomy belongs to the > action, not the agent. 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. +> leaves a receipt — and when the outcome is unknown, ctrlrun says so instead of guessing. > A Python library that sits between the decision to act and the call that acts. | Claim | Code | Proof | |---|---|---| | "The last check before an AI agent does something it can't undo." | `Control.execute` — `control.py:1318` — resolves the principal, evaluates authority and policy, consumes the approval and reserves the effect key **before** the executor runs; nothing in the wrapper calls the function first | `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote`, `test_T3_the_fake_remote_is_called_exactly_once` | | "Autonomy belongs to the action, not the agent." | `Policy.evaluate(action)` — `policy.py:449` — passes only the action's **name and arguments** to `_ActionPolicy.evaluate` (`policy.py:449`), whose signature has no principal in it. A rule cannot read who is acting even by accident. `agent_eq` and `user_eq` are refused at load by `RESERVED_ARGUMENTS` (`policy.py:449`) rather than silently matching nothing. | `test_T6_an_action_name_is_matched_exactly`, `test_a_condition_naming_an_action_field_is_refused_at_load` | -| "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." | At most once: `plan_reservation` — `effect.py:250`. Exactly as approved: the approval is bound to `action_hash` and consumed with the reservation — `_authorize_and_reserve` — `state.py:1341`. Or not at all: a refusal raises before the executor — `Control.execute` — `control.py:1318`. Says so instead of guessing: only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2223` — and everything else is `AMBIGUOUS`. A receipt: `Receipt` — `receipt.py:256`. **This sentence read *happens once … or not at all* until 0.6**, a two-way disjunction that excluded the third outcome the product exists for: a lost reply is neither, and the README's own first section says so. | `test_T3_exactly_one_agent_reserves_and_seven_are_blocked`, `test_T2_a_mutated_action_presenting_the_approval_raises_ApprovalMismatch`, `test_T1_a_lost_response_leaves_the_effect_ambiguous`, `test_T11_every_demo_receipt_carries_every_field_in_the_spec` | -| "A Python library that sits between the decision to act and the call that acts." | `@protect` — `control.py` — wraps the callable that acts, and `Control.execute` runs every check before invoking it. The category noun was on `docs.mdx` and in `pyproject.toml`'s `description` and nowhere in the README until 0.6, so a reader had to infer what CTRLRun **is** from three slogans. | `test_the_header_carries_the_fixed_copy_and_the_five_badges`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote` | +| "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." | At most once: `plan_reservation` — `effect.py:250`. Exactly as approved: the approval is bound to `action_hash` and consumed with the reservation — `_authorize_and_reserve` — `state.py:1341`. Or not at all: a refusal raises before the executor — `Control.execute` — `control.py:1318`. Says so instead of guessing: only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:2223` — and everything else is `AMBIGUOUS`. A receipt: `Receipt` — `receipt.py:256`. **This sentence read *happens once … or not at all* until 0.6**, a two-way disjunction that excluded the third outcome the product exists for: a lost reply is neither, and the README's own first section says so. | `test_T3_exactly_one_agent_reserves_and_seven_are_blocked`, `test_T2_a_mutated_action_presenting_the_approval_raises_ApprovalMismatch`, `test_T1_a_lost_response_leaves_the_effect_ambiguous`, `test_T11_every_demo_receipt_carries_every_field_in_the_spec` | +| "A Python library that sits between the decision to act and the call that acts." | `@protect` — `control.py` — wraps the callable that acts, and `Control.execute` runs every check before invoking it. The category noun was on `docs.mdx` and in `pyproject.toml`'s `description` and nowhere in the README until 0.6, so a reader had to infer what ctrlrun **is** from three slogans. | `test_the_header_carries_the_fixed_copy_and_the_five_badges`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote` | | "Runs in production on a single file, or on Postgres across hosts" | SQLite: `SQLiteStateStore` reserves inside the `BEGIN IMMEDIATE` of `_authorize_and_reserve` — `state.py:1535` — which is a write lock on the file and holds across OS processes. Postgres: `PostgresStateStore` over `UNIQUE(effect_key)` with `INSERT … ON CONFLICT DO NOTHING` and checked row counts (SPEC-v0.6 §4.2), the same `StateStore` protocol, extended by nothing | `test_T3_exactly_one_agent_reserves_and_seven_are_blocked` (8 OS processes, both backends), `test_T141_the_shipped_backends_pass`, `test_T154_postgres_passes_the_store_conformance_suite` | ## The refund that happened twice @@ -36,7 +36,7 @@ by its quoted claim, and `tests/test_docs_audit.py` fails if a named row is not | Claim | Code | Proof | |---|---|---| | "A lost reply is `AMBIGUOUS`, never `FAILED`, and a retry against an `AMBIGUOUS` effect is refused — until a human, or a `reconcile` hook, says what happened." | Only `NotExecuted` maps to `FAILED` — `_outcome` — `control.py:1325`; a retry against an `AMBIGUOUS` key is refused by `plan_reservation` — `effect.py:250`; the two things permitted to move the record on and nothing else — `resolve` — `cli/main.py:1024` — and `Control._reconciled` — `control.py:2877` | `test_T1_a_lost_response_leaves_the_effect_ambiguous`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote`, `test_T160_there_is_no_reaper`, `test_T13_a_hook_answering_not_executed_moves_the_record_to_failed` | -| "The customer is refunded twice, and nothing in the stack noticed." — said of a stack without CTRLRun; the demo runs the same sequence with it, and counts the calls the remote received | `ctrlrun demo` scenario 1, which retries against a fake remote that counts its calls and prints the count | `test_T3_the_fake_remote_is_called_exactly_once`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote` | +| "The customer is refunded twice, and nothing in the stack noticed." — said of a stack without ctrlrun; the demo runs the same sequence with it, and counts the calls the remote received | `ctrlrun demo` scenario 1, which retries against a fake remote that counts its calls and prints the count | `test_T3_the_fake_remote_is_called_exactly_once`, `test_T1_a_blind_retry_is_refused_and_never_reaches_the_remote` | ## Protect your first action @@ -164,7 +164,7 @@ catalogue, `GUARANTEES` (`verify/guarantees.py:55`). | "verifies a bearer token against a JWKS or a pinned key" | `JWTIdentityProvider._verified` — `jwt_identity.py:215`; the algorithm comes from the configured list and never from the token | `test_T88_a_valid_token_becomes_a_principal`, `test_T89_every_invalid_token_is_refused_by_cause` | | "maps the verified claims onto a principal" | `_principal` — `jwt_identity.py` — copies only the claims named in `claim_names` | `test_T88_only_the_named_claims_reach_the_principal` | | "`pip install \"ctrlrun[identity]\"`" | `identity = ["pyjwt[crypto]>=2.8"]` in `pyproject.toml`; imported lazily by `_jwt()` — `jwt_identity.py` | `test_T92_constructing_without_the_extra_names_the_install_command`, `test_T92_importing_ctrlrun_pulls_in_no_jwt_module` | -| "CTRLRun issues no credential and defines no identity format" | There is no minting, signing or issuing code path in the package: `jwt_identity.py` calls `decode` and never `encode` | `test_the_package_never_encodes_a_token` | +| "ctrlrun issues no credential and defines no identity format" | There is no minting, signing or issuing code path in the package: `jwt_identity.py` calls `decode` and never `encode` | `test_the_package_never_encodes_a_token` | | "every receipt records which policy decided it" | `Policy.policy_hash` — `policy.py:513`, over `_canonical_policy` — `policy.py:761`; carried into the receipt by `_record` — `control.py:4742` | `test_T172_every_receipt_carries_the_hash_and_the_declared_version`, `test_T172_two_policies_sharing_a_version_string_are_told_apart_by_the_hash` | | "the policy's declared `version:` and a hash of its canonical content" | `version:` is recorded and never authoritative; `policy_hash` is what tells two documents apart — `policy.py:503` | `test_T171_the_declared_version_alone_does_not_change_the_hash`, `test_T171_comments_key_order_and_whitespace_do_not_change_the_hash` | | "the approval is re-checked against the policy in force at execution" | `Control.execute` — `control.py:1318`; `_spend_unneeded_approval` — `control.py:2817` | `test_T173_the_DENY_row_refuses_and_leaves_the_approval_granted`, `test_T173_the_ALLOW_row_invalidates_the_approval_it_did_not_need` | @@ -186,13 +186,13 @@ The README also makes negative claims. They matter as much as the positive ones. | Claim | Where it holds | |---|---| -| "CTRLRun cannot guarantee exactly-once execution against external systems it doesn't control." | Stated, not implemented — see `THREAT_MODEL.md`, "Out of scope". CTRLRun never asserts what a remote did; only `NotExecuted`, raised by the executor, claims that. | -| "CTRLRun is not a transaction manager: it rolls nothing back" | There is no compensation, saga or rollback code path in the package; an `AMBIGUOUS` effect is resolved by a human or a reconcile hook and never undone — `RECONCILED_STATES` — `effect.py` | +| "ctrlrun cannot guarantee exactly-once execution against external systems it doesn't control." | Stated, not implemented — see `THREAT_MODEL.md`, "Out of scope". ctrlrun never asserts what a remote did; only `NotExecuted`, raised by the executor, claims that. | +| "ctrlrun is not a transaction manager: it rolls nothing back" | There is no compensation, saga or rollback code path in the package; an `AMBIGUOUS` effect is resolved by a human or a reconcile hook and never undone — `RECONCILED_STATES` — `effect.py` | | "The receipt chain detects alteration, and alteration is not authorship." | n/a — a disclaimer, and the scan that keeps it one: `test_T180_the_release_documents_do_not_blur_alteration_and_authorship` | | "erasing the end of the log costs two statements" | No code — this is what the chain does **not** cover, and it is asserted rather than argued: `test_erasing_a_suffix_and_rewinding_the_head_is_two_statements_and_undetected` | -| "CTRLRun does not detect prompt injection" | No code — and that is the point. Nothing in the package reads the agent's instructions: `Policy.evaluate` takes the action's name and arguments (`policy.py:449`) and `Authority` matches a grant against the action, so neither axis has the prompt to inspect. The README's problem table claims containment of the consequence, and this row is the sentence that stops it being read as detection. | `test_T6_an_action_name_is_matched_exactly`, `test_a_condition_naming_an_action_field_is_refused_at_load` | +| "ctrlrun does not detect prompt injection" | No code — and that is the point. Nothing in the package reads the agent's instructions: `Policy.evaluate` takes the action's name and arguments (`policy.py:449`) and `Authority` matches a grant against the action, so neither axis has the prompt to inspect. The README's problem table claims containment of the consequence, and this row is the sentence that stops it being read as detection. | `test_T6_an_action_name_is_matched_exactly`, `test_a_condition_naming_an_action_field_is_refused_at_load` | | "`ctrlrun verify` cannot see your executors" | `docs/verify.md`, "What it does not mean"; `THREAT_MODEL.md`, "Known v0.4 limitations" | -| "`ctrlrun scan` … reports the consequential call sites and policy entries CTRLRun is **not** covering" and "has no score, no percentage and no badge" | `ctrlrun/scan/` reads the tree with `ast` and never imports it, resolves no principal, evaluates no policy and opens no store (SPEC-scan §9.2); the limits sentence is emitted on every run including a clean one, and no percentage is computed anywhere. **`--coverage` opens a store and still computes none**: `ctrlrun/coverage.py` reports a list with a reason per entry, carries no `score`, `percentage` or `ratio` field, and does not move the exit code (SPEC-v0.11 §7, rule 4) | `test_T194_scan_never_imports_the_tree_it_reads`, `test_T205_scan_resolves_no_principal_evaluates_no_policy_and_opens_no_store`, `test_T203_the_limits_sentence_is_in_every_run_including_a_clean_one`, `test_T560_the_report_is_a_list_and_never_a_score`, `test_T563_the_coverage_flag_does_not_move_the_exit_code` | +| "`ctrlrun scan` … reports the consequential call sites and policy entries ctrlrun is **not** covering" and "has no score, no percentage and no badge" | `ctrlrun/scan/` reads the tree with `ast` and never imports it, resolves no principal, evaluates no policy and opens no store (SPEC-scan §9.2); the limits sentence is emitted on every run including a clean one, and no percentage is computed anywhere. **`--coverage` opens a store and still computes none**: `ctrlrun/coverage.py` reports a list with a reason per entry, carries no `score`, `percentage` or `ratio` field, and does not move the exit code (SPEC-v0.11 §7, rule 4) | `test_T194_scan_never_imports_the_tree_it_reads`, `test_T205_scan_resolves_no_principal_evaluates_no_policy_and_opens_no_store`, `test_T203_the_limits_sentence_is_in_every_run_including_a_clean_one`, `test_T560_the_report_is_a_list_and_never_a_score`, `test_T563_the_coverage_flag_does_not_move_the_exit_code` | | "`ctrlrun mcp-operator` … It authenticates who answered and records it; it does not check that they were entitled to." | the write tools refuse without a principal and attribute the answer to the verified one; there is no entitlement check, and `docs/SPEC-mcp-operator.md` §10 says so | `test_T184_approve_refuses_without_a_principal`, `test_T184_approve_succeeds_with_one_and_is_attributed`, `test_T183_there_is_no_flag_that_permits_a_remote_bind` | | "it makes no claim about any standard" | No standards vocabulary outside a sentence that negates it, in the README, in a docstring or in CLI output: `test_T139_the_readme_makes_no_conformance_claim`, and `tools/docs_audit/lint.py` on every document | diff --git a/docs/OWASP-AGENTIC-TOP10.md b/docs/OWASP-AGENTIC-TOP10.md index 379de78..fd659b1 100644 --- a/docs/OWASP-AGENTIC-TOP10.md +++ b/docs/OWASP-AGENTIC-TOP10.md @@ -1,13 +1,13 @@ --- title: "OWASP Top 10 for Agentic Applications" -description: "A reading of somebody else's taxonomy against the guarantees CTRLRun tests, naming the four entries it does not address." +description: "A reading of somebody else's taxonomy against the guarantees ctrlrun tests, naming the four entries it does not address." sidebarTitle: "OWASP Agentic Top 10" --- -This is a **reading** of somebody else's taxonomy against the guarantees CTRLRun tests. It is -not a compliance claim, a conformance claim, a certification, or a statement that CTRLRun +This is a **reading** of somebody else's taxonomy against the guarantees ctrlrun tests. It is +not a compliance claim, a conformance claim, a certification, or a statement that ctrlrun covers the OWASP Top 10 for Agentic Applications. Two of the ten entries are not addressed -by CTRLRun at all, and they are listed by name below. It was three until v0.10, which put +by ctrlrun at all, and they are listed by name below. It was three until v0.10, which put authority across an agent hop and moved `ASI07` into the partial half. Every row maps a guarantee to an entry, and every guarantee is backed by a passing acceptance @@ -80,7 +80,7 @@ mechanism, not the entry. | **G14** token changes across a renewal | `ctrlrun.idempotency_token()` answers inside an executor with a token derived from `(effect_key, attempt)`: stable within one attempt, including across a resume, and different after a renewal. Send it to a provider as its idempotency key. | `ASI08:2026` (partly) | A provider handed the effect key alone would answer the one retry the kernel permits, permitted *because the executor proved nothing happened*, with the cached failure of the attempt that failed. A token that moves with the attempt keeps a provider's cache from becoming a second source of stale outcomes. What it is for is reconciliation, a deterministic handle to ask a provider what became of an attempt whose outcome is unknown; it does not make a retry safe, and after `AMBIGUOUS` the kernel still refuses one. It is unique only as far as the operator's effect keys are, and nothing here checks two stores sharing a provider account. | | **G15** renewal past the ceiling refused | An action entry may declare `max_attempts`; above it the executor is not called, the record is released as `FAILED`, a `blocked` receipt names the ceiling, and `ActionDenied(reason="attempt_ceiling")` is raised. The refused attempt number is spent. | `ASI08:2026` (partly), `ASI10:2026` (partly) | Without a ceiling a renewal after `FAILED` is unbounded, so an agent that keeps proposing an action that keeps failing keeps dispatching, and one human yes on an `APPROVE` action bought unlimited dispatches. The ceiling is the operator's, not the kernel's: an entry that declares none renews exactly as before, and the decision is taken on the attempt number the store assigned, after the reservation, so two callers cannot both pass a read taken before reserving. It bounds attempts on one effect key, not what an agent does across many. | | **G16** a moved fingerprint is refused | Under `APPROVE`, a `preconditions=` provider's answer is kept as a `sha256:` fingerprint on the request; on the presenting pass it is computed again, strictly before the store call that consumes the approval, and a difference is refused with `ApprovalMismatch(reason="precondition_changed")`, reserving nothing and leaving the approval granted. | `ASI09:2026` (partly), `ASI01:2026` (partly) | A human's yes was given against the world as it was; this refuses the action where the state the approval depended on has moved since. It **narrows** the window between approval and execution and does not close it: the comparison is a network call outside the atomic reservation write, and a change that lands between the comparison and the reservation is not refused. What the provider looks at is in the operator's code, which verify does not read; verify grades the check with a provider of its own. | -| **G17** an unentitled approver refused | A control in the registry may name the role that answers for it. An approval whose **recorded** entitlement does not cover the roles the request pinned is refused with `ApprovalMismatch(reason="approver_unentitled")`, naming the control and the role in the message, the exception and the `APPROVAL_INVALIDATED` event. Roles are matched byte for byte, and every cited control must be satisfied. | `ASI03:2026` (partly), `ASI09:2026` (partly) | Before this, any principal whose credential the provider verified could answer any request: a deployment could say *who may act* and not *who may permit*. What it closes is narrower than it sounds and is stated as such. The kernel refuses an approval whose **recorded** entitlement does not cover the role; what entitled it was decided where the credential was verified, which is the operator MCP server or an embedding application. CTRLRun does not interpret the role, does not check that it exists anywhere, and makes no compliance claim on the strength of one. A control naming no role gates nobody, and a principal whose claims lack the role is not entitled: two omissions that mean opposite things. | +| **G17** an unentitled approver refused | A control in the registry may name the role that answers for it. An approval whose **recorded** entitlement does not cover the roles the request pinned is refused with `ApprovalMismatch(reason="approver_unentitled")`, naming the control and the role in the message, the exception and the `APPROVAL_INVALIDATED` event. Roles are matched byte for byte, and every cited control must be satisfied. | `ASI03:2026` (partly), `ASI09:2026` (partly) | Before this, any principal whose credential the provider verified could answer any request: a deployment could say *who may act* and not *who may permit*. What it closes is narrower than it sounds and is stated as such. The kernel refuses an approval whose **recorded** entitlement does not cover the role; what entitled it was decided where the credential was verified, which is the operator MCP server or an embedding application. ctrlrun does not interpret the role, does not check that it exists anywhere, and makes no compliance claim on the strength of one. A control naming no role gates nobody, and a principal whose claims lack the role is not entitled: two omissions that mean opposite things. | | **G18** the requester cannot approve | The principal that requested an action cannot be the principal that approves it, compared on the **resolved principal** and never on the approver string, so two grants whose strings differ are still one principal. Refused with `ApprovalMismatch(reason="approver_is_requester")`, leaving the approval granted and reserving nothing. | `ASI03:2026` (partly), `ASI09:2026` (partly) | An agent that could approve its own action has an approval gate in name only. The comparison is on the principal an identity provider verified, which is what makes it real: `approver` was a free string until v0.8, and a check against it would have been defeated by typing a different one. It does not establish that the approver understood what they approved: a persuaded human gives a valid approval and the receipt records it as one, and no page here says otherwise. | | **G19** one principal counts once | An action entry may require several approvals. The threshold counts **distinct verified principals**: a second yes from a principal that already answered is recorded, moves that entry's timestamp, and does not move the count. The count is decided inside the store's own write on all three backends, never by a read followed by a write. | `ASI09:2026` (partly), `ASI03:2026` (partly) | M-of-N is worth nothing if one person can be N of it. What this closes is a miscount: two processes answering at the same instant produce two approvers or one, never a threshold reached twice, and the test that proves it opens the window between the count's read and its write rather than starting two processes and hoping. A threshold above one in a deployment that verifies nobody is a denial, not a silent downgrade to one approval. It does not make several humans independent of each other, and it does not know whether they discussed it. | | **G20** revoked before its exp: no | Where a deployment configures a revocation feed, Security Event Tokens are consumed and a credential the issuer revoked is refused **at resolution**, before its `exp`, as an `IdentityError`. The match is against the token's own `iss`, `sub` and `jti` and never against the principal's agent name. | `ASI03:2026` (partly), `ASI10:2026` (partly) | Until v0.8 a verified token was valid until it expired, so a compromised credential stayed good for the rest of its lifetime and short lifetimes were the whole of the answer. Two things this closes less than it sounds, and both are stated wherever the feature is described. A revoked credential leaves a **log line and no receipt**: resolution happens before an action exists, where an expired credential leaves a full receipt. And a feed is worth what its source is worth: whoever can write it can refuse the operator's own agents, which is a denial of service against them and is fail-closed. What they cannot do is admit a principal the issuer revoked, because the feed is only ever consulted to refuse. | @@ -91,7 +91,7 @@ mechanism, not the entry. | **G25** a hop narrows or it is refused | Authority handed to a second agent is a **subset** of what the first agent held, on every dimension, checked when the hop is created and again at every evaluation under it. An action proposed under a hop is decided against **that hop's grant alone**, with no fallback to anything else the receiving agent holds, so a hop can only ever narrow. | `ASI07:2026` (partly), `ASI03:2026` (partly), `ASI10:2026` (partly) | Before v0.10 a second agent acted under its own grants and the first agent's limits were a convention. Delegation existed but stopped at the process. What this does **not** do is compel a receiving agent to present the hop it was given: an agent that holds a root grant of its own can act under that instead, and the deployment rule that closes it is that an agent which only ever acts on handed-over work holds no root grant. `ctrlrun scan` names the principals that do. | | **G26** a hop is named on both sides | The receipt of an action taken under a hop names the hop, and so does the record of the hop's creation, so an action and the delegation that authorised it are joined from either end without inference. | `ASI07:2026` (partly), `ASI10:2026` (partly), `ASI03:2026` (partly) | Attribution across a hop was reconstruction before this: a reader had to match timestamps and principals and hope. It is evidence, not prevention, and it is on the receipt rather than in a log that can be rotated away. A hop created *inside* an action is not named on its creator's receipt; the `DELEGATION_CREATED` event carries the `action_id` and holds the join instead. | | **G27** a swapped upstream is denied | An action entry may pin the upstream it authorises, by the SHA-256 of the server's leaf certificate or by the hash of a tool's advertised schema. A server that is not the pinned one, or a tool whose schema moved under an approved action name, is refused `upstream_mismatch`; an upstream nothing observed is refused `upstream_unverified` and never admitted. | `ASI02:2026` (partly), `ASI07:2026` (partly) | An approved action name is a name, and until v0.10 nothing checked that the thing answering to it was the thing that was approved. Enforced by `ctrlrun gateway`, the surface that holds the connection: at startup, at the decision, and at the TLS handshake. In-process there is no upstream to observe, so a pinned action refuses on every call, which is fail-closed and is why `ctrlrun verify` skips a pinned action unless a scenario asks for it by name. This is one slice of a supply chain and not the category: nothing here inspects a package, a model, a build or a signature chain. | -| **G28** truncation past an anchor fails | The chain's head is a row in the same database, so erasing the end of the log and updating that row is two statements and the chain reports itself intact. An anchor records the pair the head holds (`seq` and the hash at it) through a provider **you** supply, outside the store, and anything at or below an anchored `seq` can then no longer be removed or altered without the anchored pair failing to reproduce. Reported as `anchor_broken`, `anchor_missing` or `anchor_repudiated`, in the anchor's own report. | `ASI09:2026` (partly) | **An anchor freezes a prefix, and the limits are the point.** An **append is not detected**: a forged receipt lands above every anchored `seq`, so nothing stops reproducing and the next anchor freezes it like any other. Receipts written and erased entirely between two anchors are not detected either. It is **not a signature** and says nothing about who wrote the log, and an administrator who rewrites everything before the next anchor is still out of scope. The window you are exposed to is `(last anchored seq, current head]`, and its size is your choice of interval: that is the number to tune, and the number to quote instead of any sentence about tamper-evidence. CTRLRun ships **no** anchor provider, because an RFC 3161 client is a network client; the anchor is worth exactly what the record you point it at is worth, and one in the same directory as the database is worth nothing. | +| **G28** truncation past an anchor fails | The chain's head is a row in the same database, so erasing the end of the log and updating that row is two statements and the chain reports itself intact. An anchor records the pair the head holds (`seq` and the hash at it) through a provider **you** supply, outside the store, and anything at or below an anchored `seq` can then no longer be removed or altered without the anchored pair failing to reproduce. Reported as `anchor_broken`, `anchor_missing` or `anchor_repudiated`, in the anchor's own report. | `ASI09:2026` (partly) | **An anchor freezes a prefix, and the limits are the point.** An **append is not detected**: a forged receipt lands above every anchored `seq`, so nothing stops reproducing and the next anchor freezes it like any other. Receipts written and erased entirely between two anchors are not detected either. It is **not a signature** and says nothing about who wrote the log, and an administrator who rewrites everything before the next anchor is still out of scope. The window you are exposed to is `(last anchored seq, current head]`, and its size is your choice of interval: that is the number to tune, and the number to quote instead of any sentence about tamper-evidence. ctrlrun ships **no** anchor provider, because an RFC 3161 client is a network client; the anchor is worth exactly what the record you point it at is worth, and one in the same directory as the database is worth nothing. | | **G29** a prune adds no new chain break | Receipts accumulate, and deleting them breaks the chain by design. `ctrlrun prune` removes a **prefix** and leaves a checkpoint the reader seeds from, so the chain verifies **across** the gap. It refuses rather than warns: a prune that would leave a `(kind, seq)` pair the store did not already report, one through the head, one moving the checkpoint backwards, or one deleting a budget ledger row whose charge is still held. There is no `--force`. | `ASI09:2026` (partly) | Retention and evidence pull against each other, and the honest answer is that a prune is the only operation here that **destroys** evidence: what it deletes is gone. What this makes true is that the deletion is bounded and visible rather than silent, and that a deletion nobody can verify around is **refused** rather than completed with a warning. The rule is a **delta** and not a promise that the chain verifies afterwards: a store carrying `unchained` rows from before v0.6 can still be pruned, because the alternative is retention being impossible on exactly the oldest stores. It does not prune events, approvals, delegations or continuations, and nothing runs on a schedule. | | **G30** a held range refuses to prune | `ctrlrun hold place` names a range of receipts and a reason, and any prune overlapping it is refused with the hold named. | `ASI09:2026` (partly) | The case this is for is a legal hold arriving in the middle of a retention schedule, and the failure it prevents is a scheduled job quietly deleting what somebody has just been told to keep. **A hold has no expiry**: one that lapsed on a timer would release evidence on a schedule nobody reviewed, so a person places it and a person ends it. What it does not do is stop anyone with database access from deleting rows directly; it binds `ctrlrun prune`, not `DELETE`. | | **G32** an honest prune keeps anchors | A prune anchors its checkpoint **before** it deletes anything, and an anchor at or below an anchored checkpoint is then **superseded** rather than broken. | `ASI09:2026` (partly) | Without this the two features cancel: anchoring hourly and pruning at ninety days, every anchor older than the retention window would be permanently `anchor_broken`, and a deployment would have to choose between pruning and a permanent tamper signal. The half that stops *superseded* becoming a hole is that the checkpoint must itself be anchored, through the provider, which is outside the store: an attacker who erases a prefix and writes a checkpoint to explain it has to leave a record of the prune in the operator's own anchor history. **A prune stays visible even though the receipts are gone**, which is the whole of what retention owes evidence. | @@ -99,24 +99,24 @@ mechanism, not the entry. --- -## Not covered by CTRLRun +## Not covered by ctrlrun The half that makes the table above credible. One honest sentence each; nothing aspirational. | Entry | Title | Why not | |---|---|---| -| `ASI04:2026` | Agentic Supply Chain Vulnerabilities | **Out of scope, and no guarantee maps to it.** CTRLRun never inspects a package, a model, a build, a registry or a signature chain; it decides actions, and a poisoned dependency reaches it as an ordinary caller. What is easy to mistake for coverage, and is not: since 0.10.0 a policy entry may pin the upstream it authorises, by the SHA-256 of the server's leaf certificate or by the hash of a tool's advertised schema, so a swapped MCP server behind the same name, or a tool whose schema moved under an approved action name, is a `DENY`. **That is G27, and G27 is mapped under `ASI02` and `ASI07`, where binding a peer and a tool schema belongs** — not here. It is one property about one connection, and a supply chain is everything upstream of it. That is enforced by `ctrlrun gateway`, which is the surface that holds the connection: at startup, at the decision, and at the TLS handshake, where the pinned certificates are the connection's only trust anchors. In-process there is no upstream to observe and a pinned action is refused on every call. | -| `ASI05:2026` | Unexpected Code Execution | Out of scope. Nothing here sandboxes an interpreter or constrains what a process may run. CTRLRun sits between an agent and one remote effect, not between an agent and its own runtime. | -| `ASI07:2026` | Insecure Inter-Agent Communication | **Partly, since 0.10.0, and the part is authority rather than the channel.** Authority now propagates across an agent hop: the envelope a second agent receives is a subset of the one the first agent held on every dimension, checked at the hop and again at every evaluation, and an action proposed under a hop is decided against that hop's grant **alone**, with no fallback to anything else the receiving agent holds. A consumption charges the issuer and every ancestor, so a hop spends the issuer's budget rather than creating a second root. G25 grades the narrowing, G26 that both ends of a hop name it. **What it does not do**: CTRLRun defines no wire format, secures no channel, and authenticates no peer, so message integrity, transport security and agent identity are the deployment's, exactly as `ASI06`'s row says of identity. It cannot compel a receiving agent to present the hop it was given, and a receiver whose store cannot read the chain is refused rather than trusted. No A2A conformance claim. | +| `ASI04:2026` | Agentic Supply Chain Vulnerabilities | **Out of scope, and no guarantee maps to it.** ctrlrun never inspects a package, a model, a build, a registry or a signature chain; it decides actions, and a poisoned dependency reaches it as an ordinary caller. What is easy to mistake for coverage, and is not: since 0.10.0 a policy entry may pin the upstream it authorises, by the SHA-256 of the server's leaf certificate or by the hash of a tool's advertised schema, so a swapped MCP server behind the same name, or a tool whose schema moved under an approved action name, is a `DENY`. **That is G27, and G27 is mapped under `ASI02` and `ASI07`, where binding a peer and a tool schema belongs** — not here. It is one property about one connection, and a supply chain is everything upstream of it. That is enforced by `ctrlrun gateway`, which is the surface that holds the connection: at startup, at the decision, and at the TLS handshake, where the pinned certificates are the connection's only trust anchors. In-process there is no upstream to observe and a pinned action is refused on every call. | +| `ASI05:2026` | Unexpected Code Execution | Out of scope. Nothing here sandboxes an interpreter or constrains what a process may run. ctrlrun sits between an agent and one remote effect, not between an agent and its own runtime. | +| `ASI07:2026` | Insecure Inter-Agent Communication | **Partly, since 0.10.0, and the part is authority rather than the channel.** Authority now propagates across an agent hop: the envelope a second agent receives is a subset of the one the first agent held on every dimension, checked at the hop and again at every evaluation, and an action proposed under a hop is decided against that hop's grant **alone**, with no fallback to anything else the receiving agent holds. A consumption charges the issuer and every ancestor, so a hop spends the issuer's budget rather than creating a second root. G25 grades the narrowing, G26 that both ends of a hop name it. **What it does not do**: ctrlrun defines no wire format, secures no channel, and authenticates no peer, so message integrity, transport security and agent identity are the deployment's, exactly as `ASI06`'s row says of identity. It cannot compel a receiving agent to present the hop it was given, and a receiver whose store cannot read the chain is refused rather than trusted. No A2A conformance claim. | And the three entries where the mapping above is **partial**, with the part that is not covered stated here rather than left implied: | Entry | Title | Covered | Not covered | |---|---|---|---| -| `ASI06:2026` | Memory & Context Poisoning | G6 and G1 constrain what an agent acting on a poisoned context can *do*: the action must still be named in the policy, so a belief an attacker planted cannot reach a tool the agent was never entitled to use, and an approval granted for one action cannot be spent on another. This is the same downstream constraint that makes `ASI01` partial, and it is here for the same reason. | CTRLRun never reads a model's memory, its context or its prompt, so it neither detects nor prevents the poisoning. And the shape poisoning most often takes is the one the kernel has least to say about: **corrupted arguments to an action the agent is entitled to take** — the right operation against the wrong record. Policy conditions, resource patterns and v0.6 data scope bite on part of that, and since v0.9 **G23 bites on the identifier itself**: where a deployment configures a scope provider, the kernel asks its system of record whether `customer:90210` is this principal's before reserving anything. That closes the sharpest version of this and not the category. The provider is the operator's own code reading the operator's own data, so a poisoned source answers wrongly with the kernel none the wiser, and a deployment that configures no provider is exactly where it was. | -| `ASI01:2026` | Agent Goal Hijack | G1 and G6 constrain what a hijacked agent can *do*: it still meets the policy, and it still cannot present an approval granted for a different action. | CTRLRun does not detect or prevent the hijack. It never sees the prompt, the plan or the reasoning, so an agent whose goal was replaced proposes actions exactly as a healthy one would — and every action inside its policy and its grants will run. | -| `ASI09:2026` | Human-Agent Trust Exploitation | G1 and G2 close the shape where an approval a human gave for one action is spent on another, or spent twice. Since 0.8.0, **where the deployment configures an approver identity**, the approver is a resolved principal rather than a string: G17 refuses an approval whose recorded entitlement does not cover the role the request pinned, G18 refuses the requester approving their own action, compared on the principal and never on the approver string, and G19 counts distinct principals, so one person cannot be several of an M-of-N. | **The three checks above are opt-in**: a deployment that configures no approver identity runs none of them, and its `approver` is the string it was before 0.8.0. The one case that is not left silent is a threshold above one, which is denied before a human is asked rather than counted against strings. And CTRLRun does not decide **who is entitled**: what entitled an approver was decided where the credential was verified, which is the operator MCP server or an embedding application; the kernel matches a recorded claim byte for byte, does not check that the role exists anywhere, and a cited control naming no role gates nobody. It has no opinion on whether the human was misled: a person persuaded to approve the right action for the wrong reason gives a valid approval, and the receipt records it as one. | +| `ASI06:2026` | Memory & Context Poisoning | G6 and G1 constrain what an agent acting on a poisoned context can *do*: the action must still be named in the policy, so a belief an attacker planted cannot reach a tool the agent was never entitled to use, and an approval granted for one action cannot be spent on another. This is the same downstream constraint that makes `ASI01` partial, and it is here for the same reason. | ctrlrun never reads a model's memory, its context or its prompt, so it neither detects nor prevents the poisoning. And the shape poisoning most often takes is the one the kernel has least to say about: **corrupted arguments to an action the agent is entitled to take** — the right operation against the wrong record. Policy conditions, resource patterns and v0.6 data scope bite on part of that, and since v0.9 **G23 bites on the identifier itself**: where a deployment configures a scope provider, the kernel asks its system of record whether `customer:90210` is this principal's before reserving anything. That closes the sharpest version of this and not the category. The provider is the operator's own code reading the operator's own data, so a poisoned source answers wrongly with the kernel none the wiser, and a deployment that configures no provider is exactly where it was. | +| `ASI01:2026` | Agent Goal Hijack | G1 and G6 constrain what a hijacked agent can *do*: it still meets the policy, and it still cannot present an approval granted for a different action. | ctrlrun does not detect or prevent the hijack. It never sees the prompt, the plan or the reasoning, so an agent whose goal was replaced proposes actions exactly as a healthy one would — and every action inside its policy and its grants will run. | +| `ASI09:2026` | Human-Agent Trust Exploitation | G1 and G2 close the shape where an approval a human gave for one action is spent on another, or spent twice. Since 0.8.0, **where the deployment configures an approver identity**, the approver is a resolved principal rather than a string: G17 refuses an approval whose recorded entitlement does not cover the role the request pinned, G18 refuses the requester approving their own action, compared on the principal and never on the approver string, and G19 counts distinct principals, so one person cannot be several of an M-of-N. | **The three checks above are opt-in**: a deployment that configures no approver identity runs none of them, and its `approver` is the string it was before 0.8.0. The one case that is not left silent is a threshold above one, which is denied before a human is asked rather than counted against strings. And ctrlrun does not decide **who is entitled**: what entitled an approver was decided where the credential was verified, which is the operator MCP server or an embedding application; the kernel matches a recorded claim byte for byte, does not check that the role exists anywhere, and a cited control naming no role gates nobody. It has no opinion on whether the human was misled: a person persuaded to approve the right action for the wrong reason gives a valid approval, and the receipt records it as one. | --- diff --git a/docs/OWASP-SOLUTIONS-LANDSCAPE.md b/docs/OWASP-SOLUTIONS-LANDSCAPE.md index b1dd268..1c2b214 100644 --- a/docs/OWASP-SOLUTIONS-LANDSCAPE.md +++ b/docs/OWASP-SOLUTIONS-LANDSCAPE.md @@ -1,13 +1,13 @@ --- title: "OWASP Agentic Solutions Landscape" -description: "A reading of the OWASP Agentic Solutions Landscape checklist against what CTRLRun 0.12.2 ships, with the boxes it does not tick named." +description: "A reading of the OWASP Agentic Solutions Landscape checklist against what ctrlrun 0.12.2 ships, with the boxes it does not tick named." sidebarTitle: "OWASP Solutions Landscape" --- -This is a **reading** of somebody else's checklist against the guarantees CTRLRun tests. The +This is a **reading** of somebody else's checklist against the guarantees ctrlrun tests. The OWASP GenAI Security Project keeps a directory of solutions for agentic applications, and its submission form scores a solution on nine lifecycle stages, the ten entries of the Agentic Top -10, and about forty capability checkboxes. This page says which of those CTRLRun can honestly +10, and about forty capability checkboxes. This page says which of those ctrlrun can honestly tick, which it can tick in part, and which it cannot. One row per checkbox, in the form's own wording, each pointing at a guarantee, a command or a document. @@ -26,7 +26,7 @@ credible. | **Publisher** | OWASP GenAI Security Project, OWASP Foundation | | **Form** | [https://genai.owasp.org/solution-submission-agentic/](https://genai.owasp.org/solution-submission-agentic/) | | **Read on** | 2026-09-10 | -| **Written against** | CTRLRun **0.12.2**, guarantees `G1`–`G32`, catalogue `ctrlrun.guarantees/v7` | +| **Written against** | ctrlrun **0.12.2**, guarantees `G1`–`G32`, catalogue `ctrlrun.guarantees/v7` | **Written against what is tagged, and the version is printed on every row that needs one.** Every guarantee cited below is in the catalogue `ctrlrun verify` runs today, `G1` through @@ -43,22 +43,22 @@ records how the ten titles were derived and asks anyone holding the published PD them. **Three words.** *Yes* means a guarantee or a shipped command does what the checkbox says, and -the row names it. *Partly* means CTRLRun does a stated part of it, and the row says which part -it does not. *No* means nothing in CTRLRun addresses the box, and the reason is one sentence. +the row names it. *Partly* means ctrlrun does a stated part of it, and the row says which part +it does not. *No* means nothing in ctrlrun addresses the box, and the reason is one sentence. --- ## Lifecycle stages -The form asks which stages of the agent lifecycle a solution covers. CTRLRun is a library that +The form asks which stages of the agent lifecycle a solution covers. ctrlrun is a library that sits at one point, between the decision to act and the call that acts, so it reaches most stages from that one point rather than covering each in its own right. -| Stage | Status | What CTRLRun has there | Since | +| Stage | Status | What ctrlrun has there | Since | |---|---|---|---| | Scope & Plan | Partly | A published threat model of the execution boundary ([THREAT_MODEL](/docs/THREAT_MODEL)), and a policy document that *is* the plan for what an agent may do. Nothing that models *your* agent for you. | v0.1 | | Develop & Experiment | Yes | `@protect` on any function in the process; `ctrlrun scan` reports the consequential call sites a policy does not cover, and `ctrlrun scan --coverage` reports what a store shows was declared and never exercised. Both are lists with reasons, not scores. | v0.1, scan since v0.6, coverage since v0.11 | -| Augment & Fine Tune Data | No | CTRLRun never touches training data, models or memory. | none | +| Augment & Fine Tune Data | No | ctrlrun never touches training data, models or memory. | none | | Test & Evaluate | Yes | `ctrlrun verify` runs the kernel's own failure scenarios against your configuration and reports pass, fail or **not applicable** per guarantee ([verify](/docs/verify)). | v0.4 | | Release | Yes | The `ctrlrun verify` GitHub Action and badge as a release gate; the badge means *declared guarantees pass*, never "this agent is secure by inspection". | v0.4 | | Deploy | Yes | The MCP gateway in front of an existing tool server; framework adapters; observe mode to roll out without refusing anything yet. | v0.2, v0.3 | @@ -72,25 +72,25 @@ stages from that one point rather than covering each in its own right. The wording in the first column is the form's, quoted so a reviewer can match rows without translation. Where the form names a technology as an example (*e.g., Sigstore, Immudb*), the -example is theirs and is not a claim that CTRLRun uses it. +example is theirs and is not a claim that ctrlrun uses it. ### Scope & Plan | Checkbox | Status | What it means here | Since | |---|---|---|---| -| Support for Gen AI Security Project - Agentic Security Threat Modeling Approach | No | CTRLRun publishes its own threat model of one boundary. It does not implement or support the project's modelling approach. | none | -| Conducting Agentic Threat Modeling | Partly | [THREAT_MODEL](/docs/THREAT_MODEL) is a threat model *of CTRLRun*: what it refuses, and the four things it states as out of scope: a compromised host, a malicious administrator, a lying remote, code that bypasses the decorator. It is a worked example, not a tool. | v0.1 | +| Support for Gen AI Security Project - Agentic Security Threat Modeling Approach | No | ctrlrun publishes its own threat model of one boundary. It does not implement or support the project's modelling approach. | none | +| Conducting Agentic Threat Modeling | Partly | [THREAT_MODEL](/docs/THREAT_MODEL) is a threat model *of ctrlrun*: what it refuses, and the four things it states as out of scope: a compromised host, a malicious administrator, a lying remote, code that bypasses the decorator. It is a worked example, not a tool. | v0.1 | | Draft policy Agent for tool scopes | Yes | The policy document is the list of actions an agent may propose, with conditions; anything not in it is refused (`G6`). Starting-point policies ship under `examples/policies/`, each headed *adapt before use*. | v0.1 | -| Identify system-wide non-human Identities & Auth Protocols | No | CTRLRun consumes a principal from an `IdentityProvider` and issues nothing. It has no inventory of identities. | none | +| Identify system-wide non-human Identities & Auth Protocols | No | ctrlrun consumes a principal from an `IdentityProvider` and issues nothing. It has no inventory of identities. | none | | Draft policy for Agent privilege boundaries | Yes | An authority grant names a subject, permitted actions, resource patterns, constraints, environments and an expiry; opt-in, then fail-closed (`G7`, `G8`). | v0.3 | | Draft policy for delegation logic | Yes | A delegated grant is valid only as a subset of its parent on every dimension, checked at creation and on every evaluation; omission is rejected, not inherited (`G9`). Task binding adds one more dimension (`G24`). | v0.3, v0.9 | -| Define controls for memory scoping, isolation & long-term persistance | No | CTRLRun never reads or writes an agent's memory. | none | +| Define controls for memory scoping, isolation & long-term persistance | No | ctrlrun never reads or writes an agent's memory. | none | ### Develop & Experiment | Checkbox | Status | What it means here | Since | |---|---|---|---| -| Perform SAST/DAST on agent planning code, tool wrappers, & plugin interfaces | Partly | `ctrlrun scan` reads a Python tree and reports the consequential call sites and policy entries CTRLRun is *not* covering, and since v0.11 `--coverage` adds the runtime half: what the store shows was declared and never exercised. It is a coverage scanner, not a vulnerability scanner, and its report says what it misses by construction on every run. **Neither half produces a number**: a policy entry nothing exercised may be correctly unused, and saying otherwise would be grading the operator's document. | v0.6, v0.11 | +| Perform SAST/DAST on agent planning code, tool wrappers, & plugin interfaces | Partly | `ctrlrun scan` reads a Python tree and reports the consequential call sites and policy entries ctrlrun is *not* covering, and since v0.11 `--coverage` adds the runtime half: what the store shows was declared and never exercised. It is a coverage scanner, not a vulnerability scanner, and its report says what it misses by construction on every run. **Neither half produces a number**: a policy entry nothing exercised may be correctly unused, and saying otherwise would be grading the operator's document. | v0.6, v0.11 | | Harden agent loop logic against infinite loops, unsafe function routing, & unauthorized self-modification | Partly | A retry loop cannot turn one intended effect into several (`G3`, `G5`), an unknown action is refused (`G6`), and renewal after `FAILED` has an operator-set ceiling (`G15`). Nothing here inspects loop logic or prevents self-modification. | v0.1, v0.7 | | Validate connector (e.g., MCP) contracts (input/output schemas & permissions) | Partly | The gateway maps every MCP tool call onto a policy decision, and a policy entry may pin the upstream it authorises, by the SHA-256 of the server's leaf certificate or by the hash of a tool's advertised schema: a tool whose schema moved under an approved action name is refused `upstream_mismatch`, and an upstream nothing observed is refused `upstream_unverified` and never admitted (`G27`). Enforced by `ctrlrun gateway`, the surface that holds the connection; in-process there is no upstream to observe. It does not validate schemas in general. | v0.2, v0.10 | | Implement policy enforcement hooks in Frameworks (e.g. LangGraph, CrewAI, Others) | Yes | `@protect` for anything in-process; the adapter contract with OpenAI Agents SDK and LangGraph reference adapters, each routing an `approve` through the framework's own interrupt; the OWASP Agent Control Standard adapter ([ACS](/docs/ACS)). | v0.1, v0.2, v0.5 | @@ -109,10 +109,10 @@ example is theirs and is not a claim that CTRLRun uses it. | Checkbox | Status | What it means here | Since | |---|---|---|---| -| Agent Penetration Testing | No | CTRLRun tests its own guarantees against your configuration. It does not attack your agent. | none | +| Agent Penetration Testing | No | ctrlrun tests its own guarantees against your configuration. It does not attack your agent. | none | | Adversarial red-teaming: goal drift, prompt injection, hallucination chaining, & over-permissioned tool usage | Partly | `ctrlrun verify` exercises mutated and replayed approvals, races across OS processes, escalation on every delegation dimension and a blind retry after a lost response. Over-permissioned tool use is what `ctrlrun scan` and observe mode measure. Nothing here reads a prompt or drifts a goal. | v0.4, v0.3 | | Multi-agent scenario simulations for collusion, misalignment, or deception detection | No | Out of scope. | none | -| Validate agent decisions against expected goal plans | No | CTRLRun never sees the plan. It decides actions. | none | +| Validate agent decisions against expected goal plans | No | ctrlrun never sees the plan. It decides actions. | none | | Sandboxed testing of all tool calls, code execution, cloud API triggers | No | `verify` runs against fake remotes that count their calls; it is a test of the kernel, not a sandbox for your tools. | none | | Available Agent Scanning | Partly | Static: `ctrlrun scan`, the gap between *installed* and *in the path*. Runtime: enforcement coverage from events, meaning policy entries never exercised, gateway tools never routed, `@protect` actions never seen. No score, no percentage, no badge, by rule. | v0.6, v0.11 | @@ -120,7 +120,7 @@ example is theirs and is not a claim that CTRLRun uses it. | Checkbox | Status | What it means here | Since | |---|---|---|---| -| Generate & verify model + agent + tool SBOMs - shared responsibility | No | CTRLRun ships its own SBOM and signed SLSA provenance for its own releases. It generates nothing for your agent. | none | +| Generate & verify model + agent + tool SBOMs - shared responsibility | No | ctrlrun ships its own SBOM and signed SLSA provenance for its own releases. It generates nothing for your agent. | none | | Register all agents in an internal trust registry | No | A grant names a subject; there is no registry of agents. | none | | Sign model weights, plugin manifests, & memory snapshots | No | Out of scope. | none | @@ -129,17 +129,17 @@ example is theirs and is not a claim that CTRLRun uses it. | Checkbox | Status | What it means here | Since | |---|---|---|---| | Apply & manage runtime Guardrails | Partly | Policy at the tool call decides `allow`, `approve` or `deny` for every consequential action, and observe mode reports what would have been refused before anything is. Not input or output filtering, and not moderation; the [comparison page](/docs/compare/guardrail-libraries) says where the line is. | v0.1, v0.3 | -| Rotate all shared secrets, keys, & tokens with ephemeral, scoped credentials | No | CTRLRun issues no credentials and rotates none. | none | -| Enforce zero-trust policies between agents, tools, & external APIs | Yes | Every action carries a verified principal or does not run (`G7`); authority is evaluated on every action against the clock (`G8`); an unknown action is refused (`G6`); the gateway applies all of it to a tool server that has no idea CTRLRun exists. Between agents: the envelope a second agent receives is a subset of the first agent's, checked at the hop and on every evaluation. | v0.1, v0.3, v0.10 | +| Rotate all shared secrets, keys, & tokens with ephemeral, scoped credentials | No | ctrlrun issues no credentials and rotates none. | none | +| Enforce zero-trust policies between agents, tools, & external APIs | Yes | Every action carries a verified principal or does not run (`G7`); authority is evaluated on every action against the clock (`G8`); an unknown action is refused (`G6`); the gateway applies all of it to a tool server that has no idea ctrlrun exists. Between agents: the envelope a second agent receives is a subset of the first agent's, checked at the hop and on every evaluation. | v0.1, v0.3, v0.10 | | Configure Inter-agent authorization policies, capabilities, & roles | Yes | Delegation with attenuation (`G9`), task-bound authority (`G24`), authority propagated across A2A hops with depth and expiry, and approver roles from the control registry (`G17`). | v0.3, v0.8, v0.9, v0.10 | ### Monitor | Checkbox | Status | What it means here | Since | |---|---|---|---| -| Audit reflection accuracy by comparing stated & observed planning outcomes | No | CTRLRun never sees a plan or a reflection. | none | -| Use immutable logs (e.g., Sigstore, Immudb) for forensic readiness | Partly | Tamper-evident, not immutable. Each receipt hashes the one before it, so alteration, deletion and reordering are reported by name and by `seq` (`G11`); the head is anchored outside the database through a provider **you** supply, since CTRLRun ships none, so a suffix erased at or below an anchored `seq` is detected (`G28`) and the window you are exposed to is your own anchoring interval. Retention does not quietly break it: `ctrlrun prune` removes a prefix and leaves a checkpoint the reader seeds from, so the chain verifies across the gap, it refuses rather than warns and has no `--force` (`G29`), an honest prune leaves the anchors reproducing (`G32`), and a range under `ctrlrun hold place` refuses to prune at all, with the hold named (`G30`). Receipts are not signed, and the chain says nothing about who wrote it. | v0.6, v0.11 | -| Alert on anomalies; e.g., goal reversal, unexpected plan depth, adversarial-input, excessive tool usage, or rapid inter-agent chatter | Partly | Excessive use is refused rather than alerted on: a consequence budget on a grant is consumed on reserve and held until an `AMBIGUOUS` effect is reconciled (`G22`), and every refusal is an event on the sink your alerting reads. CTRLRun ships no alerting and sees no plan or prompt. | v0.2, v0.9 | +| Audit reflection accuracy by comparing stated & observed planning outcomes | No | ctrlrun never sees a plan or a reflection. | none | +| Use immutable logs (e.g., Sigstore, Immudb) for forensic readiness | Partly | Tamper-evident, not immutable. Each receipt hashes the one before it, so alteration, deletion and reordering are reported by name and by `seq` (`G11`); the head is anchored outside the database through a provider **you** supply, since ctrlrun ships none, so a suffix erased at or below an anchored `seq` is detected (`G28`) and the window you are exposed to is your own anchoring interval. Retention does not quietly break it: `ctrlrun prune` removes a prefix and leaves a checkpoint the reader seeds from, so the chain verifies across the gap, it refuses rather than warns and has no `--force` (`G29`), an honest prune leaves the anchors reproducing (`G32`), and a range under `ctrlrun hold place` refuses to prune at all, with the hold named (`G30`). Receipts are not signed, and the chain says nothing about who wrote it. | v0.6, v0.11 | +| Alert on anomalies; e.g., goal reversal, unexpected plan depth, adversarial-input, excessive tool usage, or rapid inter-agent chatter | Partly | Excessive use is refused rather than alerted on: a consequence budget on a grant is consumed on reserve and held until an `AMBIGUOUS` effect is reconciled (`G22`), and every refusal is an event on the sink your alerting reads. ctrlrun ships no alerting and sees no plan or prompt. | v0.2, v0.9 | | Correlate telemetry from agent step tracing, tool execution, & message logs | Yes | The OpenTelemetry sink opens one span per action carrying `ctrlrun.action_id`, `ctrlrun.effect_key` and `ctrlrun.approval_id`, and the receipt carries the same `action_id`, so a tool-execution span joins an agent trace and a receipt on one identifier ([export guide](/docs/guides/export-to-opentelemetry)). | v0.2 | ### Operate @@ -147,7 +147,7 @@ example is theirs and is not a claim that CTRLRun uses it. | Checkbox | Status | What it means here | Since | |---|---|---|---| | LLM Incident Detection & Response | Partly | Response, not detection: `ctrlrun revoke --by <principal>` and `--under <grant id>` cut everything a principal or a grant issued, idempotently; `ctrlrun resolve` settles an `AMBIGUOUS` effect; break-glass is a recorded, expiring grant, never a flag that skips a check. | v0.3, v0.8 | -| Continuously scan loaded plugins for CVEs & privilege escalation vectors | No | CTRLRun never inspects a package. | none | +| Continuously scan loaded plugins for CVEs & privilege escalation vectors | No | ctrlrun never inspects a package. | none | | Runtime guardrails & moderation; anomalous tool use | Partly | An action outside the policy or outside the grant is refused, by name. No moderation, and no model of what is anomalous beyond *not permitted*. | v0.1, v0.3 | | Monitor agent memory mutation patterns for drift | No | Out of scope. | none | | Detect task replay, infinite delegation, or hallucination loops | Partly | A replayed approval is refused (`G2`), a duplicate effect is refused (`G3`), delegation carries a depth limit across hops, and a task-bound grant is refused on a task it does not name (`G24`). Hallucination loops are not detected; their consequential actions are refused. | v0.1, v0.9, v0.10 | @@ -176,11 +176,11 @@ maps to the entry, which is a test and not an intention. |---|---|---|---|---| | `ASI01:26` Agent Goal Hijack | Partly | `G1`, `G6`, `G16`, `G21`, `G22`, `G23`, `G24` | The hijack itself. Task binding, a budget and a scope provider shrink what a hijacked agent can do; nothing reads the hijack. | v0.1, v0.7, v0.8, v0.9 | | `ASI02:26` Tool Misuse | Yes | `G3`, `G6`, `G23`, `G27` | Misuse that stays inside the policy, the grant and the budget. | v0.1, v0.9, v0.10 | -| `ASI03:26` Identity & Privilege Abuse | Yes | `G7`, `G8`, `G9`, `G17`, `G18`, `G19`, `G20`, `G21`, `G24`, `G25`, `G26` | Issuing identity. CTRLRun verifies what it is handed. | v0.1, v0.3, v0.8, v0.9, v0.10 | +| `ASI03:26` Identity & Privilege Abuse | Yes | `G7`, `G8`, `G9`, `G17`, `G18`, `G19`, `G20`, `G21`, `G24`, `G25`, `G26` | Issuing identity. ctrlrun verifies what it is handed. | v0.1, v0.3, v0.8, v0.9, v0.10 | | `ASI04:26` Agentic Supply Chain Vulnerabilities | No | none | Out of scope, and no guarantee maps to it: no package, model, build, registry or signature chain is ever inspected. Upstream pinning binds one connection and one tool schema, and it is mapped under `ASI02:26` and `ASI07:26`, where binding a peer belongs, rather than here. | none | | `ASI05:26` Unexpected Code Execution | No | none | Out of scope: nothing here sandboxes an interpreter. | none | | `ASI06:26` Memory & Context Poisoning | Partly | `G1`, `G6`, `G23` | The poisoning. A scope provider bites on an identifier an attacker chose, fetched before the reservation and fail-closed; the recheck still cannot run inside the atomic write, and that residual gap is stated wherever the feature is. | v0.1, v0.9 | -| `ASI07:26` Insecure Inter-Agent Communication | Partly | `G25`, `G26`, `G27` | Transport security. Authority across an A2A hop is a subset of the sender's, checked at the hop and on every evaluation, and a pinned upstream refuses a swapped peer; the channel itself is not CTRLRun's. | v0.10 | +| `ASI07:26` Insecure Inter-Agent Communication | Partly | `G25`, `G26`, `G27` | Transport security. Authority across an A2A hop is a subset of the sender's, checked at the hop and on every evaluation, and a pinned upstream refuses a swapped peer; the channel itself is not ctrlrun's. | v0.10 | | `ASI08:26` Cascading Failures | Yes | `G3`, `G4`, `G5`, `G10`, `G12`, `G13`, `G14`, `G15`, `G22` | A failure that never reaches a consequential action. | v0.1, v0.7, v0.9 | | `ASI09:26` Human-Agent Trust Exploitation | Partly | `G1`, `G2`, `G11`, `G12`, `G16`, `G17`, `G18`, `G19`, `G28`, `G29`, `G30`, `G31`, `G32` | Persuasion. A human misled into approving the right action for the wrong reason gives a valid approval, and the receipt records it as one. | v0.1, v0.6, v0.7, v0.8, v0.11 | | `ASI10:26` Rogue Agents | Partly | `G8`, `G9`, `G15`, `G20`, `G21`, `G22`, `G24`, `G25`, `G26` | Detection. A rogue agent is bounded, expired and revoked; it is not recognised as rogue. | v0.3, v0.7, v0.8, v0.9, v0.10 | @@ -208,4 +208,4 @@ released, and that each `ASI` row's guarantees are exactly what the - [OWASP Agentic Top 10](/docs/OWASP-AGENTIC-TOP10): the row-by-row mapping this summary is drawn from. - [Verify](/docs/verify): how each guarantee is checked against your configuration, and why *not applicable* is not a pass. -- [Why CTRLRun](/docs/why) and [Get started](/docs/get-started/quickstart). +- [Why ctrlrun](/docs/why) and [Get started](/docs/get-started/quickstart). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e89039c..6626f42 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -21,15 +21,15 @@ Standards: none. `THREAT_MODEL.md` is the only compliance-adjacent claim. ## v0.2 — Zero-friction deployment ✅ shipped -- MCP adapter and `ctrlrun gateway --upstream <mcp server>` so an existing MCP tool server gets CTRLRun semantics with no agent changes. +- MCP adapter and `ctrlrun gateway --upstream <mcp server>` so an existing MCP tool server gets ctrlrun semantics with no agent changes. - OpenTelemetry export of events (align with ACS observability; don't invent a tracing format). - Webhook approval provider (Slack/Teams/anything that can POST back). - `ctrlrun inspect <action_id>`. - Reconciliation hook: `@protect(..., reconcile=...)` resolves AMBIGUOUS automatically, and only where its answer points. -- `examples/` directory with standalone scripts per scenario (`double-refund/`, `approval-mutation/`, `agent-race/`, `approval-replay/`). In v0.1 `ctrlrun demo` is the example; separate scripts earn their keep once there is more than one way to wire CTRLRun in. +- `examples/` directory with standalone scripts per scenario (`double-refund/`, `approval-mutation/`, `agent-race/`, `approval-replay/`). In v0.1 `ctrlrun demo` is the example; separate scripts earn their keep once there is more than one way to wire ctrlrun in. - Sector policy templates: `examples/policies/<sector>.yaml` for devops, payments, e-commerce, insurance, healthcare, legal, security, government, hr. Header comment: *"Starting point on the v0.1 kernel. Adapt before use."* Uses only v0.1 primitives. Tier one of the sector-pack content track below. -Adoption story: *existing MCP server + one CTRLRun gateway = action safety.* +Adoption story: *existing MCP server + one ctrlrun gateway = action safety.* **Reconciled against what shipped.** Three things arrived earlier than this file expected, and one arrived that it did not list: @@ -85,12 +85,12 @@ requirement for the same reason: a self-reported name cannot be an authorization - **Not applicable is not a pass.** A guarantee this configuration cannot exercise is reported `N/A` with the reason, excluded from the denominator and listed separately. There is no flag that folds one into the count. - **Every guarantee carries a positive control.** A refusal asserted against a scenario in which nothing ran passes on a kernel with the guard deleted, so a control that misbehaves is `fail` with `reason: "control failed"` — never a pass, and never an N/A. - Counterexample output on failure: the ordered events, receipts and effect records that show the violation. -- GitHub Action + badge, "CTRLRun verified N/M", where M is **applicable** guarantees. The badge means *declared guarantees pass*, never "this agent is secure." +- GitHub Action + badge, "ctrlrun verified N/M", where M is **applicable** guarantees. The badge means *declared guarantees pass*, never "this agent is secure." - `research/framework-probe/`, outside `src/` and never packaged: what an agent stack does with a lost response when nothing guards the effect. Behaviour, not quality. Exit: every acceptance test in `SPEC-v0.4.md §8` passes, and every one in v0.1, v0.2 and v0.3 still does. `ctrlrun verify` against `examples/authority/payments.yaml` reports 11/11; against `examples/policies/payments.yaml`, 6/6 with five not applicable — the N/A rule dogfooded rather than described. -Standards: first mapping doc — `docs/OWASP-AGENTIC-TOP10.md`, each guarantee mapped to the OWASP Top 10 for Agentic Applications entries it mitigates, and the four entries CTRLRun does not address listed by name. A reading of somebody else's taxonomy, and it says so on its first line. +Standards: first mapping doc — `docs/OWASP-AGENTIC-TOP10.md`, each guarantee mapped to the OWASP Top 10 for Agentic Applications entries it mitigates, and the four entries ctrlrun does not address listed by name. A reading of somebody else's taxonomy, and it says so on its first line. ## v0.5 — Adapter contract (Released 2026-09-05) @@ -168,7 +168,7 @@ Standards: none new. Not a kernel milestone, and listed here because the question it answers had no command. `ctrlrun scan` reads a Python tree and a policy document and reports the consequential call -sites and policy entries CTRLRun is **not** covering — the gap between *installed* and *in the +sites and policy entries ctrlrun is **not** covering — the gap between *installed* and *in the path*. `docs/SPEC-scan.md` is the contract; it was written first and its §8 tests were red before any of it existed. @@ -208,13 +208,13 @@ in the form "does this work with a WhatsApp agent, a Slack agent, a hosted OpenA agent I only configure". The answer is a property of what already ships, not a feature to build, and a roadmap that never states it leaves every reader to derive it from `v0.2 §6.3`. -**The claim, worded so it can be tested.** CTRLRun controls any agent whose consequential +**The claim, worded so it can be tested.** ctrlrun controls any agent whose consequential actions pass through a tool or an API the operator runs. The agent's code, language, framework and vendor do not enter into it: the gateway checks a `tools/call` on the wire (v0.2), and `@protect` checks a call at the endpoint that acts (v0.1). An agent nobody can program, a no-code builder, a vendor's bot on WhatsApp or Slack, a hosted assistant with a custom connector, all reach their tools the same way, and that is where the check is. It is the same -sentence the v0.2 adoption story already makes, *existing MCP server + one CTRLRun gateway = +sentence the v0.2 adoption story already makes, *existing MCP server + one ctrlrun gateway = action safety*, with the agent named as the thing that does not matter. **Where it stops, stated so nobody sells past it.** An action that never leaves the platform, @@ -250,7 +250,7 @@ track does not change that unless the run above says it must. ## v0.7 — Execution boundary ✅ shipped -Every guarantee shipped so far is a guarantee about what happens *inside* CTRLRun. But the kernel does not decide whether the remote side acted — an executor does, by raising `NotExecuted` or not. It does not own the clock its leases are measured against, once the store is on another host. It does not know whether the world still looks the way it did when a human said yes. v0.7 asks what the kernel owes at each of those edges. +Every guarantee shipped so far is a guarantee about what happens *inside* ctrlrun. But the kernel does not decide whether the remote side acted — an executor does, by raising `NotExecuted` or not. It does not own the clock its leases are measured against, once the store is on another host. It does not know whether the world still looks the way it did when a human said yes. v0.7 asks what the kernel owes at each of those edges. - **A transport classifier in core.** `FAILED` versus `AMBIGUOUS` is the one decision this project exists to get right, and the kernel does not make it — the user's executor does. The correct rule is already written and already implemented, in the gateway's `outcome.py`: the connection was never established, or the peer said in band and before dispatch that it rejected the request; everything after the first byte is `AMBIGUOUS`. It is reachable today only by installing `ctrlrun[gateway]`, while `@protect` — the surface the README leads with — gets a docstring. One rule, one implementation, reachable from core. - **Clock-skew detection.** v0.6 moved the store to another host so several hosts could share it; lease liveness stayed on the application clock. Skew is fail-closed and therefore quiet — a host running ahead marks a live reservation `AMBIGUOUS` while its real holder is mid-flight and about to succeed, and nothing names the cause. This makes divergence observable. It does not change how a lease is evaluated. @@ -330,7 +330,7 @@ control and each `N/A` with a true reason on a configuration that carries no bud task. All three PASS on `examples/authority/payments.yaml`, so the milestone's own guarantees are graded on what this repository ships rather than only on a fixture. -**This is the gate for the category line, and it used to be the gate for the H1.** Recorded 2026-09-12: the H1 moved ahead of v0.9, to *CTRLRun stops AI agents from taking wrong, restricted, or malicious actions in your workflows*, because it states what the shipped kernel does today and claims nothing about authority. *Action governance* still waits: after v0.9 it is true in code, and only then does the category line move up. +**This is the gate for the category line, and it used to be the gate for the H1.** Recorded 2026-09-12: the H1 moved ahead of v0.9, to *ctrlrun stops AI agents from taking wrong, restricted, or malicious actions in your workflows*, because it states what the shipped kernel does today and claims nothing about authority. *Action governance* still waits: after v0.9 it is true in code, and only then does the category line move up. Standards: none new. @@ -377,7 +377,7 @@ Standards: A2A, as code. No conformance claim. One question: can the record be trusted after the fact, and kept? -- **An external anchor for the receipt chain.** The chain detects alteration and says on every page that it does not detect truncation or append — both measured at two statements, undetected, because the head is a row in the same database. v0.11 anchors the head outside the database at an interval (an RFC 3161 timestamp, or an equivalent the operator supplies) so that **anything at or below an anchored `seq` can no longer be removed or altered** without the anchored pair failing to reproduce. An anchor freezes a prefix: **an append is not detected**, because an appended row lands above every anchored `seq`, and nor is a receipt created and destroyed entirely between two anchors. This sentence said "erased or appended" until 2026-09-14, when `SPEC-v0.11.md`'s review ran the cases; §2.4 there is the table, and the named kinds are the anchor's own, not the six chain-break kinds. No keys of its own: it consumes a timestamp and issues nothing, which is why it is here and signing is not. **Built by item 2 on 2026-09-14**, with `G28` grading it and `ctrlrun anchor` running it. CTRLRun ships **no** provider: an RFC 3161 client is a network client, so the operator supplies four calls (`make`, `check`, `latest`, `since`) and `examples/anchored-chain/` shows the smallest one that works. `since()` is the call a review added and the reason the design holds: with `make` and `check` alone, the record of *which* anchors exist lived in CTRLRun's own table, so deleting the newest row there left the older anchor reproducing and the truncation invisible, at a cost of one more statement. +- **An external anchor for the receipt chain.** The chain detects alteration and says on every page that it does not detect truncation or append — both measured at two statements, undetected, because the head is a row in the same database. v0.11 anchors the head outside the database at an interval (an RFC 3161 timestamp, or an equivalent the operator supplies) so that **anything at or below an anchored `seq` can no longer be removed or altered** without the anchored pair failing to reproduce. An anchor freezes a prefix: **an append is not detected**, because an appended row lands above every anchored `seq`, and nor is a receipt created and destroyed entirely between two anchors. This sentence said "erased or appended" until 2026-09-14, when `SPEC-v0.11.md`'s review ran the cases; §2.4 there is the table, and the named kinds are the anchor's own, not the six chain-break kinds. No keys of its own: it consumes a timestamp and issues nothing, which is why it is here and signing is not. **Built by item 2 on 2026-09-14**, with `G28` grading it and `ctrlrun anchor` running it. ctrlrun ships **no** provider: an RFC 3161 client is a network client, so the operator supplies four calls (`make`, `check`, `latest`, `since`) and `examples/anchored-chain/` shows the smallest one that works. `since()` is the call a review added and the reason the design holds: with `make` and `check` alone, the record of *which* anchors exist lived in ctrlrun's own table, so deleting the newest row there left the older anchor reproducing and the truncation invisible, at a cost of one more statement. - **Retention and legal hold.** There is no retention policy today and `docs/postgres.md` says so, in the same breath as the reason one is hard to write: deleting receipts from the middle or the end of the chain is detected as a break by design. v0.11 pays that debt: a chain-preserving prune that leaves a checkpoint receipt verifiable across the gap, and a hold that refuses to prune, both recorded as receipts themselves. **v0.9 adds a second growing table and states the invariant rather than the command**: the budget ledger only grows, and `SPEC-v0.9.md` §7.3 says that rows older than the longest window on any budget of a grant cannot affect a future decision, so somebody else's archiving is safe. One caveat travels with it, because the invariant is about decisions and not about evidence: an `AMBIGUOUS` effect older than that window still **holds** a charge the operator surfaces display, so an archiver on a live ledger excludes un-released rows. `ctrlrun stats` reports the row count so the growth is visible before it matters. **Built by item 3 on 2026-09-14**, as `ctrlrun prune` and `ctrlrun hold`, with `G29`, `G30` and `G32` grading it. Two things the build settled that the line above did not say. The ledger rule is **settlement and then a window**, not "un-released": `COMMITTED` holds permanently and only `FAILED` releases, so "un-released" would have been almost every row forever, and a `COMMITTED` row is prunable only **outside** §7.3's window, because pruning one inside it hands back authority nobody granted. And the window is **supplied** on the command line rather than derived: a ledger row carries no window and no limit, those travel on `Charge` from the authority document, and a store that resolved them would be reading the policy. - **Enforcement coverage.** From what is already written: policy entries never exercised, gateway tools never routed, `@protect` actions never seen. The runtime half of `ctrlrun scan`, under the same rule — a clean result is not a verdict, no score, no percentage, no badge. **Built by item 5 on 2026-09-14** as `ctrlrun scan --coverage`. One correction the build earned: this line said *from events already written*, and the action name is **not on the event**. `ACTION_PROPOSED` carries an `action_hash` and nothing that maps it back to a name, so the answer comes from receipts, which every action that reached a decision leaves — **a denial included**, which is why an action that is always denied counts as exercised rather than as a gap. No new event type and no new column either way, which is what §7 made the test of whether the question was the right one. It does not move the exit code: a number that ranked a deployment would be the verdict this rule forbids, wearing a shell's clothes. - **One chain, several receipt schemas.** `ctrlrun.receipt/v7` is the schema today, and the rule since `SPEC-v0.3.md` §12.2 is that every reader upgrades before any writer switches, so an older receipt on disk still parses. v0.8 (the verified approver; the grant id under break-glass), v0.9 (budget consumption) and v0.10 (the hop) each add fields and each bump the version, so a chain kept from v0.6 across them holds **five receipt schema versions**: `v3`, which 0.6 wrote, `v4`, which v0.7 added, and `v5`, `v6` and `v7` after it. This sentence has now gone stale twice and is corrected here rather than quietly both times. It said *three shapes* and named `v3` as the schema today, before v0.7's precondition fields bumped it; v0.7's release pass fixed that and left *four* and `v4`, which v0.10's hop field made wrong again. A count of versions in a document is a number that goes stale at every release, which is the argument for reading `receipt.py`'s constants instead. And nothing yet proved that `verify` walks it end to end, hash by hash, each receipt hashed by the rule its own version wrote. **v0.11's item 4 proves it and `G31` grades it, since 2026-09-14.** No new field: the version string already existed. What is new is the proof, and the rule that a receipt whose version the binary does not know is *named* and not reported as a break — the same distinction v0.6 §3.2 draws for a `schema_version` row the binary does not know. **The proof is built from the released wheels rather than from fixtures** (`scripts/five_schema_chain.py`): five environments, `pip install ctrlrun==0.6.1`, `0.7.0`, `0.8.0`, `0.9.0`, `0.10.0`, one store, then this build verifies across the whole thing, because a fixture is only this build's opinion of what 0.6 wrote. One thing that proof got wrong first is worth keeping: run with `PYTHONPATH=src`, the variable is inherited by every child, so all five "released wheels" imported the build under test and the run reported **one** schema version while looking exactly like a pass. The script now strips it and checks, per release, that the interpreter ran from that release's own environment. Added 2026-09-10, proved 2026-09-14. diff --git a/docs/THREAT_MODEL.md b/docs/THREAT_MODEL.md index 31f9c96..9fe5c66 100644 --- a/docs/THREAT_MODEL.md +++ b/docs/THREAT_MODEL.md @@ -1,9 +1,9 @@ --- title: "Threat model" -description: "What CTRLRun defends against, what it deliberately does not, and the fail-closed rules that follow from both." +description: "What ctrlrun defends against, what it deliberately does not, and the fail-closed rules that follow from both." --- -CTRLRun sits in the execution path of consequential actions. This document states what it defends against, what it explicitly does not, and the fail-closed rules that follow. It covers every shipped version through v0.6, and grows with the roadmap. +ctrlrun sits in the execution path of consequential actions. This document states what it defends against, what it explicitly does not, and the fail-closed rules that follow. It covers every shipped version through v0.6, and grows with the roadmap. ## Assets @@ -15,7 +15,7 @@ CTRLRun sits in the execution path of consequential actions. This document state ``` untrusted ─────────────┐ trusted ──────────────────┐ - agent reasoning │ CTRLRun process │ + agent reasoning │ ctrlrun process │ LLM outputs │ policy file │ tool outputs │ StateStore (SQLite file) │ retry logic │ approver's shell │ @@ -24,7 +24,7 @@ CTRLRun sits in the execution path of consequential actions. This document state The agent is treated as a potentially compromised or hallucinating principal. Everything it proposes is verified; nothing it asserts is trusted. -## In scope — CTRLRun v0.1 defends against +## In scope — ctrlrun v0.1 defends against | Threat | Control | |---|---| @@ -40,7 +40,7 @@ The agent is treated as a potentially compromised or hallucinating principal. Ev | Malformed or missing policy | Load-time error; no Control without valid policy | | Float-based hash collisions/mismatches | Floats rejected in arguments | -## In scope — CTRLRun v0.3 adds +## In scope — ctrlrun v0.3 adds The authority model answers a question v0.1 and v0.2 could not: *who is acting, and what are they entitled to?* Everything above still holds; these are the threats the second axis closes. @@ -61,7 +61,7 @@ they entitled to?* Everything above still holds; these are the threats the secon | An unauthenticated principal reaching an authorization decision | `--principal-from-client-info` removed; `AcsControlHook` refuses an `Authority` without an `identity` provider | | An environment chosen by the caller | The environment is set once on the `Control` and is never read off the wire | -## In scope — CTRLRun v0.9 adds +## In scope — ctrlrun v0.9 adds The authority model bounded **one action** and never an aggregate: a grant saying `amount_lte: 5000` is silent about the thousand actions that each pass it. v0.9 answers *how @@ -115,21 +115,21 @@ Stated here because a limit reads like more of a defence than it is. - **It does not propagate across agent hops.** A grant is evaluated where the action is proposed; `docs/ROADMAP.md` puts propagation in v0.10. -## Out of scope — CTRLRun does not defend against +## Out of scope — ctrlrun does not defend against -- A compromised CTRLRun process, host, or Python environment. +- A compromised ctrlrun process, host, or Python environment. - A root attacker or a malicious administrator with write access to the policy file or SQLite database. - A compromised external service (Stripe lying about outcomes). -- A compromised approver, or social engineering of the approver. CTRLRun proves *what* was approved, not that the human was right. -- Executors that raise `NotExecuted` incorrectly (asserting no side effect when one occurred). This is an integration bug, and it is the most dangerous one available: `NotExecuted` is the one exception that makes an effect retryable, so an executor that raises it after the remote acted turns the one guarantee CTRLRun is built around into a licence to act twice. **`ctrlrun verify` does not and cannot check for it.** Verify reads the operator's configuration and supplies its own executors; it never calls the one behind `@protect` and never imports the module it lives in (SPEC-v0.4 §1.2). An earlier version of this line said v0.4 verify would include such a check. It does not, and the sentence was wrong when it was written. -- Data exfiltration through *read* actions the policy allows. CTRLRun is not DLP. +- A compromised approver, or social engineering of the approver. ctrlrun proves *what* was approved, not that the human was right. +- Executors that raise `NotExecuted` incorrectly (asserting no side effect when one occurred). This is an integration bug, and it is the most dangerous one available: `NotExecuted` is the one exception that makes an effect retryable, so an executor that raises it after the remote acted turns the one guarantee ctrlrun is built around into a licence to act twice. **`ctrlrun verify` does not and cannot check for it.** Verify reads the operator's configuration and supplies its own executors; it never calls the one behind `@protect` and never imports the module it lives in (SPEC-v0.4 §1.2). An earlier version of this line said v0.4 verify would include such a check. It does not, and the sentence was wrong when it was written. +- Data exfiltration through *read* actions the policy allows. ctrlrun is not DLP. - Denial of service by flooding approval requests. - Bypassing the decorator entirely (calling the raw function). v0.2 gateway mode narrows this; process-level enforcement is out of scope. -- **A compromised identity provider.** CTRLRun *consumes* identities: it verifies a token somebody else issued and maps the verified claims onto a `Principal`. It issues nothing, and an issuer that signs a token for the wrong subject has told CTRLRun the truth as far as CTRLRun can tell. Everything downstream — grants, delegation, receipts — is then wrong, correctly and consistently. +- **A compromised identity provider.** ctrlrun *consumes* identities: it verifies a token somebody else issued and maps the verified claims onto a `Principal`. It issues nothing, and an issuer that signs a token for the wrong subject has told ctrlrun the truth as far as ctrlrun can tell. Everything downstream — grants, delegation, receipts — is then wrong, correctly and consistently. - **A `HeaderIdentityProvider` behind a proxy that does not overwrite the header.** It is worth exactly what the thing setting it is worth, and RFC 7239 §8.1 says the same of the header it standardizes. If the agent can set the header, the agent chooses its own authority. It warns at construction and it is still the operator's call. - **A revoked token before its `exp`, where no feed is configured.** Without one, a verified token is valid until it expires, which is why one with no `exp` is refused, and short lifetimes are the whole of the story. Since v0.8 a deployment may pass `JWTIdentityProvider(revocations=...)` a feed of Security Event Tokens, and a credential the issuer revoked is then refused at resolution. Two things that closes less than they sound: **a revoked credential leaves a log line and no receipt**, because resolution happens before an action exists, where an *expired* one leaves a receipt; and **a feed is worth what its source is worth**. Somebody who can write the file, or stand in front of the poll endpoint, can refuse the operator's own agents at will, which is a denial of service against them and is fail-closed. They cannot admit a principal the issuer revoked: the feed is only ever consulted to refuse, and there is no path on which its answer makes an otherwise-invalid credential valid. - **A tenant-templated issuer.** `issuer` is matched as an exact string, so a multi-tenant endpoint cannot be configured correctly here. Pointing it at one without pinning the tenant makes every tenant on that platform a valid issuer — stated because the fail-open is inviting. -- **Authority across an agent-to-agent hop.** A grant covers the principal CTRLRun resolved for *this* call. Propagating attenuated authority across hops is v0.10. +- **Authority across an agent-to-agent hop.** A grant covers the principal ctrlrun resolved for *this* call. Propagating attenuated authority across hops is v0.10. - **Approving an authority change.** `ctrlrun delegate --as` is an assertion typed at a shell, not an authentication; the record keeps `created_via` so a reader can tell an act from an assertion. Authenticating the *approver* remains out of scope, as in v0.1. ## Known v0.4 limitations — what `ctrlrun verify` does not see @@ -144,7 +144,7 @@ cannot see matters more than the feature does, so it is here as well as in executors and never imports the operator's module. - **Not the operator's `reconcile` hooks**, for the same reason: a hook is a Python callable passed to `@protect`, and it does not appear in any file verify reads. -- **Not where the decorator was placed.** Code that calls the raw function bypasses CTRLRun +- **Not where the decorator was placed.** Code that calls the raw function bypasses ctrlrun entirely — the "bypassing the decorator" line above — and no amount of configuration-reading finds that. - **Not the deployment.** Whether the proxy in front of `HeaderIdentityProvider` overwrites the @@ -180,7 +180,7 @@ certified, not audited. - Approver identity is free text; no authentication of the approver (v0.3). - Receipts are not signed, and they are not signed after v0.6 either. v0.6 adds a **hash chain** (`SPEC-v0.6.md` §6): each receipt carries the hash of the one before it, with `seq` inside the hashed content, so a partial tamper is detected and named — an `UPDATE` on one row, a `DELETE` from the middle, a reordering. What that closes is **alteration that keeps the receipts after it**: changing what receipt *n* says while leaving the rest in place costs a rewrite of all of them plus the head, rather than one statement. **Not a truncation at the end, and not an append.** Two earlier versions of this line claimed the first; a review measured both at **two statements, undetected** — delete the rows and rewind the head, or insert a well-formed row and advance it. The head is a row in the same database as the receipts, so it raises the cost of *forgetting* and not the cost of erasing; an anchor outside the database is what closes that, and **v0.11 adds one**. -- **What the anchor changes, and exactly how far** (`SPEC-v0.11.md` §2.4, §3). An anchor records the pair the head holds, a `seq` and the hash at it, through a provider the operator supplies and CTRLRun does not ship. It **freezes a prefix**: anything at or below an anchored `seq` can no longer be removed or altered without the anchored pair failing to reproduce, and that is decided by the operator's own record rather than by a row in the database under suspicion. So the truncation measured above at two statements is now named `anchor_broken`, and so is the administrator who rewrites every row **including the head**, for everything at or below an anchored `seq`: the malicious-administrator line is narrowed again rather than removed. +- **What the anchor changes, and exactly how far** (`SPEC-v0.11.md` §2.4, §3). An anchor records the pair the head holds, a `seq` and the hash at it, through a provider the operator supplies and ctrlrun does not ship. It **freezes a prefix**: anything at or below an anchored `seq` can no longer be removed or altered without the anchored pair failing to reproduce, and that is decided by the operator's own record rather than by a row in the database under suspicion. So the truncation measured above at two statements is now named `anchor_broken`, and so is the administrator who rewrites every row **including the head**, for everything at or below an anchored `seq`: the malicious-administrator line is narrowed again rather than removed. **An append is still not detected**, and this is the half most likely to be misread. A forged receipt lands at head + 1, above every anchored `seq`, so nothing stops reproducing, and a later anchor freezes the forged chain as readily as an honest one. Receipts written and erased entirely **between** two anchors are not detected either, because they were never at or below an anchored `seq`. An earlier version of the roadmap said the anchor closed "a suffix erased or appended"; a review ran both cases and it closes only the first. @@ -200,7 +200,7 @@ model. They shipped in 0.2.0 and every one of them describes behaviour you can r scope-challenge `403` — to `FAILED`, permitting an automatic retry. They are the closest thing MCP offers to an executor raising `NotExecuted` (SPEC-v0.1 §5.5): the peer is stating in band that it rejected the request rather than running the method. An upstream that does - work and *then* returns `-32602` violates JSON-RPC 2.0, and CTRLRun will retry against a side + work and *then* returns `-32602` violates JSON-RPC 2.0, and ctrlrun will retry against a side effect that already landed. The alternative — mapping every error to `AMBIGUOUS` — makes a routine token expiry or a typo'd tool name cost a human `ctrlrun resolve`, which is how a guarantee becomes something people switch off. The asymmetry stays where v0.1 put it: @@ -240,7 +240,7 @@ model. They shipped in 0.2.0 and every one of them describes behaviour you can r in the events file. Cutting a chain of *unknown* width means setting `delegable: false` on the root grant and restarting, after which §5.6 rule 6 denies every descendant. - **Observe mode executes.** It is the rollout path, not a sandbox: effects land at remotes - and the records of them are real. What it suspends is CTRLRun's refusals, wholesale — every + and the records of them are real. What it suspends is ctrlrun's refusals, wholesale — every ⚠ row of `SPEC-v0.3.md` §9 at once. It is not a per-action opt-out and cannot be made one. - **A `mode: observe` writer and a ≤ 0.2 reader do not mix.** `ReceiptResult` gains `observed`, and `Receipt.from_dict` parses `result` into a closed enum — so an older process diff --git a/docs/adapters.md b/docs/adapters.md index 7f7c99e..9df8781 100644 --- a/docs/adapters.md +++ b/docs/adapters.md @@ -5,7 +5,7 @@ description: "The three ways in, when you do not need an adapter, what an adapte ## You probably do not need one -There are **three ways to put CTRLRun in front of a consequential action**, and only one of them +There are **three ways to put ctrlrun in front of a consequential action**, and only one of them is an adapter. | | Covers | Needs | @@ -36,7 +36,7 @@ the date read, and every place its framework's behaviour shows through the contr **Prevention or attribution** is the sentence to read first. `carries_approved_arguments = True` means the framework's resumption carries the arguments a human answered against, and core re-checks them against the proposal's `action_hash` — a mutated action is *refused*. -`False` means the framework carries nothing an adapter can inspect: CTRLRun records **who +`False` means the framework carries nothing an adapter can inspect: ctrlrun records **who answered** and cannot re-check **what they answered about**. Neither is a defect; they are different frameworks. An adapter that blurred the two would be the false-green problem in prose. @@ -85,7 +85,7 @@ Three things bite here, and all three were found the hard way: `control.policy.mode` is `observe` (§3.6). Otherwise a human is asked, and because your framework will not invoke a declined tool, their *no* **stops an action that observe mode promises to let run**. -- **The framework's answer is keyed to *its* unit, not to a CTRLRun action.** A tool body can +- **The framework's answer is keyed to *its* unit, not to a ctrlrun action.** A tool body can raise `ApprovalRequired` more than once. Bind the answer to the action you gated, and to one request, or one human "yes" authorizes everything raised under that call. - **Exceptions.** If your framework wraps or swallows what a tool raised, restore it (§12.7). diff --git a/docs/agents-you-cant-modify.mdx b/docs/agents-you-cant-modify.mdx index 9530018..88cc6c9 100644 --- a/docs/agents-you-cant-modify.mdx +++ b/docs/agents-you-cant-modify.mdx @@ -1,13 +1,13 @@ --- title: "Control AI agents you can't modify" sidebarTitle: "Agents you can't modify" -description: "WhatsApp, Slack and Teams bots, Claude Code, Cursor, Codex, ChatGPT: if an agent acts through your tools or API, CTRLRun checks the action first." +description: "WhatsApp, Slack and Teams bots, Claude Code, Cursor, Codex, ChatGPT: if an agent acts through your tools or API, ctrlrun checks the action first." --- -CTRLRun works with agents you can't modify as well as the ones you can, because it checks the +ctrlrun works with agents you can't modify as well as the ones you can, because it checks the action, not the agent. A WhatsApp, Slack or Teams bot, Claude Code, Cursor or Codex, a ChatGPT connector, a no-code builder: if the agent's actions reach a tool server or an API you run, -CTRLRun sits at that point and decides each one before it runs. The agent is not rebuilt, +ctrlrun sits at that point and decides each one before it runs. The agent is not rebuilt, redeployed or told. That is [the principle](/docs/why): autonomy belongs to the action, not the agent. If you own the agent's code, [Three ways in](/docs/get-started/three-ways-in) covers it. @@ -15,7 +15,7 @@ agent. If you own the agent's code, [Three ways in](/docs/get-started/three-ways It depends on where the action goes, not on who built the agent. -| The agent | For example | Where CTRLRun goes | +| The agent | For example | Where ctrlrun goes | |---|---|---| | Connects to a tool server you choose | A hosted assistant with a custom MCP connector | `ctrlrun gateway`, between the agent and the server | | Calls an API you own | A custom action, an OpenAPI tool, a webhook step in a no-code builder | `@ctrlrun.protect` on the handler that acts | @@ -27,7 +27,7 @@ The first two cover most business agents. The third is the one to check before y Any AI agent you have. An unlisted tool is covered like the row it resembles. -| Kind | Examples | How CTRLRun covers it | +| Kind | Examples | How ctrlrun covers it | |---|---|---| | Coding agents | Claude Code, Cursor, Codex, GitHub Copilot, Windsurf, Gemini CLI | Point their MCP config at the gateway. Their own shell and file edits do not pass through it; the MCP tools you give them do | | Hosted assistants | ChatGPT, Claude.ai, Microsoft Copilot, Gemini | A custom connector or action points at a public gateway or a protected API | @@ -85,7 +85,7 @@ action. [Protect a function](/docs/guides/protect-a-function) has the version th When a platform's agent uses the platform's own tool, the call starts and ends inside the platform. Meta AI sending a WhatsApp message never touches anything you run, so nothing you run -can check it, CTRLRun included. The way to a yes is to move the capability, not to intercept it: +can check it, ctrlrun included. The way to a yes is to move the capability, not to intercept it: 1. **Take the built-in away.** Turn off the native action, or run the agent under an account with no write access to the system that matters. diff --git a/docs/architecture/specifications.mdx b/docs/architecture/specifications.mdx index 1fad20a..c2c7ad1 100644 --- a/docs/architecture/specifications.mdx +++ b/docs/architecture/specifications.mdx @@ -3,7 +3,7 @@ title: "The specifications" description: "Six documents, one per version, each a delta over the ones before and each still binding. They live in the repository; this page says what each asked." --- -Every version of CTRLRun was a specification before it was code: what it must do, the +Every version of ctrlrun was a specification before it was code: what it must do, the acceptance tests it is judged by, the public names it freezes, and what is deliberately out of scope. All six are still binding in full, and nothing in a later one relaxes an earlier one. diff --git a/docs/authority.md b/docs/authority.md index 4a95a2e..1309cce 100644 --- a/docs/authority.md +++ b/docs/authority.md @@ -4,7 +4,7 @@ description: "Grants, containment and the omission rule in plain language: who m sidebarTitle: "Authority in depth" --- -Until v0.3, a CTRLRun policy could see the action and nothing else. It answered *how much +Until v0.3, a ctrlrun policy could see the action and nothing else. It answered *how much autonomy does this action have* — run it, ask a human, refuse it — and the principal was attribution on a receipt. That is a real question and it is still the one `actions:` answers. diff --git a/docs/compare/durable-workflows.mdx b/docs/compare/durable-workflows.mdx index 4fd0188..eb402ff 100644 --- a/docs/compare/durable-workflows.mdx +++ b/docs/compare/durable-workflows.mdx @@ -1,11 +1,11 @@ --- -title: "CTRLRun and durable workflow engines" +title: "ctrlrun and durable workflow engines" sidebarTitle: "vs durable workflows" -description: "A workflow engine makes a workflow finish, retrying until it succeeds. CTRLRun decides whether an effect may happen at all, and never retries an unknown." +description: "A workflow engine makes a workflow finish, retrying until it succeeds. ctrlrun decides whether an effect may happen at all, and never retries an unknown." --- A durable workflow engine guarantees that a workflow finishes: it persists every step, replays -after a crash, and retries an activity until it succeeds. CTRLRun guarantees that a consequential +after a crash, and retries an activity until it succeeds. ctrlrun guarantees that a consequential effect is authorized and happens at most once, and refuses to retry when nobody knows whether it already happened. One drives work forward; the other decides whether the work may happen. They compose, and the second question is not the first one's job. @@ -16,11 +16,11 @@ Long-running processes that must survive a crash, a deploy or a week of waiting. replay. Timers, signals, child workflows, fan-out. Visibility into where a workflow is. An activity that fails transiently and should be retried until the network cooperates. If your agent's work is a multi-step process with state, that is exactly the problem they solve, and -CTRLRun does not solve it. +ctrlrun does not solve it. ## What they do not do -| | Durable engines | CTRLRun | +| | Durable engines | ctrlrun | |---|---|---| | Guarantee | the workflow progresses; activities run at least once | the effect happens at most once per intent, or not at all | | Retry policy | retry until success is the default | a retry against an unknown outcome is refused | @@ -31,7 +31,7 @@ CTRLRun does not solve it. | Evidence | the workflow history | a receipt per action, portable, chained, readable without the engine | The sharp edge is the retry default. Retry until it succeeds is right for a read and wrong for a -refund, and the usual advice, make your activities idempotent, is exactly the work CTRLRun does +refund, and the usual advice, make your activities idempotent, is exactly the work ctrlrun does for you: an effect key per consequence, reserved atomically, enforced by the store rather than by convention. @@ -45,8 +45,8 @@ effect key already reserved. ## The distinction that matters -An engine asks *has this step finished*. CTRLRun asks *did this effect happen, and may it happen -now*. An engine that cannot get an answer retries. CTRLRun that cannot get an answer stops and +An engine asks *has this step finished*. ctrlrun asks *did this effect happen, and may it happen +now*. An engine that cannot get an answer retries. ctrlrun that cannot get an answer stops and says so. ## Next diff --git a/docs/compare/framework-hitl.mdx b/docs/compare/framework-hitl.mdx index 52e1b5d..aacf853 100644 --- a/docs/compare/framework-hitl.mdx +++ b/docs/compare/framework-hitl.mdx @@ -1,10 +1,10 @@ --- -title: "CTRLRun and framework human-in-the-loop" +title: "ctrlrun and framework human-in-the-loop" sidebarTitle: "vs framework HITL" description: "A framework interrupt is where a human says yes. It does not bind that yes to the arguments that execute, or refuse a retry after a lost reply. Use both." --- -A framework's human-in-the-loop primitive is the right place for a human to answer, and CTRLRun +A framework's human-in-the-loop primitive is the right place for a human to answer, and ctrlrun uses it rather than replacing it. What it does not do is bind that answer to the exact action that executes, notice that the same effect already happened, or leave evidence outside the framework's own run state. Those are different jobs, and an adapter joins them. @@ -19,7 +19,7 @@ replaces that; the adapters exist so a human keeps answering there. ## What it does not do -| | A framework interrupt | CTRLRun | +| | A framework interrupt | ctrlrun | |---|---|---| | Where the yes is given | in the framework's own console | the same place, through an adapter | | What the yes authorizes | a tool call, identified by the framework's id | one canonical action, identified by a SHA-256 over its name, arguments, resource, principal and environment | @@ -42,10 +42,10 @@ anyway, and `ApprovalRequired` is raised for your own code to handle. ## The distinction that matters -A framework binds an approval to *a call it is about to make*. CTRLRun binds it to *what that +A framework binds an approval to *a call it is about to make*. ctrlrun binds it to *what that call would do*. Where the framework's resumption carries the arguments the human saw, an -adapter hands them back and CTRLRun re-checks the hash: prevention. Where it carries only a -verdict, CTRLRun records who answered and cannot re-check what about: attribution. Each +adapter hands them back and ctrlrun re-checks the hash: prevention. Where it carries only a +verdict, ctrlrun records who answered and cannot re-check what about: attribution. Each adapter's page says which it is, in that word. ## Next diff --git a/docs/compare/governance-toolkits.mdx b/docs/compare/governance-toolkits.mdx index 5d69090..6c2ce0d 100644 --- a/docs/compare/governance-toolkits.mdx +++ b/docs/compare/governance-toolkits.mdx @@ -1,24 +1,24 @@ --- -title: "CTRLRun and agent oversight toolkits" +title: "ctrlrun and agent oversight toolkits" sidebarTitle: "vs oversight toolkits" -description: "Oversight toolkits catalogue agents, monitor their behaviour and report on it. CTRLRun refuses, in the execution path, per action, and emits the evidence." +description: "Oversight toolkits catalogue agents, monitor their behaviour and report on it. ctrlrun refuses, in the execution path, per action, and emits the evidence." --- An oversight toolkit answers questions about a fleet: which agents exist, what they are allowed -to touch, what they did last week, and whether anything looks unusual. CTRLRun answers one +to touch, what they did last week, and whether anything looks unusual. ctrlrun answers one question about one action, in the moment before it happens: may this run, has it already run, -and what is recorded. A toolkit describes; CTRLRun refuses. +and what is recorded. A toolkit describes; ctrlrun refuses. ## What oversight toolkits are good at An inventory of agents and their owners. Central configuration across teams. Dashboards, anomaly detection and reporting over what agents did. Mapping activity to internal control frameworks. Answering someone who asks what runs in production. These are real problems, they -are organisational rather than per-call, and CTRLRun does none of them. +are organisational rather than per-call, and ctrlrun does none of them. ## What they do not do -| | Oversight toolkits | CTRLRun | +| | Oversight toolkits | ctrlrun | |---|---|---| | Where it sits | beside the agent, reading its activity | in the call path, between the decision and the effect | | When it acts | after, or at configuration time | before the executor runs | @@ -32,13 +32,13 @@ are organisational rather than per-call, and CTRLRun does none of them. ## When to use both A fleet needs both kinds of answer. Use the toolkit for the inventory, the reporting and the -organisational questions, and put CTRLRun in the path of the actions that cannot be undone. The +organisational questions, and put ctrlrun in the path of the actions that cannot be undone. The receipts are portable JSON and go wherever your reporting lives; the OpenTelemetry sink puts each action in the same traces your platform already collects. -## What CTRLRun will not claim +## What ctrlrun will not claim -CTRLRun makes no standards claim and does not map itself to a control framework as a product +ctrlrun makes no standards claim and does not map itself to a control framework as a product feature. `controls:` in a policy lets *you* name the house control an action satisfies and cites it on the receipt, uninterpreted. The reading of the OWASP Top 10 for Agentic Applications in this repository names the four entries it does not address. Enforcement in the path and evidence diff --git a/docs/compare/guardrail-libraries.mdx b/docs/compare/guardrail-libraries.mdx index dffdecf..160ffd0 100644 --- a/docs/compare/guardrail-libraries.mdx +++ b/docs/compare/guardrail-libraries.mdx @@ -1,11 +1,11 @@ --- -title: "CTRLRun and guardrail libraries" +title: "ctrlrun and guardrail libraries" sidebarTitle: "vs guardrail libraries" -description: "Guardrail libraries inspect what goes into and comes out of a model. CTRLRun sits at the boundary between deciding to act and having acted." +description: "Guardrail libraries inspect what goes into and comes out of a model. ctrlrun sits at the boundary between deciding to act and having acted." --- A guardrail library reads text: the prompt going in, the completion coming out, sometimes a -tool call's arguments, and it blocks or rewrites what it does not like. CTRLRun does not read +tool call's arguments, and it blocks or rewrites what it does not like. ctrlrun does not read text at all. It sits one layer down, where a decision becomes an effect, and asks whether this exact action may run, whether it has already run, and what to record. @@ -18,7 +18,7 @@ that live in the text. ## What they do not do -| | Guardrails | CTRLRun | +| | Guardrails | ctrlrun | |---|---|---| | Input | prompts, completions, tool arguments as text | one canonical action: name, arguments, resource, principal, environment | | Question asked | is this content acceptable | may this action run, has this effect already happened, what happened | @@ -29,27 +29,27 @@ that live in the text. | What it cannot see | the second execution | the injected instruction that reads as legitimate text | That last row is the pair. A guardrail can spot an injected instruction in the page an agent -read. CTRLRun cannot: the refund request that arrives looks exactly like a real one. What -CTRLRun does instead is make the *consequence* survivable: the refund needs a grant the agent +read. ctrlrun cannot: the refund request that arrives looks exactly like a real one. What +ctrlrun does instead is make the *consequence* survivable: the refund needs a grant the agent does not hold, the amount needs a human, the recipient is bound to what the human saw, and the effect happens once. ## When to use both Most deployments that matter want both, in this order: a guardrail on the model's input and -output, and CTRLRun in front of the calls that change the world. They fail differently, which is +output, and ctrlrun in front of the calls that change the world. They fail differently, which is the point of having two. A guardrail that misses one injection has let a request through; if that request must still get past authority, policy, a human and an effect key, the injection has not bought much. ## The distinction that matters -Guardrails are about what is *said*. CTRLRun is about what is *done*. A library that filters +Guardrails are about what is *said*. ctrlrun is about what is *done*. A library that filters text cannot tell you whether the refund happened twice, and a library that owns effects cannot tell you whether the completion was rude. ## Next -- [Why](/docs/why): the boundary CTRLRun owns. +- [Why](/docs/why): the boundary ctrlrun owns. - [Fail closed](/docs/concepts/fail-closed) · [Effect keys](/docs/concepts/effect-keys). - [Get started](/docs/get-started/quickstart). diff --git a/docs/compare/idempotency-keys.mdx b/docs/compare/idempotency-keys.mdx index 06a6d69..23dd3e0 100644 --- a/docs/compare/idempotency-keys.mdx +++ b/docs/compare/idempotency-keys.mdx @@ -1,5 +1,5 @@ --- -title: "CTRLRun and idempotency keys" +title: "ctrlrun and idempotency keys" sidebarTitle: "vs idempotency keys" description: "An idempotency key deduplicates at one API that chose to support it. An effect key deduplicates at the agent, across every API it touches." --- @@ -33,7 +33,7 @@ them well, you have solved a large part of this problem. The row that matters most is the unknown outcome. Resending with an idempotency key is the right move when the remote implements them and the window has not passed. It is a guess when the remote does not, when the call went to a second API, when the window expired, or when the client -never learned whether the first request arrived. CTRLRun refuses to guess and makes someone +never learned whether the first request arrived. ctrlrun refuses to guess and makes someone look. ## When to use both diff --git a/docs/concepts/approval-binding.mdx b/docs/concepts/approval-binding.mdx index 2bd86de..1c202ee 100644 --- a/docs/concepts/approval-binding.mdx +++ b/docs/concepts/approval-binding.mdx @@ -38,8 +38,8 @@ tool-approval interruption: every path ends in the same two store calls, `grant_ one nobody is watching. Where a framework carries the arguments the human answered against, the adapter hands them -back and CTRLRun rebuilds the hash and compares: that is *prevention*. Where it carries only -the verdict, CTRLRun records who answered and cannot re-check what they answered about: that +back and ctrlrun rebuilds the hash and compares: that is *prevention*. Where it carries only +the verdict, ctrlrun records who answered and cannot re-check what they answered about: that is *attribution*, and the adapter's page says which it is, in that word. ## The guarantee it supports @@ -49,7 +49,7 @@ by `ctrlrun verify` against your own policy, wherever it has an `approve` rule. ## What it does not do -An approval does not prove the human was right, or that the human was who they said; CTRLRun +An approval does not prove the human was right, or that the human was who they said; ctrlrun records the approver as given and authenticates nobody. It does not survive a policy change in one direction: if the policy now denies the action, the action is refused and the approval is left granted; if the policy now allows it outright, the action runs and the approval is diff --git a/docs/concepts/authority-and-delegation.mdx b/docs/concepts/authority-and-delegation.mdx index 68c476a..2279ee3 100644 --- a/docs/concepts/authority-and-delegation.mdx +++ b/docs/concepts/authority-and-delegation.mdx @@ -65,7 +65,7 @@ finance agent delegates €50,000 ✗ containment: constraints The principal comes from an identity provider the operator installs: `ctrlrun.context(agent=...)` for a process that knows who it is, a `HeaderIdentityProvider` behind a proxy that authenticates, or a `JWTIdentityProvider` that verifies a bearer token against a JWKS or a pinned key and maps -the verified claims onto a principal. CTRLRun issues no credential and defines no identity +the verified claims onto a principal. ctrlrun issues no credential and defines no identity format. An expired credential is refused before authority and before policy. ## The guarantee it supports diff --git a/docs/concepts/fail-closed.mdx b/docs/concepts/fail-closed.mdx index f69251e..4c0bd18 100644 --- a/docs/concepts/fail-closed.mdx +++ b/docs/concepts/fail-closed.mdx @@ -3,7 +3,7 @@ title: "Fail closed" description: "An unknown action, a missing or malformed policy, a missing or expired principal, a missing or mismatched approval: every one of them is denied." --- -Fail closed means that anything CTRLRun cannot decide, it denies. An unknown action, a missing +Fail closed means that anything ctrlrun cannot decide, it denies. An unknown action, a missing policy, a malformed policy, a missing or expired principal, a missing, expired, consumed or mismatched approval, a template that cannot be resolved, a store whose schema it does not recognise: all refused, before the executor runs. There is no flag that makes a consequential @@ -42,7 +42,7 @@ every row above has an acceptance test. ## What it does not do -Failing closed is about what CTRLRun decides. It cannot refuse a call that bypasses the decorator +Failing closed is about what ctrlrun decides. It cannot refuse a call that bypasses the decorator entirely, a compromised host, or an executor that raises `NotExecuted` after the remote acted; those are in the threat model as limits, not as vulnerabilities. diff --git a/docs/concepts/observe-mode.mdx b/docs/concepts/observe-mode.mdx index 022379a..e9f130b 100644 --- a/docs/concepts/observe-mode.mdx +++ b/docs/concepts/observe-mode.mdx @@ -3,7 +3,7 @@ title: "Observe mode" description: "mode: observe runs every real decision against real traffic and records what enforcement would have blocked, without blocking anything." --- -Observe mode is one top-level line, `mode: observe`, that makes CTRLRun evaluate every action +Observe mode is one top-level line, `mode: observe`, that makes ctrlrun evaluate every action exactly as it would in enforce mode, execute it regardless, and record on the receipt what would have been blocked and why. It is how a rollout measures before it enforces. It is not a dry run: the executor runs and effects land at remotes. diff --git a/docs/concepts/outcomes-and-ambiguous.mdx b/docs/concepts/outcomes-and-ambiguous.mdx index 0e5a891..c41a51f 100644 --- a/docs/concepts/outcomes-and-ambiguous.mdx +++ b/docs/concepts/outcomes-and-ambiguous.mdx @@ -3,7 +3,7 @@ title: "Outcomes and AMBIGUOUS" description: "An executed action ends COMMITTED, FAILED or AMBIGUOUS. Only NotExecuted, raised by the executor, means FAILED." --- -An outcome is what CTRLRun knows about the consequence after the executor returns or raises, +An outcome is what ctrlrun knows about the consequence after the executor returns or raises, and there are three: `COMMITTED`, the remote did it; `FAILED`, the remote definitely did not; and `AMBIGUOUS`, nobody knows. A timeout is not a failure. A lost reply is not a failure. An exception nobody expected is not a failure. All three are `AMBIGUOUS`, and an `AMBIGUOUS` effect @@ -93,7 +93,7 @@ ambiguous) in `ctrlrun verify`. ## What it does not do -CTRLRun cannot find out what the remote did. It refuses to guess, and it makes the question +ctrlrun cannot find out what the remote did. It refuses to guess, and it makes the question impossible to skip. It also cannot tell that an executor lied with `NotExecuted`; that is the integration bug the threat model names as the most dangerous one available. diff --git a/docs/concepts/receipts-and-evidence.mdx b/docs/concepts/receipts-and-evidence.mdx index 38732df..6e70ffa 100644 --- a/docs/concepts/receipts-and-evidence.mdx +++ b/docs/concepts/receipts-and-evidence.mdx @@ -71,7 +71,7 @@ asserted to carry every field the specification names. ## What it does not do -A receipt records what CTRLRun saw, not what the remote did after the reply was lost; an +A receipt records what ctrlrun saw, not what the remote did after the reply was lost; an `AMBIGUOUS` outcome is recorded as ambiguous, and the resolution, when it comes, is a further event naming who resolved it. A receipt that failed to write leaves no gap in `seq`; the events log is where that is reconciled. And receipts are evidence, not a dashboard: there is no UI, by diff --git a/docs/cookbook/openai-agents-tool-approval.mdx b/docs/cookbook/openai-agents-tool-approval.mdx index 21414b3..8138a66 100644 --- a/docs/cookbook/openai-agents-tool-approval.mdx +++ b/docs/cookbook/openai-agents-tool-approval.mdx @@ -65,7 +65,7 @@ print(result.final_output) The SDK asks before invoking, because `protected_tool` answers its `needs_approval` from the policy; the run returns with one interruption naming the tool and its arguments. After `state.approve(item)` the resumed run invokes the tool with exactly that call's arguments, and -CTRLRun records `openai-agents:tool-approval` as the approver. A refusal by CTRLRun, a +ctrlrun records `openai-agents:tool-approval` as the approver. A refusal by ctrlrun, a duplicate for instance, reaches your `except` as `DuplicateEffect` through `gate.run`, not the model as "please try again". @@ -73,7 +73,7 @@ model as "please try again". `approve/committed`. The binding across the interrupt is the SDK's, keyed by `call_id`, so the receipt attributes the approval and cannot re-check the arguments against the hash: that is -attribution, and the adapter's README says so in that word. A rejection leaves no CTRLRun +attribution, and the adapter's README says so in that word. A rejection leaves no ctrlrun evidence at all, because the SDK never invokes a rejected tool; record it where you call `state.reject(item)`. diff --git a/docs/cookbook/receipts-to-opentelemetry.mdx b/docs/cookbook/receipts-to-opentelemetry.mdx index 38aa231..0a74fd3 100644 --- a/docs/cookbook/receipts-to-opentelemetry.mdx +++ b/docs/cookbook/receipts-to-opentelemetry.mdx @@ -94,7 +94,7 @@ stripe.refund status=ERROR result=ambiguous events=5 argument values in attributes: none ``` -A refusal is `UNSET`, not an error: CTRLRun doing its job is not a fault in the trace. +A refusal is `UNSET`, not an error: ctrlrun doing its job is not a fault in the trace. ## The receipt @@ -103,7 +103,7 @@ ctrlrun receipts --last 3 ``` The receipts are still in the store and the JSONL file, chained; the spans carry the receipt -id so a trace can be joined back to the evidence. Deleting a trace deletes nothing CTRLRun +id so a trace can be joined back to the evidence. Deleting a trace deletes nothing ctrlrun relies on. ## When an AMBIGUOUS appears diff --git a/docs/cookbook/verify-in-github-actions.mdx b/docs/cookbook/verify-in-github-actions.mdx index f7e0612..196857a 100644 --- a/docs/cookbook/verify-in-github-actions.mdx +++ b/docs/cookbook/verify-in-github-actions.mdx @@ -1,6 +1,6 @@ --- title: "Run verify in GitHub Actions" -description: "Run ctrlrun verify against your policy on every push with the CTRLRun action: the workflow, the report it produces, the N/A line, the exit codes." +description: "Run ctrlrun verify against your policy on every push with the ctrlrun action: the workflow, the report it produces, the N/A line, the exit codes." --- Your policy lives in the repository with the agent. Every push should prove the guarantees it @@ -38,7 +38,7 @@ rm -f verify-report.json In CI, the workflow: ```yaml -name: CTRLRun verify +name: ctrlrun verify on: [push, pull_request] @@ -62,7 +62,7 @@ by tag where you want a ref nobody can move. The agent sees nothing; this is the operator's check. The build sees: ```text -CTRLRun verify — ctrlrun 0.12.2, catalogue ctrlrun.guarantees/v7 +ctrlrun verify — ctrlrun 0.12.2, catalogue ctrlrun.guarantees/v7 policy /Users/arpanghoshal/ctrlrun-project/wt/v11-i3/examples/cookbook/verify-in-github-actions/ctrlrun.yaml (ctrlrun.policy/v2, mode: enforce) authority none store sqlite, scratch (created and destroyed for this run) diff --git a/docs/faq.mdx b/docs/faq.mdx index 967fd7a..f1b7523 100644 --- a/docs/faq.mdx +++ b/docs/faq.mdx @@ -32,8 +32,8 @@ The fourteen questions that come up first, answered in under eighty words each. <Accordion title="Why not a durable workflow engine?"> Different guarantee. An engine makes a workflow finish, retrying activities until they - succeed; CTRLRun decides whether a consequential effect is authorized and refuses to retry - an unknown outcome. Their advice, make activities idempotent, is the work CTRLRun does for + succeed; ctrlrun decides whether a consequential effect is authorized and refuses to retry + an unknown outcome. Their advice, make activities idempotent, is the work ctrlrun does for you and enforces in the store. Run the workflow in the engine and decorate the activity that acts. [More](/docs/compare/durable-workflows). </Accordion> @@ -46,10 +46,10 @@ The fourteen questions that come up first, answered in under eighty words each. </Accordion> <Accordion title="Is it exactly-once?"> - No, and nothing can be against systems it does not control. CTRLRun guarantees it will not + No, and nothing can be against systems it does not control. ctrlrun guarantees it will not knowingly execute the same logical effect twice, and never treats an unknown outcome as a failure. The remote is the only thing that knows what the remote did; when nobody knows, - CTRLRun says so and stops. [More](/docs/concepts/outcomes-and-ambiguous). + ctrlrun says so and stops. [More](/docs/concepts/outcomes-and-ambiguous). </Accordion> <Accordion title="What happens on a timeout?"> @@ -62,7 +62,7 @@ The fourteen questions that come up first, answered in under eighty words each. <Accordion title="Can the agent bypass it?"> It can call the undecorated function, and the threat model says so: process-level enforcement is out of scope. Two things narrow it. The gateway sits between the agent and - its tools, where the agent has no choice; and CTRLRun is never a tool the agent decides to + its tools, where the agent has no choice; and ctrlrun is never a tool the agent decides to call, because a check the agent opts into is not a check. [Threat model](/docs/THREAT_MODEL). </Accordion> @@ -115,20 +115,20 @@ The fourteen questions that come up first, answered in under eighty words each. "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ - {"@type": "Question", "name": "Is CTRLRun production-ready?", "acceptedAnswer": {"@type": "Answer", "text": "It runs in production on a single file or on Postgres across hosts, and every guarantee is graded by one suite against both stores. It has not had an external security audit, a third-party review of the kernel, or a soak of the length the roadmap asks for."}}, + {"@type": "Question", "name": "Is ctrlrun production-ready?", "acceptedAnswer": {"@type": "Answer", "text": "It runs in production on a single file or on Postgres across hosts, and every guarantee is graded by one suite against both stores. It has not had an external security audit, a third-party review of the kernel, or a soak of the length the roadmap asks for."}}, {"@type": "Question", "name": "Is SQLite really enough for production?", "acceptedAnswer": {"@type": "Answer", "text": "On one host, yes. BEGIN IMMEDIATE is a write lock on the file, so one effect executes once across threads and across OS processes on that machine. Move to Postgres when a second host must write to the store, not before."}}, {"@type": "Question", "name": "Isn't this just idempotency keys?", "acceptedAnswer": {"@type": "Answer", "text": "No. An idempotency key deduplicates at one API that chose to support it, inside its retention window. An effect key deduplicates at the agent, before the call, across every remote it touches, is bound to the approval and the receipt, and refuses a retry when the outcome is unknown."}}, - {"@type": "Question", "name": "Why not a durable workflow engine?", "acceptedAnswer": {"@type": "Answer", "text": "A workflow engine makes a workflow finish, retrying activities until they succeed. CTRLRun decides whether a consequential effect is authorized and refuses to retry an unknown outcome. Run the workflow in the engine and decorate the activity that acts."}}, + {"@type": "Question", "name": "Why not a durable workflow engine?", "acceptedAnswer": {"@type": "Answer", "text": "A workflow engine makes a workflow finish, retrying activities until they succeed. ctrlrun decides whether a consequential effect is authorized and refuses to retry an unknown outcome. Run the workflow in the engine and decorate the activity that acts."}}, {"@type": "Question", "name": "Do I need an adapter for my framework?", "acceptedAnswer": {"@type": "Answer", "text": "Probably not. The @protect decorator covers anything in your Python process and the gateway covers anything behind an MCP server in any language. An adapter only routes an approval through the framework's own interrupt."}}, - {"@type": "Question", "name": "Is CTRLRun exactly-once?", "acceptedAnswer": {"@type": "Answer", "text": "No, and nothing can be against systems it does not control. CTRLRun guarantees it will not knowingly execute the same logical effect twice, and never treats an unknown outcome as a failure."}}, + {"@type": "Question", "name": "Is ctrlrun exactly-once?", "acceptedAnswer": {"@type": "Answer", "text": "No, and nothing can be against systems it does not control. ctrlrun guarantees it will not knowingly execute the same logical effect twice, and never treats an unknown outcome as a failure."}}, {"@type": "Question", "name": "What happens on a timeout?", "acceptedAnswer": {"@type": "Answer", "text": "The effect becomes AMBIGUOUS, never FAILED, and a retry against it is refused. Only NotExecuted, raised by your executor when it knows the remote did nothing, means failed. A human or a reconcile hook resolves it."}}, - {"@type": "Question", "name": "Can the agent bypass CTRLRun?", "acceptedAnswer": {"@type": "Answer", "text": "It can call the undecorated function; process-level enforcement is out of scope. The gateway narrows this by sitting between the agent and its tools, where the agent has no choice."}}, - {"@type": "Question", "name": "Does CTRLRun phone home?", "acceptedAnswer": {"@type": "Answer", "text": "No. There is no telemetry, licence check or network call in the kernel. ctrlrun stats counts the local SQLite file, and a test runs the demo in a subprocess whose sockets are all refused."}}, + {"@type": "Question", "name": "Can the agent bypass ctrlrun?", "acceptedAnswer": {"@type": "Answer", "text": "It can call the undecorated function; process-level enforcement is out of scope. The gateway narrows this by sitting between the agent and its tools, where the agent has no choice."}}, + {"@type": "Question", "name": "Does ctrlrun phone home?", "acceptedAnswer": {"@type": "Answer", "text": "No. There is no telemetry, licence check or network call in the kernel. ctrlrun stats counts the local SQLite file, and a test runs the demo in a subprocess whose sockets are all refused."}}, {"@type": "Question", "name": "What if the human takes an hour to approve?", "acceptedAnswer": {"@type": "Answer", "text": "The approval request expires at its TTL, fifteen minutes by default, and a waiting call raises ApprovalTimeout with nothing executed. An agent can also surface the request id and come back later."}}, - {"@type": "Question", "name": "Does CTRLRun work across many hosts?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. On one host the store is a SQLite file. Across hosts, install the postgres extra and change the store: a unique index on the effect key and compare-and-set updates give the same guarantee."}}, - {"@type": "Question", "name": "What is in a CTRLRun receipt?", "acceptedAnswer": {"@type": "Answer", "text": "Who proposed the action, its canonical arguments, the decision and why, the approval and approver, the effect key, the outcome, the timestamps, the policy hash and version, and the hash of the receipt before it."}}, + {"@type": "Question", "name": "Does ctrlrun work across many hosts?", "acceptedAnswer": {"@type": "Answer", "text": "Yes. On one host the store is a SQLite file. Across hosts, install the postgres extra and change the store: a unique index on the effect key and compare-and-set updates give the same guarantee."}}, + {"@type": "Question", "name": "What is in a ctrlrun receipt?", "acceptedAnswer": {"@type": "Answer", "text": "Who proposed the action, its canonical arguments, the decision and why, the approval and approver, the effect key, the outcome, the timestamps, the policy hash and version, and the hash of the receipt before it."}}, {"@type": "Question", "name": "Is the receipt chain a signature?", "acceptedAnswer": {"@type": "Answer", "text": "No. The chain detects alteration and names it by seq. It does not prove who wrote a receipt, receipts are not signed, and it does not survive an administrator who can rewrite every row including the chain head."}}, - {"@type": "Question", "name": "What does CTRLRun not cover?", "acceptedAnswer": {"@type": "Answer", "text": "A compromised host, a malicious administrator with write access to the store, a lying remote, a compromised approver, an executor that raises NotExecuted after the remote acted, data exfiltration through reads, and authority across an agent-to-agent hop."}} + {"@type": "Question", "name": "What does ctrlrun not cover?", "acceptedAnswer": {"@type": "Answer", "text": "A compromised host, a malicious administrator with write access to the store, a lying remote, a compromised approver, an executor that raises NotExecuted after the remote acted, data exfiltration through reads, and authority across an agent-to-agent hop."}} ] })} </script> diff --git a/docs/get-started/quickstart.mdx b/docs/get-started/quickstart.mdx index 4a122a3..271a8c0 100644 --- a/docs/get-started/quickstart.mdx +++ b/docs/get-started/quickstart.mdx @@ -16,7 +16,7 @@ would rather see it before you type it, [the execution boundary](/execution-boun same decisions with no install. <div className="cr-film-block"> - <DemoFilm src="/images/demo.mp4" poster="/images/demo-poster.jpg" label="A terminal recording of a CTRLRun policy file and the decisions it produces. Captions are shown in the video." /> + <DemoFilm src="/images/demo.mp4" poster="/images/demo-poster.jpg" label="A terminal recording of a ctrlrun policy file and the decisions it produces. Captions are shown in the video." /> <p className="cr-caption">Forty-nine seconds: the policy file below, and the refusals it produces. Plays muted with captions.</p> </div> @@ -173,7 +173,7 @@ same decisions with no install. presenting the same key would have been refused. - **Receipts.** Everything above is in the evidence log, in order. -What you did not see is a lost reply. That is the case CTRLRun exists for, and +What you did not see is a lost reply. That is the case ctrlrun exists for, and [Outcomes and AMBIGUOUS](/docs/concepts/outcomes-and-ambiguous) is where to read it next. ## If it didn't work diff --git a/docs/get-started/three-ways-in.mdx b/docs/get-started/three-ways-in.mdx index 3526de1..375a1e4 100644 --- a/docs/get-started/three-ways-in.mdx +++ b/docs/get-started/three-ways-in.mdx @@ -3,7 +3,7 @@ title: "Three ways in" description: "The @protect decorator covers anything in your Python process, the MCP gateway covers tools behind an MCP server in any language." --- -There are three ways to put CTRLRun in front of a consequential action, and only one of them is +There are three ways to put ctrlrun in front of a consequential action, and only one of them is an adapter. Most readers need the decorator and should not look for an adapter. | | Covers | Needs | @@ -42,7 +42,7 @@ decorated call. Everything else on the wire is relayed untouched. ```text before agent ──▶ MCP server -after agent ──▶ CTRLRun gateway ──▶ MCP server +after agent ──▶ ctrlrun gateway ──▶ MCP server ``` ```bash diff --git a/docs/guides/export-to-opentelemetry.mdx b/docs/guides/export-to-opentelemetry.mdx index 5f5a952..7949a2f 100644 --- a/docs/guides/export-to-opentelemetry.mdx +++ b/docs/guides/export-to-opentelemetry.mdx @@ -83,7 +83,7 @@ the spans without one). <Step title="Read the span"> The span's status is an error for `failed` and `ambiguous`, unset for a refusal (a refusal - is CTRLRun doing its job, not an error), and ok for `committed`. Attributes carry the action + is ctrlrun doing its job, not an error), and ok for `committed`. Attributes carry the action name, the decision, the effect key, the outcome and the receipt id; the receipt itself stays in the store. Open spans are bounded, so a process that dies mid-action leaves at most a fixed number unended, stated rather than solved. @@ -94,7 +94,7 @@ the spans without one). It is not the evidence. Receipts live in the store and the JSONL file, are chained, and are what `ctrlrun receipts --verify-chain` checks; a trace is a view of them for the people who -already look at traces. Deleting a trace deletes nothing CTRLRun relies on. +already look at traces. Deleting a trace deletes nothing ctrlrun relies on. ## If it didn't work diff --git a/docs/guides/gateway-in-front-of-mcp.mdx b/docs/guides/gateway-in-front-of-mcp.mdx index 299a21a..286583f 100644 --- a/docs/guides/gateway-in-front-of-mcp.mdx +++ b/docs/guides/gateway-in-front-of-mcp.mdx @@ -11,7 +11,7 @@ language. ```text before agent ──▶ MCP server -after agent ──▶ CTRLRun gateway ──▶ MCP server +after agent ──▶ ctrlrun gateway ──▶ MCP server ``` **Prerequisites:** an MCP server reachable over HTTP, `pip install "ctrlrun[gateway]"`, and a @@ -56,7 +56,7 @@ validated, never trusted. ``` ```text - CTRLRun gateway → http://localhost:8000/mcp as mcp.acme.* + ctrlrun gateway → http://localhost:8000/mcp as mcp.acme.* listening on http://127.0.0.1:8900/mcp environment: production identity: fixed principal refund-agent authority: none 1 action(s) have no effect: template and get no reservation: diff --git a/docs/guides/langchain-middleware.mdx b/docs/guides/langchain-middleware.mdx index 924b40a..8823e24 100644 --- a/docs/guides/langchain-middleware.mdx +++ b/docs/guides/langchain-middleware.mdx @@ -3,7 +3,7 @@ title: "Use the LangChain middleware" description: "Gate every LangChain tool call with ctrlrun-langchain, through wrap_tool_call, so a refused call never runs and every decision leaves a receipt." --- -This guide provides a quick overview for getting started with the CTRLRun [middleware](https://docs.langchain.com/oss/langchain/middleware/overview). CTRLRun checks every tool call your agent makes against a policy you write, before the call runs, and records what happened after. +This guide provides a quick overview for getting started with the ctrlrun [middleware](https://docs.langchain.com/oss/langchain/middleware/overview). ctrlrun checks every tool call your agent makes against a policy you write, before the call runs, and records what happened after. ## Overview @@ -25,7 +25,7 @@ This guide provides a quick overview for getting started with the CTRLRun [middl ## Setup -No account and no API key. CTRLRun is a library, and the policy is a file in your repository. +No account and no API key. ctrlrun is a library, and the policy is a file in your repository. ### Installation @@ -84,10 +84,10 @@ Every protected call needs a principal: who is acting is an authorization input, The middleware uses [`wrap_tool_call`](https://docs.langchain.com/oss/langchain/middleware/custom), so a refused call is short-circuited — the tool is never invoked, and the model receives a `ToolMessage` explaining why: ```text -issue_refund amount=900000 CTRLRun refused this call: rule[2]. The tool did not run. -rm_rf CTRLRun refused this call: unknown_action. The tool did not run. +issue_refund amount=900000 ctrlrun refused this call: rule[2]. The tool did not run. +rm_rf ctrlrun refused this call: unknown_action. The tool did not run. issue_refund amount=1000 (the tool runs) -issue_refund amount=1000 CTRLRun refused this call: this effect is already committed +issue_refund amount=1000 ctrlrun refused this call: this effect is already committed ``` That last line is the property worth knowing about. Because `handler` is the executor, the effect is reserved before the tool runs and committed from what it returned. Two agents sharing a store cannot both execute the same effect key, and a tool that raises leaves the outcome `AMBIGUOUS` rather than `FAILED` — so the retry is refused until a person resolves it, instead of becoming a double charge. @@ -97,7 +97,7 @@ That last line is the property worth knowing about. Because `handler` is the exe Where the policy says `approve`, the call is held and the model is told how to release it: ```text -CTRLRun is holding this call for a human. Approve it with 'ctrlrun approve apr_...', +ctrlrun is holding this call for a human. Approve it with 'ctrlrun approve apr_...', then ask again. The tool did not run. ``` diff --git a/docs/guides/langgraph-adapter.mdx b/docs/guides/langgraph-adapter.mdx index 99a018e..b227c3e 100644 --- a/docs/guides/langgraph-adapter.mdx +++ b/docs/guides/langgraph-adapter.mdx @@ -77,7 +77,7 @@ them against a real LangGraph install in this repository's CI. one that does not check. `True`: the resumption must carry `arguments`, core rebuilds the proposal with them and compares the hash, and an answer given against €5 that arrives for a €5,000 action is refused with `ApprovalMismatch`. `False`: only the verdict comes back, - the binding across the interrupt is LangGraph's checkpoint, and CTRLRun records who + the binding across the interrupt is LangGraph's checkpoint, and ctrlrun records who answered without being able to re-check what about; the conformance kit reports `binding` as not applicable, never as a pass. Choose `False` only if your console cannot echo what it displayed. diff --git a/docs/guides/observe-to-enforce.mdx b/docs/guides/observe-to-enforce.mdx index 85857e1..ade089e 100644 --- a/docs/guides/observe-to-enforce.mdx +++ b/docs/guides/observe-to-enforce.mdx @@ -85,7 +85,7 @@ the gateway in front of them. The blocks below simulate a week in a few calls. ``` ```text - CTRLRun — 2026-09-06T10:55:05.474Z .. 2026-09-06T10:55:05.476Z (observe mode) + ctrlrun — 2026-09-06T10:55:05.474Z .. 2026-09-06T10:55:05.476Z (observe mode) actions 7 would have been denied 1 diff --git a/docs/guides/openai-agents-adapter.mdx b/docs/guides/openai-agents-adapter.mdx index 6846241..007ffbe 100644 --- a/docs/guides/openai-agents-adapter.mdx +++ b/docs/guides/openai-agents-adapter.mdx @@ -6,7 +6,7 @@ description: "Route an approve decision through the OpenAI Agents SDK's tool-app `ctrlrun-openai-agents` makes an `approve` decision stop the run with the SDK's own `ToolApprovalItem` instead of `ApprovalRequired` being raised past the runner. The human answers with `state.approve(item)`, where this SDK's users already answer, and one core provider -writes the grant. The binding across the interrupt is the SDK's, keyed by `call_id`, so CTRLRun +writes the grant. The binding across the interrupt is the SDK's, keyed by `call_id`, so ctrlrun records who answered and cannot re-check what they answered about: that is attribution, in that word. @@ -60,15 +60,15 @@ blocks are the adapter's own example, run against a real SDK install in this rep result = await gate.run(agent, state) ``` - `gate.run` and `gate.run_sync` are `Runner.run` with CTRLRun's exceptions arriving as + `gate.run` and `gate.run_sync` are `Runner.run` with ctrlrun's exceptions arriving as themselves: the SDK wraps a tool's exception in `UserError`, and these walk the chain back. `unwrap(error)` does the same if you call `Runner` yourself. </Step> <Step title="Know what a rejection leaves behind"> - The SDK does not invoke a tool whose approval was refused, so no CTRLRun action is + The SDK does not invoke a tool whose approval was refused, so no ctrlrun action is proposed: no `APPROVAL_DENIED`, no `ACTION_DENIED`, no receipt. The refusal is real and in - the SDK's run output; CTRLRun was never asked. Record it where you call `state.reject(item)` + the SDK's run output; ctrlrun was never asked. Record it where you call `state.reject(item)` if you need it in the evidence log. The conformance kit reports `denial` as not applicable for the same reason. </Step> diff --git a/docs/guides/resolve-an-ambiguous-effect.mdx b/docs/guides/resolve-an-ambiguous-effect.mdx index cf5ba7b..ff884f1 100644 --- a/docs/guides/resolve-an-ambiguous-effect.mdx +++ b/docs/guides/resolve-an-ambiguous-effect.mdx @@ -4,7 +4,7 @@ description: "Find the effects nobody knows the outcome of with ctrlrun effects, --- An `AMBIGUOUS` effect is one whose executor raised something other than `NotExecuted`, timed -out, or never returned. CTRLRun will not guess, so a person asks the remote and records the +out, or never returned. ctrlrun will not guess, so a person asks the remote and records the answer with `ctrlrun resolve`. This guide makes one, finds it, resolves it both ways, and shows what the evidence says afterwards. diff --git a/docs/guides/run-on-postgres.mdx b/docs/guides/run-on-postgres.mdx index f5a01eb..bfd7400 100644 --- a/docs/guides/run-on-postgres.mdx +++ b/docs/guides/run-on-postgres.mdx @@ -7,7 +7,7 @@ Use Postgres when workers on more than one host must share one store, because `B IMMEDIATE` is a write lock on a local file and does not reach across hosts. The store is the same protocol, graded by the same suite as SQLite; what changes is a URL. -**Prerequisites:** a Postgres 14 or later server, a database, a role for CTRLRun, and +**Prerequisites:** a Postgres 14 or later server, a database, a role for ctrlrun, and `pip install "ctrlrun[postgres]"`. <Steps> @@ -27,7 +27,7 @@ same protocol, graded by the same suite as SQLite; what changes is a URL. The URL goes to `psycopg.connect` unchanged, so `?sslmode=require`, `?connect_timeout=5`, a `service=` name and the `PG*` environment variables all work. Put the password in `~/.pgpass` or `PGPASSWORD`, not in the URL. On the command line the schema travels as - CTRLRun's own query parameter: + ctrlrun's own query parameter: ```bash export CTRLRUN_STORE_URL='postgresql://db.internal/ctrlrun?ctrlrun_schema=ctrlrun' diff --git a/docs/guides/verify-in-ci.mdx b/docs/guides/verify-in-ci.mdx index 37c2df0..d37b175 100644 --- a/docs/guides/verify-in-ci.mdx +++ b/docs/guides/verify-in-ci.mdx @@ -1,6 +1,6 @@ --- title: "Verify in CI" -description: "Run ctrlrun verify against your policy on every push with the CTRLRun GitHub Action, read the two shapes of report, understand the N/A line." +description: "Run ctrlrun verify against your policy on every push with the ctrlrun GitHub Action, read the two shapes of report, understand the N/A line." --- `ctrlrun verify` runs the kernel's own failure scenarios against your policy, in a scratch @@ -32,7 +32,7 @@ guarantees pass. ``` ```text - CTRLRun verify — ctrlrun 0.12.2, catalogue ctrlrun.guarantees/v7 + ctrlrun verify — ctrlrun 0.12.2, catalogue ctrlrun.guarantees/v7 policy /Users/arpanghoshal/ctrlrun-project/wt/v11-i3/examples/cookbook/verify-in-github-actions/ctrlrun.yaml (ctrlrun.policy/v2, mode: enforce) authority none store sqlite, scratch (created and destroyed for this run) @@ -100,7 +100,7 @@ guarantees pass. <Step title="Add the action"> ```yaml - name: CTRLRun verify + name: ctrlrun verify on: [push, pull_request] diff --git a/docs/how-this-is-built.md b/docs/how-this-is-built.md index 9ddcfc9..d86de1f 100644 --- a/docs/how-this-is-built.md +++ b/docs/how-this-is-built.md @@ -3,7 +3,7 @@ title: "How this is built" description: "Specification first, every requirement mutation-tested, independent review, every claim mapped to a test, and what has not been done yet." --- -CTRLRun is built specification-first, every requirement in it is mutation-tested, anything +ctrlrun is built specification-first, every requirement in it is mutation-tested, anything that touches authorization is reviewed by a session that did not write it, and every sentence in the README maps to a test. That discipline is the reason to trust the code, and it is also what makes it safe that AI coding agents write most of it. This page says both, plainly, with diff --git a/docs/mcp/approve-from-your-assistant.mdx b/docs/mcp/approve-from-your-assistant.mdx index 0f49320..2c555c9 100644 --- a/docs/mcp/approve-from-your-assistant.mdx +++ b/docs/mcp/approve-from-your-assistant.mdx @@ -164,5 +164,5 @@ The agent then runs the refund it was waiting on, and the receipt says who let i ## Next -- [CTRLRun and MCP](/docs/mcp/overview) · [The gateway in five minutes](/docs/mcp/gateway-in-5-minutes). +- [ctrlrun and MCP](/docs/mcp/overview) · [The gateway in five minutes](/docs/mcp/gateway-in-5-minutes). - [Get started](/docs/get-started/quickstart) · [Why](/docs/why). diff --git a/docs/mcp/gateway-in-5-minutes.mdx b/docs/mcp/gateway-in-5-minutes.mdx index bbe28aa..0f51351 100644 --- a/docs/mcp/gateway-in-5-minutes.mdx +++ b/docs/mcp/gateway-in-5-minutes.mdx @@ -1,6 +1,6 @@ --- title: "The gateway in five minutes" -description: "You already run an MCP server. Two commands put CTRLRun between the agent and it." +description: "You already run an MCP server. Two commands put ctrlrun between the agent and it." --- You already run an MCP server and an agent that calls it. Put `ctrlrun gateway` between them @@ -10,7 +10,7 @@ server does not change at all. ```text before agent ──▶ http://localhost:8000/mcp -after agent ──▶ http://127.0.0.1:8900/mcp (CTRLRun) ──▶ http://localhost:8000/mcp +after agent ──▶ http://127.0.0.1:8900/mcp (ctrlrun) ──▶ http://localhost:8000/mcp ``` **Prerequisites:** `pip install "ctrlrun[gateway]"`; the server reachable over HTTP; a @@ -49,7 +49,7 @@ accepts `2025-11-25`, `2025-06-18` and `2025-03-26`. ``` ```text - CTRLRun gateway → http://localhost:8000/mcp as mcp.ops.* + ctrlrun gateway → http://localhost:8000/mcp as mcp.ops.* listening on http://127.0.0.1:8900/mcp environment: production identity: fixed principal deploy-agent authority: none 1 action(s) have no effect: template and get no reservation: @@ -136,4 +136,4 @@ accepts `2025-11-25`, `2025-06-18` and `2025-03-26`. ## Next - [Put the gateway in front of MCP](/docs/guides/gateway-in-front-of-mcp): the longer guide, with the lost-reply case. -- [CTRLRun and MCP](/docs/mcp/overview) · [Get started](/docs/get-started/quickstart) · [Why](/docs/why). +- [ctrlrun and MCP](/docs/mcp/overview) · [Get started](/docs/get-started/quickstart) · [Why](/docs/why). diff --git a/docs/mcp/overview.mdx b/docs/mcp/overview.mdx index c8c5145..c680964 100644 --- a/docs/mcp/overview.mdx +++ b/docs/mcp/overview.mdx @@ -1,10 +1,10 @@ --- -title: "CTRLRun and MCP" +title: "ctrlrun and MCP" sidebarTitle: "Overview" description: "Four ways: the gateway enforces policy in front of any MCP server, and the operator server lets an approver answer from their assistant." --- -CTRLRun works with MCP in four ways. The gateway sits in front of any MCP server and applies +ctrlrun works with MCP in four ways. The gateway sits in front of any MCP server and applies your policy to every `tools/call`, with no change to the agent or the server. The operator server lets the person who has to answer an approval answer it from the assistant they are already talking to. This documentation is itself an MCP server your coding tool can search. And @@ -20,7 +20,7 @@ wire is relayed untouched. ```text before agent ──▶ MCP server -after agent ──▶ CTRLRun gateway ──▶ MCP server +after agent ──▶ ctrlrun gateway ──▶ MCP server ``` - No code changes: the agent and the server are untouched. @@ -82,7 +82,7 @@ configuration and a transcript. The guarantees are the same three ways in. A refund refused by the gateway is refused for the same reason, recorded in the same receipt shape, and verified by the same `ctrlrun verify` as -one refused by the decorator. An agent that calls a CTRLRun tool to check its own actions would +one refused by the decorator. An agent that calls a ctrlrun tool to check its own actions would not be enforcement, because a tool the agent chooses to call is a tool it can choose not to; the gateway is in the path whether the agent likes it or not. diff --git a/docs/mcp/use-the-docs-from-your-editor.mdx b/docs/mcp/use-the-docs-from-your-editor.mdx index 4ddd01c..a5d97b7 100644 --- a/docs/mcp/use-the-docs-from-your-editor.mdx +++ b/docs/mcp/use-the-docs-from-your-editor.mdx @@ -34,7 +34,7 @@ Both shapes are the ones Mintlify's documentation gives for a hosted docs server ## Three questions it can now answer -- *What happens in CTRLRun when a tool call times out?* The assistant finds +- *What happens in ctrlrun when a tool call times out?* The assistant finds [Outcomes and AMBIGUOUS](/docs/concepts/outcomes-and-ambiguous) and answers that a timeout is `AMBIGUOUS`, not `FAILED`, and that a retry is refused until a human or a reconcile hook resolves it. @@ -49,11 +49,11 @@ Both shapes are the ones Mintlify's documentation gives for a hosted docs server ## What it is not It is not enforcement. An assistant that can search this documentation can tell you what the -gateway does; it cannot stand in the agent's path. An agent that called a CTRLRun tool to check +gateway does; it cannot stand in the agent's path. An agent that called a ctrlrun tool to check its own actions could also choose not to, which is why the gateway is a process between the agent and its tools and not a tool the agent picks. ## Next -- [CTRLRun and MCP](/docs/mcp/overview). +- [ctrlrun and MCP](/docs/mcp/overview). - [Get started](/docs/get-started/quickstart) · [Why](/docs/why). diff --git a/docs/not-only-agents.mdx b/docs/not-only-agents.mdx index 827763b..dc4419a 100644 --- a/docs/not-only-agents.mdx +++ b/docs/not-only-agents.mdx @@ -8,7 +8,7 @@ Every page on this site says *agent*, and the failure underneath them does not r retry that repeats a write whose outcome nobody knows is a bug in ordinary software, and it was a bug in ordinary software for twenty years before anything called itself an agent. If you run a task queue, a webhook handler or a cron job that moves money, sends mail or deletes records, -this is your problem too, and CTRLRun does not ask whether a model was involved. +this is your problem too, and ctrlrun does not ask whether a model was involved. Three examples in the repository have no model, no prompt and no framework in them. @@ -24,7 +24,7 @@ each of them re-runs the task when it raises, and none of them can tell *nothing python examples/without-an-agent/retried-task/main.py ``` -CTRLRun records the outcome as `AMBIGUOUS` rather than failed, and the second and third attempts +ctrlrun records the outcome as `AMBIGUOUS` rather than failed, and the second and third attempts are refused before they reach the processor. [`retried-task/main.py`](https://github.com/CTRLRun/ctrlrun/blob/main/examples/without-an-agent/retried-task/main.py). diff --git a/docs/postgres.md b/docs/postgres.md index 61bc0e3..bce0935 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -33,7 +33,7 @@ store = PostgresStateStore("postgresql://ctrlrun@db.internal:5432/ctrlrun") The URL is passed to `psycopg.connect` unchanged, so everything libpq accepts works: a `postgres://` scheme, `?sslmode=require`, `?connect_timeout=5`, a `service=` name, or the -standard `PG*` environment variables with an otherwise-bare URL. CTRLRun parses none of it. +standard `PG*` environment variables with an otherwise-bare URL. ctrlrun parses none of it. A second schema is a keyword: @@ -47,7 +47,7 @@ be a plain identifier — letters, digits and underscores — which is what make interpolate; it is the only name in the module that reaches SQL, and it never comes from an action, an argument or a request header. -On the command line the schema travels in the URL, as CTRLRun's own query parameter, peeled off +On the command line the schema travels in the URL, as ctrlrun's own query parameter, peeled off before anything reaches the driver: ```bash @@ -97,7 +97,7 @@ touches no other schema. **The store refuses a database whose `server_encoding` is not `UTF8`**, at open, naming it. -This looks fussy and is not. CTRLRun hashes the exact code points it is given and applies no +This looks fussy and is not. ctrlrun hashes the exact code points it is given and applies no Unicode normalization (`SPEC-v0.1.md` §2.3). An effect key that survives a round trip through `SQL_ASCII` as different bytes is a **different identity**, so two attempts at one logical effect would reserve two different keys and both would execute. That is a double execution reached @@ -192,7 +192,7 @@ neither. Reads would appear to work, which is the problem. ### Losing the primary, honestly An asynchronous replica that is promoted after losing transactions loses effect records with -them, and CTRLRun cannot tell that this happened — a key that was reserved and executed comes +them, and ctrlrun cannot tell that this happened — a key that was reserved and executed comes back absent, and the next attempt reserves it again and executes again. If you need the store's guarantee to survive a failover, you need the *database's* durability to survive it: synchronous commit to at least one standby. This is a property of your Postgres configuration and not diff --git a/docs/production/anchoring.mdx b/docs/production/anchoring.mdx index d8d5372..9f5731e 100644 --- a/docs/production/anchoring.mdx +++ b/docs/production/anchoring.mdx @@ -25,7 +25,7 @@ ctrlrun anchor --provider yourpkg.anchors:provider # on a schedule ctrlrun anchor --provider yourpkg.anchors:provider --verify # in the job after a restore ``` -CTRLRun **ships no provider**. An RFC 3161 client is a network client, and this library's core is +ctrlrun **ships no provider**. An RFC 3161 client is a network client, and this library's core is the standard library plus `pyyaml` and `click`. You supply one with four calls: `make`, `check`, `latest` and `since`. @@ -41,7 +41,7 @@ anchored pair failing to reproduce. That includes the administrator who rewrites *including* the head, which the chain alone cannot catch, because the operator's own record is what decides rather than a row in the database under suspicion. -The verification asks your provider what it holds **before** reading CTRLRun's local table, so +The verification asks your provider what it holds **before** reading ctrlrun's local table, so deleting rows from that table does not remove the question: a store whose anchor cache was emptied reports `anchor_missing`, which is a break. diff --git a/docs/production/index.mdx b/docs/production/index.mdx index 7752db3..af03e8e 100644 --- a/docs/production/index.mdx +++ b/docs/production/index.mdx @@ -70,7 +70,7 @@ questions about the store. - **It does not run anything for you.** There is no daemon, no scheduler and no background thread. Nothing sweeps expired leases, nothing retries on your behalf, and a restarted process repairs nothing. See [recovery](/docs/production/recovery) for why that is deliberate. -- **It does not make an external system idempotent.** CTRLRun refuses to knowingly act twice. +- **It does not make an external system idempotent.** ctrlrun refuses to knowingly act twice. Whether the remote acted is a fact only the remote holds. - **It does not roll anything back.** It is not a transaction manager and it never pretends a remote write is undone. diff --git a/docs/production/migrations.mdx b/docs/production/migrations.mdx index a14b95a..a37994a 100644 --- a/docs/production/migrations.mdx +++ b/docs/production/migrations.mdx @@ -3,7 +3,7 @@ title: "Migrations and schema versions" description: "Migrations run at open, forward only, and nothing half-applies. Five shapes a database can be in, and three of them are a refusal." --- -Deploy a newer CTRLRun over an existing database and it migrates at open, forward only, one +Deploy a newer ctrlrun over an existing database and it migrates at open, forward only, one transaction per migration. You run no command and set no flag, because there is no flag: a database that opened un-migrated would be a database serving reads it half understands. @@ -22,7 +22,7 @@ Let *known* be the migration ids your binary ships, and *applied* be what the da Those five are the shapes a database that already records a schema version can be in, and they are exhaustive: the store classifies into exactly one, and anything it cannot place is refused rather than opened. Three more sit in front of them. An **empty** database is migrated to head. -A database with **CTRLRun's tables and no version record** — anything from before v0.6 — is +A database with **ctrlrun's tables and no version record** — anything from before v0.6 — is adopted, given the record it never had, and migrated; that is the case an upgrade actually meets. A database with **foreign tables and no effects table** is refused, naming what it found. diff --git a/docs/production/operations.mdx b/docs/production/operations.mdx index 3d29d7c..2140c6b 100644 --- a/docs/production/operations.mdx +++ b/docs/production/operations.mdx @@ -31,7 +31,7 @@ Set the alert on the first row and the last. The middle rows are for the dashboa Every executed action leaves a portable JSON receipt, and receipts are the export. With `pip install "ctrlrun[otel]"` you get one OpenTelemetry span per action and one span event per -step, and argument values stay out of it unless you ask for them. There is no CTRLRun dashboard +step, and argument values stay out of it unless you ask for them. There is no ctrlrun dashboard and there will not be one. ## Routine diff --git a/docs/production/recovery.mdx b/docs/production/recovery.mdx index 517cf24..ca7da0d 100644 --- a/docs/production/recovery.mdx +++ b/docs/production/recovery.mdx @@ -38,7 +38,7 @@ prints it as `executing (lease expired)`. Reading is not a transition: listing e one, or opening the store moves nothing. The effect record's `resolved_by` column holds `cli:local` for a person, not their username. -CTRLRun does not authenticate the person at the terminal, so a name there would be a claim about +ctrlrun does not authenticate the person at the terminal, so a name there would be a claim about somebody made from a value that somebody controls. `cli:local` says exactly what is known: somebody with the operator's terminal. The `EFFECT_RESOLVED` event carries the same string in its `resolver` field, which is what lets you join the two, and the coarser `human` or `reconcile` in diff --git a/docs/reference/api/CTRLRunError.mdx b/docs/reference/api/CTRLRunError.mdx index 4f06fcf..a042150 100644 --- a/docs/reference/api/CTRLRunError.mdx +++ b/docs/reference/api/CTRLRunError.mdx @@ -1,6 +1,6 @@ --- title: "CTRLRunError" -description: "Base class for every error raised by CTRLRun." +description: "Base class for every error raised by ctrlrun." --- {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} @@ -16,7 +16,7 @@ from ctrlrun import CTRLRunError class CTRLRunError(Exception) ``` -Base class for every error raised by CTRLRun. +Base class for every error raised by ctrlrun. ## Next diff --git a/docs/reference/api/Suspended.mdx b/docs/reference/api/Suspended.mdx index d5d338f..d970eaf 100644 --- a/docs/reference/api/Suspended.mdx +++ b/docs/reference/api/Suspended.mdx @@ -24,7 +24,7 @@ happened" — an explicit opt-in signal, never a default and never inferred. The outcome to record: the effect record stays `EXECUTING`, its lease is extended, the continuation is held, and the caller gets this back to relay. -`continuation` is whatever the remote said to present again. It is opaque here — CTRLRun +`continuation` is whatever the remote said to present again. It is opaque here — ctrlrun never parses it, and only ever compares it with `hmac.compare_digest`. ## Next diff --git a/docs/reference/api/acs-AcsControlHook.mdx b/docs/reference/api/acs-AcsControlHook.mdx index 37a4bea..1c92183 100644 --- a/docs/reference/api/acs-AcsControlHook.mdx +++ b/docs/reference/api/acs-AcsControlHook.mdx @@ -1,6 +1,6 @@ --- title: "AcsControlHook" -description: "Answer ACS `steps/*` hooks with CTRLRun's decisions and outcomes." +description: "Answer ACS `steps/*` hooks with ctrlrun's decisions and outcomes." --- {/* generated by tools/docs_audit/render_api.py from the docstrings — edit the docstring, never this page */} @@ -19,7 +19,7 @@ class AcsControlHook def __init__(control: Control, *, prefix: str = 'acs', approver_id: str = 'cli:local', ask_timeout_seconds: int = DEFAULT_ASK_TIMEOUT_SECONDS, identity: IdentityProvider | None = None) ``` -Answer ACS `steps/*` hooks with CTRLRun's decisions and outcomes. +Answer ACS `steps/*` hooks with ctrlrun's decisions and outcomes. One `Control`, one prefix. `prefix` names the tool namespace in the action name, the way the gateway's `--alias` does: `<prefix>.<provider>.<tool>` — so a policy addresses one diff --git a/docs/reference/api/index.mdx b/docs/reference/api/index.mdx index 898bace..b6fd0aa 100644 --- a/docs/reference/api/index.mdx +++ b/docs/reference/api/index.mdx @@ -26,7 +26,7 @@ a name with no docstring fails a test, so every page has one. | [`ctrlrun.AuthorityDenied`](/docs/reference/api/AuthorityDenied) | class | The principal holds no grant that covers this action (SPEC-v0.3 §4.3). | | [`ctrlrun.AuthorityEscalation`](/docs/reference/api/AuthorityEscalation) | class | A delegation that may not exist: it is not contained in its parent (SPEC-v0.3 §5.3). | | [`ctrlrun.AuthorityResult`](/docs/reference/api/AuthorityResult) | class | What the authority axis decided, and which grant it decided on (§4.8). | -| [`ctrlrun.CTRLRunError`](/docs/reference/api/CTRLRunError) | class | Base class for every error raised by CTRLRun. | +| [`ctrlrun.CTRLRunError`](/docs/reference/api/CTRLRunError) | class | Base class for every error raised by ctrlrun. | | [`ctrlrun.Condition`](/docs/reference/api/Condition) | class | One `<argument>_<op>: operand` test against an action's arguments (SPEC-v0.1 §3.2). | | [`ctrlrun.Control`](/docs/reference/api/Control) | class | Policy, state and evidence composed around a single action (SPEC-v0.1 §8). | | [`ctrlrun.Decision`](/docs/reference/api/Decision) | class | What may happen to an action: exactly three outcomes in v0.1 (SPEC-v0.1 §3.3). | @@ -84,7 +84,7 @@ a name with no docstring fails a test, so every page has one. | [`ctrlrun.postgres.PostgresStateStore`](/docs/reference/api/postgres-PostgresStateStore) | class | Approvals, effects and evidence in a Postgres schema (SPEC-v0.6 §4). | | [`ctrlrun.otel.OTelEventSink`](/docs/reference/api/otel-OTelEventSink) | class | Export every `Event` and `Receipt` as OpenTelemetry spans (SPEC-v0.2 §8). | | [`ctrlrun.jwt_identity.JWTIdentityProvider`](/docs/reference/api/jwt_identity-JWTIdentityProvider) | class | Verify a bearer JWT and map its verified claims onto a `Principal` (SPEC-v0.3 §3.4). | -| [`ctrlrun.acs.AcsControlHook`](/docs/reference/api/acs-AcsControlHook) | class | Answer ACS `steps/*` hooks with CTRLRun's decisions and outcomes. | +| [`ctrlrun.acs.AcsControlHook`](/docs/reference/api/acs-AcsControlHook) | class | Answer ACS `steps/*` hooks with ctrlrun's decisions and outcomes. | | [`ctrlrun.gateway.serve`](/docs/reference/api/gateway-serve) | function | Run a gateway in front of one upstream MCP server (SPEC-v0.2 §6.1). | | [`ctrlrun.verify.run`](/docs/reference/api/verify-run) | function | Run the applicable guarantees against this configuration and report (§9.1). | | [`ctrlrun.conformance.run`](/docs/reference/api/conformance-run) | function | Drive every suite through `adapter` and report what each came to (SPEC-v0.5 §5). | diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 07de7eb..1694c6a 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -15,7 +15,7 @@ nothing and migrates nothing. Each section below is the command's own `--help`, ```text Usage: ctrlrun [OPTIONS] COMMAND [ARGS]... - CTRLRun — the execution safety layer for AI agents. + ctrlrun — the execution safety layer for AI agents. Options: --version Show the version and exit. @@ -134,7 +134,7 @@ Usage: ctrlrun anchor [OPTIONS] and its size is your choice of interval. Options: - --provider MODULE:ATTR Your anchor provider (SPEC-v0.11 §3.2). CTRLRun + --provider MODULE:ATTR Your anchor provider (SPEC-v0.11 §3.2). ctrlrun ships none. [required] --verify Check every anchor the provider holds against this chain, and make none. diff --git a/docs/reference/errors.mdx b/docs/reference/errors.mdx index 17e8b9b..bcf6e8f 100644 --- a/docs/reference/errors.mdx +++ b/docs/reference/errors.mdx @@ -1,6 +1,6 @@ --- title: "Errors" -description: "The closed set of CTRLRun exceptions, each with its base class and when it is raised: ActionDenied, ApprovalRequired, DuplicateEffect and the rest." +description: "The closed set of ctrlrun exceptions, each with its base class and when it is raised: ActionDenied, ApprovalRequired, DuplicateEffect and the rest." --- {/* generated by tools/docs_audit/render_schemas.py from the code — edit the dataclasses and docstrings, never this page */} @@ -11,7 +11,7 @@ set is closed by the specification and a new one is a specification amendment fi | Exception | Base | Raised when | |---|---|---| -| `CTRLRunError` | `Exception` | Base class for every error raised by CTRLRun. | +| `CTRLRunError` | `Exception` | Base class for every error raised by ctrlrun. | | `InvalidArgument` | `CTRLRunError` | An argument cannot be accepted as given. | | `PolicyError` | `CTRLRunError` | The policy is missing, unreadable, or malformed. Raised at load time (SPEC-v0.1 §3.4). | | `EffectKeyError` | `CTRLRunError` | An effect template cannot be resolved to a key (SPEC-v0.1 §5.1). | @@ -33,7 +33,7 @@ set is closed by the specification and a new one is a specification amendment fi ### CTRLRunError -Base class for every error raised by CTRLRun. +Base class for every error raised by ctrlrun. ### InvalidArgument @@ -127,7 +127,7 @@ happened" — an explicit opt-in signal, never a default and never inferred. The outcome to record: the effect record stays `EXECUTING`, its lease is extended, the continuation is held, and the caller gets this back to relay. -`continuation` is whatever the remote said to present again. It is opaque here — CTRLRun +`continuation` is whatever the remote said to present again. It is opaque here — ctrlrun never parses it, and only ever compares it with `hmac.compare_digest`. ### IdentityError diff --git a/docs/reference/exit-codes.mdx b/docs/reference/exit-codes.mdx index 8c45417..fa483e3 100644 --- a/docs/reference/exit-codes.mdx +++ b/docs/reference/exit-codes.mdx @@ -3,7 +3,7 @@ title: "Exit codes" description: "What each ctrlrun command's exit status means: 0 done, 1 a refusal or a failed guarantee, 2 a usage error or an unusable configuration." --- -Every `ctrlrun` command exits 0 when it did what it was asked, 1 when CTRLRun refused +Every `ctrlrun` command exits 0 when it did what it was asked, 1 when ctrlrun refused (a `CTRLRunError`, printed as one line), and 2 on a usage error. `ctrlrun verify` adds a third code for its own failure, because a verifier that crashed must not look like one that refused. @@ -12,7 +12,7 @@ code for its own failure, because a verifier that crashed must not look like one | Code | Meaning | Examples | |---|---|---| | `0` | the command did what it was asked | a grant written, a receipt printed, a revocation applied (revoking an already-revoked delegation is idempotent and exits 0) | -| `1` | CTRLRun refused, and said why on one line | `resolve` on an effect that is not `AMBIGUOUS`; `approve` on a request that expired; `delegate` beyond the parent's grant; a `--store-url` naming a database this binary does not recognise | +| `1` | ctrlrun refused, and said why on one line | `resolve` on an effect that is not `AMBIGUOUS`; `approve` on a request that expired; `delegate` beyond the parent's grant; a `--store-url` naming a database this binary does not recognise | | `2` | the command line was wrong | a missing argument, an unknown option, `resolve` without exactly one of `--committed` and `--failed` | A refusal is the command's exit code and its last line. Nothing is retried on the operator's diff --git a/docs/reference/policy-yaml.mdx b/docs/reference/policy-yaml.mdx index 104265a..f28f5c2 100644 --- a/docs/reference/policy-yaml.mdx +++ b/docs/reference/policy-yaml.mdx @@ -73,7 +73,7 @@ a requirement and decide nothing. v6 gives it one key that decides something. | Key | Type | Since | When omitted | Notes | |---|---|---|---|---| | `title` | string | v4 | required | what the control requires, in a sentence | -| `source` | string | v4 | none | where the requirement comes from. CTRLRun does not interpret it | +| `source` | string | v4 | none | where the requirement comes from. ctrlrun does not interpret it | | `approver_role` | non-empty string | v6 | **this control gates nobody** | the role a principal must hold for their approval of an action citing this control to be consumable. Matched **byte for byte** against the claim the deployment names: no case folding, no trimming, no prefix matching and no pattern grammar, because a wildcard in a role would be an entitlement nobody wrote. Leading or trailing whitespace is a load error rather than a role that matches nothing for ever. Where an action cites several controls, **every** required role must be held, since any-of would let the weakest control in a set decide who may answer. Inside the policy hash | **Omission is not entitlement, and it is not refusal either.** A control with no `approver_role` @@ -84,7 +84,7 @@ principal. **What the kernel refuses is an approval whose *recorded* entitlement does not cover the role.** What entitled it was decided where the credential was verified — the operator MCP server, or an -embedding application. CTRLRun does not interpret the role, does not check that such a role exists +embedding application. ctrlrun does not interpret the role, does not check that such a role exists anywhere, and claims nothing about a standard or an audit on the strength of one. ## A rule @@ -99,7 +99,7 @@ anywhere, and claims nothing about a standard or an audit on the strength of one A condition key is `<subject>_<operator>`. The subject is an argument name, or the derived `data_scope`; the operator is one of seven, and there is no other syntax. Amounts and every -other numeric operand are integers: `float` is refused everywhere in CTRLRun. +other numeric operand are integers: `float` is refused everywhere in ctrlrun. | Operator | Meaning | Example | |---|---|---| diff --git a/docs/reference/receipt-and-event-schemas.mdx b/docs/reference/receipt-and-event-schemas.mdx index 1536a7e..f159094 100644 --- a/docs/reference/receipt-and-event-schemas.mdx +++ b/docs/reference/receipt-and-event-schemas.mdx @@ -1,6 +1,6 @@ --- title: "Receipt and event schemas" -description: "Every field of a CTRLRun receipt and of an event, and every event type, rendered from the dataclasses that write them." +description: "Every field of a ctrlrun receipt and of an event, and every event type, rendered from the dataclasses that write them." --- {/* generated by tools/docs_audit/render_schemas.py from the code — edit the dataclasses and docstrings, never this page */} @@ -8,7 +8,7 @@ description: "Every field of a CTRLRun receipt and of an event, and every event A receipt is one executed action; an event is one step on the way. Both are written to the store, appended to `.ctrlrun/receipts.jsonl` and `.ctrlrun/events.jsonl` as one JSON object per line, and exported to any sink installed. Every enum renders by value, so a reader that -never imported CTRLRun can read the evidence. +never imported ctrlrun can read the evidence. ## Receipt diff --git a/docs/security/assurance-case.mdx b/docs/security/assurance-case.mdx index 2dc71ed..5d76704 100644 --- a/docs/security/assurance-case.mdx +++ b/docs/security/assurance-case.mdx @@ -4,7 +4,7 @@ description: "Why the three guarantees hold: the threat model and its boundary, --- An assurance case is the argument, with its evidence, that a system meets its security -requirements. This page is CTRLRun's. It adds no guarantee; it says why the ones the +requirements. This page is ctrlrun's. It adds no guarantee; it says why the ones the [threat model](/docs/THREAT_MODEL) states can be relied on, and where the evidence for each sits. Read it beside that page and the [architecture](/docs/ARCHITECTURE). @@ -18,13 +18,13 @@ The threat model names three assets, and each one is a requirement on the kernel | **R2. Integrity of human approval** | What a human approved is what executes. An approval binds to the hash of the canonical action, is consumed once, and expires. | | **R3. Integrity of evidence** | Receipts reflect what happened, in order, and an edit, a reordering or a deletion within the retained chain is detected. Erasing the end of the log and rewinding the head is outside this guarantee, and the [receipt chain page](/docs/security/receipt-chain) says so. | -Everything below argues these three. What CTRLRun does not defend against is listed in the +Everything below argues these three. What ctrlrun does not defend against is listed in the threat model under *Out of scope* and is not argued here. ## The trust boundary Untrusted: agent reasoning, model outputs, tool outputs, retry logic, other agents, and every -token or header a caller presents. Trusted: the CTRLRun process, the policy file, the state +token or header a caller presents. Trusted: the ctrlrun process, the policy file, the state store, and the approver's shell. Data crosses the boundary in one place: the proposed action with its arguments, and whatever diff --git a/docs/security/disclosure.mdx b/docs/security/disclosure.mdx index f970746..6ba2cd4 100644 --- a/docs/security/disclosure.mdx +++ b/docs/security/disclosure.mdx @@ -12,7 +12,7 @@ actions. A failing test is the fastest possible report. ## What counts as a vulnerability -CTRLRun sits in the execution path of consequential actions, so anything that breaks one of +ctrlrun sits in the execution path of consequential actions, so anything that breaks one of these is a security issue rather than a bug: - An action executes that the policy should have denied. @@ -32,7 +32,7 @@ think one is stated too generously, say so: - A compromised process, host or Python environment. - A malicious administrator with write access to the policy file or the store. - A remote that lies about what it did. -- A compromised approver: CTRLRun proves what was approved, not that the human was right. +- A compromised approver: ctrlrun proves what was approved, not that the human was right. - An executor that raises `NotExecuted` after the remote acted. - Code that bypasses the decorator entirely. - Data exfiltration through reads the policy allows. @@ -47,7 +47,7 @@ than the code does is a defect in this project's terms, and ## Supported versions -CTRLRun is pre-1.0 and only the latest release receives fixes. +ctrlrun is pre-1.0 and only the latest release receives fixes. ## Next diff --git a/docs/security/receipt-chain.mdx b/docs/security/receipt-chain.mdx index d18ee41..303de83 100644 --- a/docs/security/receipt-chain.mdx +++ b/docs/security/receipt-chain.mdx @@ -53,7 +53,7 @@ reconciled. ## What to do with it -Treat a break as an incident about the store rather than about CTRLRun: the chain is the alarm, +Treat a break as an incident about the store rather than about ctrlrun: the chain is the alarm, not the lock. If you need evidence of origin rather than of integrity, sign or anchor the receipts outside the database yourself; they are portable JSON, one object per line, for exactly that reason. diff --git a/docs/verify.md b/docs/verify.md index 6013a20..4488ad1 100644 --- a/docs/verify.md +++ b/docs/verify.md @@ -3,7 +3,7 @@ title: "ctrlrun verify" description: "Running the guarantee catalogue against your own configuration: the report, the N/A rule, the badge, and what verify cannot see." --- -Everything CTRLRun guarantees is proven by this repository's tests against this repository's +Everything ctrlrun guarantees is proven by this repository's tests against this repository's configurations. That is the right place to start and the wrong place to stop, because the thing you deploy is *your* policy, *your* grants and *your* store — and a guarantee that has never been exercised against those is a guarantee nobody has checked. @@ -14,7 +14,7 @@ what could not be tested at all. ```console $ ctrlrun verify -CTRLRun verify — ctrlrun 0.12.2, catalogue ctrlrun.guarantees/v7 +ctrlrun verify — ctrlrun 0.12.2, catalogue ctrlrun.guarantees/v7 policy examples/authority/payments.yaml (ctrlrun.policy/v7, mode: enforce) authority same document, 3 grants store sqlite, scratch (created and destroyed for this run) @@ -89,7 +89,7 @@ under a `--store-url postgresql://remote-host/…`. > configuration can exercise was exercised, and none of them failed. That is the whole claim. It is not a statement that your system is secure, that your policy is -a good policy, or that CTRLRun has audited anything. A configuration that permits everything +a good policy, or that ctrlrun has audited anything. A configuration that permits everything and constrains nobody can pass every guarantee in the catalogue, because they are about **the kernel doing what it says under that configuration** — not about whether the configuration is wise. @@ -104,7 +104,7 @@ Verify sees **the configuration, not the code**. It does not check: own executors and never imports your module. - **Your `reconcile` hooks**, for the same reason: a hook is a Python callable passed to `@protect`, and it does not appear in any file verify reads. -- **Where you put the decorator.** Code that calls the raw function bypasses CTRLRun entirely, +- **Where you put the decorator.** Code that calls the raw function bypasses ctrlrun entirely, and no amount of configuration-reading finds that. - **Your deployment.** Whether the proxy in front of `HeaderIdentityProvider` overwrites the header, whether `$CTRLRUN_STATE` points where you think, whether two gateways share a state @@ -116,7 +116,7 @@ Verify sees **the configuration, not the code**. It does not check: authoritative-looking opinion it has no basis for. The words **secure**, **safe**, **compliant**, **certified** and **audited** do not appear as -claims about CTRLRun or about your system on the badge, in its JSON, in the job summary, or on +claims about ctrlrun or about your system on the badge, in its JSON, in the job summary, or on this page. --- @@ -285,7 +285,7 @@ report guarantees about a configuration nobody deployed. ## In CI ```yaml -name: CTRLRun verify +name: ctrlrun verify on: [push, pull_request] @@ -415,10 +415,10 @@ behind it is worth, and the run is in the workflow log. Then the badge is: ```markdown -[![CTRLRun](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/badges/verify-badge.json)](docs/verify.md#what-the-badge-means) +[![ctrlrun](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/badges/verify-badge.json)](docs/verify.md#what-the-badge-means) ``` -It renders as **CTRLRun verified N/M**, where `N` is passes and `M` is **applicable** +It renders as **ctrlrun verified N/M**, where `N` is passes and `M` is **applicable** guarantees — never the catalogue size. It is `brightgreen` when nothing failed and `red` otherwise; there is no amber for N/A, because the badge's colour is about failures and the N/A count lives in the report the badge links to. @@ -431,5 +431,5 @@ A partial run (`--only`) and a run that exited 2 or 3 write **no badge at all**. - [`SPEC-v0.4.md`](https://github.com/CTRLRun/ctrlrun/blob/main/docs/SPEC-v0.4.md) — the contract this implements, guarantee by guarantee. - [`OWASP-AGENTIC-TOP10.md`](/docs/OWASP-AGENTIC-TOP10) — a reading of somebody else's taxonomy - against these guarantees, with the entries CTRLRun does not address listed by name. + against these guarantees, with the entries ctrlrun does not address listed by name. - [`THREAT_MODEL.md`](/docs/THREAT_MODEL) — what fail-closed means here, and what is out of scope. diff --git a/docs/verify/get-the-badge.mdx b/docs/verify/get-the-badge.mdx index be9ae4f..7a73250 100644 --- a/docs/verify/get-the-badge.mdx +++ b/docs/verify/get-the-badge.mdx @@ -10,7 +10,7 @@ proved, and updates itself. <Steps> <Step title="Verify on every push"> ```yaml - name: CTRLRun verify + name: ctrlrun verify on: push: @@ -79,7 +79,7 @@ proved, and updates itself. <Step title="Point Shields at it"> ```markdown - [![CTRLRun verified](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/badges/verify-badge.json)](https://github.com/CTRLRun/ctrlrun-docs/blob/main/docs/verify.md#what-the-badge-means) + [![ctrlrun verified](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/OWNER/REPO/badges/verify-badge.json)](https://github.com/CTRLRun/ctrlrun-docs/blob/main/docs/verify.md#what-the-badge-means) ``` Link it to what the badge means, as above. A badge nobody can click through to is a claim diff --git a/docs/why.mdx b/docs/why.mdx index ce76622..64efdd8 100644 --- a/docs/why.mdx +++ b/docs/why.mdx @@ -1,19 +1,19 @@ --- -title: "Why CTRLRun" +title: "Why ctrlrun" sidebarTitle: "Why" description: "Five principles for AI agents that act on the real world: a failure is not an unknown, an approval binds to what the human saw." --- Everyone is rushing to ship AI agents without thinking about consequences. An agent that only reads is wrong at no cost. An agent that can pay, delete, deploy, grant or send is wrong at the -cost of the thing it did, and its framework was built to make it act, not to act once. CTRLRun +cost of the thing it did, and its framework was built to make it act, not to act once. ctrlrun exists for the boundary between intending an effect and having caused one. Five principles. ## FAILED is not UNKNOWN A timeout tells you nothing. The refund may have committed at Stripe a millisecond before the connection dropped. A framework that marks the call *failed* and retries has turned one unknown -into a probable double. CTRLRun has three outcomes: `COMMITTED`, `FAILED` and `AMBIGUOUS`. Only +into a probable double. ctrlrun has three outcomes: `COMMITTED`, `FAILED` and `AMBIGUOUS`. Only the executor can say `FAILED`, by raising `NotExecuted`, and only when it knows the remote did nothing. Everything else after the first byte is `AMBIGUOUS`, and an `AMBIGUOUS` effect blocks a blind retry until a human or a reconcile hook says what happened. @@ -23,7 +23,7 @@ blind retry until a human or a reconcile hook says what happened. ## An approval is bound to what the human saw A human who approved "refund €2,000 on txn_2" did not approve €5,000, and did not approve -€2,000 again next week. CTRLRun binds an approval to the SHA-256 of the exact action: name, +€2,000 again next week. ctrlrun binds an approval to the SHA-256 of the exact action: name, canonical arguments, resource, principal, environment. It is used once, expires, and is consumed in the same atomic write that reserves the effect, so a mutated action matches nothing and a replayed one finds its approval spent. @@ -63,7 +63,7 @@ authorship, and the page that describes it says so in the same breath. ## What follows -CTRLRun does not host models, plan, prompt, retrieve, route, remember or orchestrate. It is not a +ctrlrun does not host models, plan, prompt, retrieve, route, remember or orchestrate. It is not a guardrail library, an IAM system, a workflow engine or a compliance product, and it issues no credential. It cannot guarantee exactly-once execution against a remote it does not control; it guarantees it will not *knowingly* act twice and will never call an unknown a failure. diff --git a/execution-boundary.mdx b/execution-boundary.mdx index 364f14b..bf05e26 100644 --- a/execution-boundary.mdx +++ b/execution-boundary.mdx @@ -4,8 +4,8 @@ sidebarTitle: "The boundary" description: "One action, five ways to stop it. The path an agent action takes, the refusal each check raises, and how the boundary goes into your process." canonical: "https://ctrlrun.dev/execution-boundary" "og:url": "https://ctrlrun.dev/execution-boundary" -"og:title": "The execution boundary for agent actions | CTRLRun" -"twitter:title": "The execution boundary for agent actions | CTRLRun" +"og:title": "The execution boundary for agent actions | ctrlrun" +"twitter:title": "The execution boundary for agent actions | ctrlrun" mode: "custom" --- @@ -13,11 +13,11 @@ import { ExecutionBoundary } from "/snippets/execution-boundary.jsx"; <div className="cr-site cr-try-page"> <section className="cr-try-header" aria-labelledby="cr-try-page-title"> - <div><a className="cr-text-link" href="/">← CTRLRun</a><p className="cr-eyebrow">THE EXECUTION BOUNDARY</p><h1 id="cr-try-page-title">The model decides what.<br />CTRLRun decides whether<span className="cr-dot">.</span></h1><p>Pick your domain. The picture is the whole decision.</p></div> + <div><a className="cr-text-link" href="/">← ctrlrun</a><p className="cr-eyebrow">THE EXECUTION BOUNDARY</p><h1 id="cr-try-page-title">The model decides what.<br />ctrlrun decides whether<span className="cr-dot">.</span></h1><p>Pick your domain. The picture is the whole decision.</p></div> </section> <section className="cr-try-workspace" aria-label="The execution boundary"> <ExecutionBoundary /> </section> - <div className="cr-footer" role="contentinfo"><span>Give agents autonomy.<br /><strong>Keep control of their actions.</strong></span><div><a href="/" data-cr-event="documentation_clicked">CTRLRun ↗</a><a href="/docs/get-started/quickstart">Quickstart ↗</a><a href="https://github.com/CTRLRun/ctrlrun" data-cr-event="github_clicked">GitHub ↗</a></div></div> + <div className="cr-footer" role="contentinfo"><span>Give agents autonomy.<br /><strong>Keep control of their actions.</strong></span><div><a href="/" data-cr-event="documentation_clicked">ctrlrun ↗</a><a href="/docs/get-started/quickstart">Quickstart ↗</a><a href="https://github.com/CTRLRun/ctrlrun" data-cr-event="github_clicked">GitHub ↗</a></div></div> </div> diff --git a/generated/badges.readme.md b/generated/badges.readme.md index ae02d9c..0cbd10d 100644 --- a/generated/badges.readme.md +++ b/generated/badges.readme.md @@ -8,7 +8,7 @@ <a href="https://github.com/CTRLRun/ctrlrun/actions/workflows/codeql.yml"><img src="https://github.com/CTRLRun/ctrlrun/actions/workflows/codeql.yml/badge.svg?branch=main" alt="CodeQL"></a> <a href="https://github.com/CTRLRun/ctrlrun/actions/workflows/fuzz.yml"><img src="https://github.com/CTRLRun/ctrlrun/actions/workflows/fuzz.yml/badge.svg?branch=main" alt="Fuzz"></a> <a href="https://docs.ctrlrun.dev/how-this-is-built"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/CTRLRun/ctrlrun/badges/tests-badge.json" alt="Tests"></a> - <a href="https://docs.ctrlrun.dev/security/verify-guarantees"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/CTRLRun/ctrlrun/badges/verify-badge.json" alt="CTRLRun verified"></a> + <a href="https://docs.ctrlrun.dev/security/verify-guarantees"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/CTRLRun/ctrlrun/badges/verify-badge.json" alt="ctrlrun verified"></a> <a href="https://scorecard.dev/viewer/?uri=github.com/CTRLRun/ctrlrun"><img src="https://api.scorecard.dev/projects/github.com/CTRLRun/ctrlrun/badge" alt="OpenSSF Scorecard"></a> <a href="https://www.bestpractices.dev/projects/14615"><img src="https://www.bestpractices.dev/projects/14615/badge" alt="OpenSSF Best Practices"></a> <a href="https://github.com/CTRLRun/ctrlrun/blob/main/LICENSE"><img src="https://img.shields.io/pypi/l/ctrlrun?color=B8730A" alt="License"></a> diff --git a/generated/capabilities.mdx b/generated/capabilities.mdx index 36a673b..0067793 100644 --- a/generated/capabilities.mdx +++ b/generated/capabilities.mdx @@ -40,7 +40,7 @@ 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. + 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. diff --git a/generated/capabilities.txt b/generated/capabilities.txt index 1fbdc0d..38fe873 100644 --- a/generated/capabilities.txt +++ b/generated/capabilities.txt @@ -11,7 +11,7 @@ generated from capabilities.yaml (text) — edit the YAML, never this list - Reconciliation: A reconcile hook asks the remote what happened and resolves an AMBIGUOUS effect. - Webhook approvals: Approval requests go to a webhook, such as Slack, and the answer comes back. - OpenTelemetry export: One span per action, one span event per step; argument values are opt-in. -- Consumed identity: A principal comes from a verified header or JWT; CTRLRun issues nothing. +- Consumed identity: A principal comes from a verified header or JWT; ctrlrun issues nothing. - Runtime delegation: A principal narrows its own grant at runtime; one revocation cuts the chain. - Observe mode: Records what enforcement would have blocked, blocks nothing, and counts it. - Verify: Runs the guarantee catalogue against your policy and store; N/A is not a pass. diff --git a/images/wordmark.svg b/images/wordmark.svg index 0da3149..de79186 100644 --- a/images/wordmark.svg +++ b/images/wordmark.svg @@ -1,4 +1,4 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="600" height="120" viewBox="0 0 600 120" role="img" aria-label="CTRLRun"> +<svg xmlns="http://www.w3.org/2000/svg" width="600" height="120" viewBox="0 0 600 120" role="img" aria-label="ctrlrun"> <style>.t{fill:#14161b}@media (prefers-color-scheme: dark){.t{fill:#f2f3f5}}</style> <rect x="8" y="18" width="94" height="88" rx="21" fill="#B8730A"/> <rect x="8" y="10" width="94" height="88" rx="21" fill="#F5A623"/> diff --git a/index.mdx b/index.mdx index 834c2e3..0f6aaf6 100644 --- a/index.mdx +++ b/index.mdx @@ -2,22 +2,22 @@ title: "Stop wrong, restricted, or malicious AI agent actions" sidebarTitle: "Overview" description: "The last check before an AI agent does something it can't undo. Autonomy belongs to the action, not the agent." -"og:title": "CTRLRun: stop wrong, restricted, or malicious agent actions" -"twitter:title": "CTRLRun: stop wrong, restricted, or malicious agent actions" +"og:title": "ctrlrun: stop wrong, restricted, or malicious agent actions" +"twitter:title": "ctrlrun: stop wrong, restricted, or malicious agent actions" canonical: "https://ctrlrun.dev/" "og:url": "https://ctrlrun.dev/" --- import { HowDiagram } from "/snippets/how-diagram.jsx"; -**CTRLRun stops AI agents from taking wrong, restricted, or malicious actions in your workflows.** +**ctrlrun stops AI agents from taking wrong, restricted, or malicious actions in your workflows.** Every action is checked against your rules before it runs. Allowed actions go through. Sensitive ones wait for a person. Forbidden ones are blocked. -CTRLRun is a Python library that sits between an agent's decision to act and the call that acts. +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. +the outcome is unknown, ctrlrun says so instead of guessing. ```bash pip install ctrlrun && ctrlrun demo @@ -28,7 +28,7 @@ is production-grade on one host; Postgres is for many. Apache-2.0. ## How it works -CTRLRun stops an agent from taking an action your rules do not allow. Every action that leaves +ctrlrun stops an agent from taking an action your rules do not allow. Every action that leaves your agents, tools and workflows is normalized into one action, decided against your policy, held for a person where you require it, reserved so it cannot run twice, executed, resolved and recorded. An action with no rule is blocked, arguments changed after sign-off void the approval, @@ -46,7 +46,7 @@ have.** If it acts through your systems, it is checked. ## Protect one function -CTRLRun wraps the call that has the consequence, and a YAML file says how much autonomy that +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 @@ -92,7 +92,7 @@ with ctrlrun.context(agent="refund-agent"): What the same function does next, and what stops it: -| The agent | CTRLRun | +| The agent | ctrlrun | |---|---| | refunds €100 | runs it; one receipt | | refunds €2,000 | raises `ApprovalRequired`; `ctrlrun approve <id>` from the shell lets it through | @@ -113,7 +113,7 @@ reply is lost, the agent retries, and the retry is refused. The customer was ref ```console $ ctrlrun demo -CTRLRun demo — five ways an agent action goes wrong, and what stops it. +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 @@ -174,7 +174,7 @@ the same decisions without an install, or read the full transcript in the 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. + 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. @@ -251,7 +251,7 @@ the framework's own interrupt, and a framework with no such primitive does not n ## Built on this kernel -Two products run on CTRLRun and credit it on every page. [ctrl ai agents](https://ctrlaiagents.com), the hosted product for a person or a team, puts this boundary under any agent you buy or build, with the inbox, the receipts and the analysis in one dashboard. [ctrl payments](https://ctrlpayments.com) is the same boundary for money: every payment an agent attempts is allowed, held for a person, or refused before it leaves, and there is a receipt either way. The kernel that decides and refuses is this one, Apache-2.0, and the receipt format they write is the one documented here. +Two products run on ctrlrun and credit it on every page. [ctrl ai agents](https://ctrlaiagents.com), the hosted product for a person or a team, puts this boundary under any agent you buy or build, with the inbox, the receipts and the analysis in one dashboard. [ctrl payments](https://ctrlpayments.com) is the same boundary for money: every payment an agent attempts is allowed, held for a person, or refused before it leaves, and there is a receipt either way. The kernel that decides and refuses is this one, Apache-2.0, and the receipt format they write is the one documented here. ## Start here @@ -306,6 +306,6 @@ domain the URL moves with it; the current one is always in this block. ## Next -- [Why](/docs/why): what CTRLRun believes and why. +- [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. diff --git a/pyproject.toml b/pyproject.toml index 3518e70..8125581 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,4 @@ -# This repository ships no package. It holds the CTRLRun documentation site, the tools that +# This repository ships no package. It holds the ctrlrun documentation site, the tools that # render it from the library's own source, and the tests that check it -- so the only thing # here is configuration for those tools. # diff --git a/scripts/render-how-diagram.py b/scripts/render-how-diagram.py index 86a5e24..b29926b 100644 --- a/scripts/render-how-diagram.py +++ b/scripts/render-how-diagram.py @@ -48,13 +48,13 @@ ) BOT = ("YOUR SYSTEMS", "The action arrives already checked.") BOT_NOTE = "Allowed by your rules, approved where you require it, and never run twice." -BOX_LABEL = "CTRLRun · THE EXECUTION BOUNDARY" +BOX_LABEL = "ctrlrun · THE EXECUTION BOUNDARY" # The commercial band that used to sit on the boundary is gone. The tiers have their own # section directly below the diagram, and the drawing repeated it a screen early. DESC = ( - "An action leaves the agents, tools and workflows you already run. Inside CTRLRun it is " + "An action leaves the agents, tools and workflows you already run. Inside ctrlrun it is " "normalized into one action, decided against your rules, held for approval, reserved so it " "cannot run twice, executed, resolved and recorded. Only then does it reach your systems. " "An action with no rule is blocked, changed arguments void the approval, and an unknown " @@ -133,7 +133,7 @@ def wide() -> str: return ( f' <svg className="cr-diagram cr-dia-wide" viewBox="0 0 1200 {total}" role="img"\n' ' aria-labelledby="cr-dia-t cr-dia-d" preserveAspectRatio="xMidYMid meet">\n' - ' <title id="cr-dia-t">How CTRLRun works\n' + ' How ctrlrun works\n' f' {esc(DESC)}\n {body}\n \n' ) @@ -194,7 +194,7 @@ def narrow() -> str: return ( f' \n' - ' How CTRLRun works\n' + ' How ctrlrun works\n' f' {esc(DESC)}\n' f" {joined}\n \n" ) diff --git a/snippets/architecture-review.jsx b/snippets/architecture-review.jsx index 971e516..55506a6 100644 --- a/snippets/architecture-review.jsx +++ b/snippets/architecture-review.jsx @@ -31,7 +31,7 @@ export const ArchitectureReview = () => { }, []); useEffect(() => { if (prepared && reviewRef.current) reviewRef.current.focus(); }, [prepared]); const brief = ['Architecture review request', '', 'Company: ' + company, 'Reply email: ' + emailAddress, domain && 'Domain: ' + domain, 'Agent purpose: ' + purpose, 'Actions it can execute: ' + actions, 'Production status: ' + status, 'Primary concerns: ' + (concerns.join(', ') || 'Discuss during review'), risk && 'Execution risk check: ' + risk].filter(line => line !== false).join('\n'); - const email = 'mailto:contact@arpanghoshal.com?subject=' + encodeURIComponent('CTRLRun architecture review: ' + company) + '&body=' + encodeURIComponent(brief); + const email = 'mailto:contact@arpanghoshal.com?subject=' + encodeURIComponent('ctrlrun architecture review: ' + company) + '&body=' + encodeURIComponent(brief); const sendReview = async () => { if (sendingRef.current || sent) return; sendingRef.current = true; setSending(true); setError(''); @@ -60,8 +60,8 @@ export const ArchitectureReview = () => {