From 0c914166b0cebf92513693f1c5c8bac91063f429 Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 9 Sep 2026 05:46:10 +0530 Subject: [PATCH 1/6] Make the homepage minimal and route the demo through /try The homepage embedded the full 48-domain ScenarioExplorer and also linked to /try, which renders the same component. The refund story's CTA said "in the demo" while pointing off-page, past the demo sitting below it. Drop the inline explorer. The try section becomes a compact card whose primary button is the refund deep link the failure story sets up (/try?domain=Payments&action=Refund payment&situation=uncertain), with a second link for picking your own domain and a third for the real wheel in the browser. Hero and footer "Try CTRLRun" now go to /try. Also tighten the how-it-works and integration copy, and add a direct contact link to the commercial section and the footer. --- docs/index.mdx | 33 +++++++++++++++++++-------------- docs/style.css | 6 ++++++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/docs/index.mdx b/docs/index.mdx index 1b4769e..8a94592 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -9,8 +9,6 @@ canonical: "https://ctrlrun.dev/" "og:url": "https://ctrlrun.dev/" --- -import { ScenarioExplorer } from "/snippets/scenario-explorer.jsx"; -

CONTROL THE ACTION. KEEP THE AUTONOMY.

@@ -18,7 +16,7 @@ import { ScenarioExplorer } from "/snippets/scenario-explorer.jsx";

Let agents act. Keep control of what happens next.

CTRLRun sits at the execution boundary before an AI agent changes a real system. It prevents duplicate effects, binds approval to the exact action, and stops blind retries when the outcome is uncertain.

- Try CTRLRun + Try CTRLRun Documentation Open source on GitHub
@@ -35,35 +33,42 @@ import { ScenarioExplorer } from "/snippets/scenario-explorer.jsx";

ONE LOST RESPONSE. TWO REFUNDS.

The refund succeeded. The agent thinks it failed.

An agent refunds $500. The provider processes it, but the response is lost. The agent sees an error and tries again.

-
WITHOUT CTRLRUN

Retry another $500 may go out.

WITH CTRLRUN

Outcome uncertain → pause → confirm what happened → only then continue.

Walk through this exact case in the demo →

+
WITHOUT CTRLRUN

Retry another $500 may go out.

WITH CTRLRUN

Outcome uncertain → pause → confirm what happened → only then continue.

-

TRY THE EXECUTION BOUNDARY

Choose an action. See what CTRLRun stops.

Your model chooses the action. CTRLRun decides whether it may execute. Pick a domain, pick the moment it goes wrong, and read which check refused it.

No login. No API key. No setup.
48 domains, 7 ways an action goes wrong.
Open the demo on its own page ↗

- +

TRY THE EXECUTION BOUNDARY

Choose an action. See what CTRLRun stops.

+
+

Your model chooses the action. CTRLRun decides whether it may execute. Pick a domain — there are 48 — then pick the moment it goes wrong: a lost reply, an approval the agent edited, a retry of something that already happened. You get the decision, the check that produced it, and the line your own code would see.

No login. No API key. No setup.

+ +

HOW IT WORKS

Where CTRLRun sits,
in plain English.

A library inside your process.
Not a service in front of it.

Your agent decides what to do. Somewhere after that, your code calls something that changes the real world: charges a card, deletes a user, sends the email. CTRLRun goes in that one gap, and only that gap. It never sees the prompt, the reasoning or the conversation.

-
01 / YOU NAME THE ACTION

Wrap the function that causes the effect

You give it a name and an effect key: the identity of the business action, like refund:txn_4821. Two attempts at the same business action carry the same key, whether they come from a retry, a second worker, or a process that restarted.

-
02 / IT CHECKS BEFORE THE CALL

Four questions, every time

Is this agent entitled to act at all? Does policy allow these exact arguments, require a person, or deny them? If a human approved something, was it this exact action? Could this action already have happened? Anything unclear stops the call.

-
03 / IT RESERVES FIRST

The claim is written before the effect

The effect is reserved in a database before your function runs, not after it returns. That ordering is the whole trick: if the process dies mid-call, the reservation is still there, so the retry meets a claim instead of an empty table.

-
04 / IT RECORDS WHAT HAPPENED

Including “we do not know”

Committed, failed, or unknown. A timeout is not a failure, it is an unknown outcome, and CTRLRun holds it that way until something confirms it. A receipt records the action, the decision behind it, the approval and the result.

+
01 / YOU NAME THE ACTION

Wrap the function that causes the effect

You give it a name and an effect key: the identity of the business action, like refund:txn_4821. A retry, a second worker and a restarted process all carry the same key.

+
02 / IT CHECKS BEFORE THE CALL

Four questions, every time

Is this agent entitled to act at all? Does policy allow these exact arguments? If a human approved something, was it this exact action? Could this action already have happened? Anything unclear stops the call.

+
03 / IT RESERVES FIRST

The claim is written before the effect

The effect is reserved in a database before your function runs, not after it returns. If the process dies mid-call, the reservation is still there, so the retry meets a claim instead of an empty table.

+
04 / IT RECORDS WHAT HAPPENED

Including “we do not know”

Committed, failed, or unknown. A timeout is not a failure, and CTRLRun holds it that way until something confirms it. A receipt records the action, the decision, the approval and the result.

-

What it is not. It does not read prompts, choose models, rank tools, or judge whether the agent's reasoning was any good. It is not an LLM, and it does not call one. It checks the action, not the thinking.

Read the architecture: the modules, the store, the receipt chain →
+

What it is not. It does not read prompts, choose models, rank tools, or judge whether the agent's reasoning was any good. It is not an LLM, and it does not call one. It checks the action, not the thinking.

Read the architecture →
-

FOR THE PEOPLE BUILDING IT

Start at the function
that takes action.

Wrap the call that causes the real-world effect. Policy decides whether it can run, needs approval, or must stop. CTRLRun protects the effect and records what happened.

Python, agent frameworks, or an MCP gateway.
The execution boundary stays the same.

Execution state survives restarts, so an uncertain outcome still blocks a blind retry. How recovery works →

Start integrating
+

FOR THE PEOPLE BUILDING IT

Start at the function
that takes action.

Wrap the call that causes the real-world effect. Policy decides whether it can run, needs approval, or must stop. CTRLRun protects the effect and records what happened.

Python, agent frameworks, or an MCP gateway — the execution boundary stays the same. Execution state survives restarts, so an uncertain outcome still blocks a blind retry. How recovery works →

Start integrating
pip install ctrlrun
{"import ctrlrun\n\n@ctrlrun.protect(\n    \"stripe.refund\",\n    effect=\"refund:{payment_id}\"\n)\ndef refund(payment_id, amount):\n    ..."}

Illustrative wrapper. The quickstart adds your policy, identity, and approval flow.

  • allow → execute
  • approve → wait for exact approval
  • deny → do not execute
  • unknown outcome → do not retry blindly

Not only agents. The same execution boundary protects workers, webhooks, scheduled jobs, and other systems that can retry a consequential action. Learn why →

-

FOR AGENTS MOVING INTO PRODUCTION

Know which actions are safe to automate.

Get a focused review of your agent's actions, permissions, approval paths, duplicate-execution risks, and retry behavior. Leave with a concrete execution-control map: what can run automatically, what needs a person, and what must stop until the outcome is known.

Get an execution-safety review Take the free risk check →

Architecture reviews · Integration sprints · Production support
CTRLRun OSS remains Apache-2.0 and free.

+

FOR AGENTS MOVING INTO PRODUCTION

Know which actions are safe to automate.

Get a focused review of your agent's actions, permissions, approval paths, duplicate-execution risks, and retry behavior. Leave with a concrete execution-control map: what can run automatically, what needs a person, and what must stop until the outcome is known.

Get an execution-safety review Take the free risk check →Or just email me: contact@arpanghoshal.com ↗

Architecture reviews · Integration sprints · Production support
CTRLRun OSS remains Apache-2.0 and free.

-
Give agents autonomy.
Keep control of their actions.
Try CTRLRun ↗Read the docs →GitHub ↗
+
Give agents autonomy.
Keep control of their actions.
Try CTRLRun ↗Read the docs →GitHub ↗Contact ↗
diff --git a/docs/style.css b/docs/style.css index 51097af..0b2285c 100644 --- a/docs/style.css +++ b/docs/style.css @@ -46,6 +46,10 @@ .cr-safe strong { color:var(--cr-ink); } .cr-section-heading { display:flex; justify-content:space-between; gap:20px; align-items:flex-end; margin-bottom:28px; } .cr-section-heading .cr-eyebrow { margin-bottom:12px; } +.cr-try-card { display:flex; justify-content:space-between; align-items:center; gap:44px; padding:30px 32px; border:1px solid var(--cr-line); border-radius:8px; background:var(--cr-paper); } +.cr-site .cr-try-card p { font-size:14px; line-height:1.75; max-width:620px; } +.cr-try-card .cr-caption { margin-top:12px; } +.cr-try-actions { display:flex; flex-direction:column; align-items:flex-start; gap:12px; flex-shrink:0; } .cr-explorer { border:1px solid var(--cr-line); border-radius:8px; background:var(--cr-paper); } .cr-demo-toolbar { display:grid; grid-template-columns:1fr 1.1fr 1.1fr; gap:16px; padding:22px; border-bottom:1px solid var(--cr-line); } .cr-field { display:flex; flex-direction:column; gap:8px; font-size:12px; font-weight:550; min-width:0; } @@ -149,6 +153,7 @@ .cr-control { padding:18px 14px; } .cr-boundary-node strong { font-size:21px; } .cr-failure,.cr-integration,.cr-commercial { gap:30px; } + .cr-try-card { gap:28px; } .cr-demo-toolbar { grid-template-columns:1fr 1fr; } .cr-demo-toolbar>.cr-field:last-child { grid-column:1/-1; } .cr-section-heading { align-items:flex-start; flex-direction:column; gap:10px; } @@ -165,6 +170,7 @@ .cr-flow-arrow { transform:rotate(90deg); line-height:1; } .cr-boundary-node>span:last-child { font-size:12px; } .cr-failure,.cr-integration,.cr-commercial,.cr-demo-stage { grid-template-columns:1fr; } + .cr-try-card { flex-direction:column; align-items:flex-start; gap:22px; padding:24px; } .cr-section { padding:40px 0; } .cr-demo-toolbar { grid-template-columns:1fr; padding:18px; gap:14px; } .cr-demo-toolbar>.cr-field:last-child { grid-column:auto; } From 4610fe0f74f7bdc88c74d6ba08393547d6ccbcd9 Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 9 Sep 2026 05:54:54 +0530 Subject: [PATCH 2/6] Rebuild the homepage around the adopt deck's two ideas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The page carried seven sections and said the same thing three times: a boundary strip (propose / check / act), a four-step how-it-works, and a try card that re-explained the demo the failure story had just set up. Follow the adopt.ctrlrun.dev deck, which says it in two moves. The boundary strip and the refund comparison collapse into one section: the same failure as two chains, without controls and with CTRLRun, ending at $1,000 and $500 once. The try CTAs move onto the end of that chain, where the reader already has the case in mind, so the standalone try card goes. The four how-it-works steps become the deck's three pillars — set limits, check actions, know what happened — with the deck's wording. Hero keeps one line, the deck's promise, instead of a lede plus an intro. Integration and commercial each lose a paragraph. Seven sections to five, 820 words to 555. CSS: drop .cr-boundary, .cr-boundary-node, .cr-control and .cr-try-card with their responsive rules, all now unused; add .cr-chain and .cr-pillar. The classes try.mdx shares — .cr-failure, .cr-comparison, .cr-how-steps, .cr-how-close, .cr-flow-arrow — stay. --- docs/index.mdx | 77 +++++++++++++++++++++++++++++--------------------- docs/style.css | 57 +++++++++++++++++++++++-------------- 2 files changed, 81 insertions(+), 53 deletions(-) diff --git a/docs/index.mdx b/docs/index.mdx index 8a94592..e5908ed 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -13,8 +13,7 @@ canonical: "https://ctrlrun.dev/"

CONTROL THE ACTION. KEEP THE AUTONOMY.

Execution safety
for AI agents.

-

Let agents act. Keep control of what happens next.

-

CTRLRun sits at the execution boundary before an AI agent changes a real system. It prevents duplicate effects, binds approval to the exact action, and stops blind retries when the outcome is uncertain.

+

Control what AI agents can do in your systems, what needs human approval, and what must never happen.

Try CTRLRun Documentation @@ -23,51 +22,65 @@ canonical: "https://ctrlrun.dev/"

Apache-2.0 · Runs in your environment
SQLite on one host · Postgres across hosts

-
-
01 / PROPOSEAI agentDecides what to do
- -
02 / CHECKCTRLRun_The control point before execution
  • Is this action allowed?
  • Is this exactly what was approved?
  • Could this action already have happened?
  • Do we know enough to continue safely?
- -
03 / ACTReal systemMove money. Change access.
Send email. Deploy software.
-
- -
-

ONE LOST RESPONSE. TWO REFUNDS.

The refund succeeded. The agent thinks it failed.

An agent refunds $500. The provider processes it, but the response is lost. The agent sees an error and tries again.

-
WITHOUT CTRLRUN

Retry another $500 may go out.

WITH CTRLRUN

Outcome uncertain → pause → confirm what happened → only then continue.

-
- -
-

TRY THE EXECUTION BOUNDARY

Choose an action. See what CTRLRun stops.

-
-

Your model chooses the action. CTRLRun decides whether it may execute. Pick a domain — there are 48 — then pick the moment it goes wrong: a lost reply, an approval the agent edited, a retry of something that already happened. You get the decision, the check that produced it, and the line your own code would see.

No login. No API key. No setup.

+
+
+

ONE LOST RESPONSE. TWO REFUNDS.

The refund succeeded.
The agent thinks it failed.

+

Nothing below is a bug in the agent.
Every step is a system behaving normally.

+
+
+
+ Without controls +
    +
  1. $500 refund issued
  2. Succeeds at the provider
  3. The response is lost
  4. The agent retries
  5. $1,000 paid
  6. +
+
+
+ With CTRLRun +
    +
  1. $500 refund issued
  2. Succeeds at the provider
  3. The response is lost
  4. CTRLRun holds the retry
  5. $500 paid, once
  6. +
+
+
+
+

Your model chooses the action. CTRLRun decides whether it may execute. Pick a domain — there are 48 — then pick the moment it goes wrong. No login. No API key. No setup.

-

HOW IT WORKS

Where CTRLRun sits,
in plain English.

A library inside your process.
Not a service in front of it.

-

Your agent decides what to do. Somewhere after that, your code calls something that changes the real world: charges a card, deletes a user, sends the email. CTRLRun goes in that one gap, and only that gap. It never sees the prompt, the reasoning or the conversation.

-
-
01 / YOU NAME THE ACTION

Wrap the function that causes the effect

You give it a name and an effect key: the identity of the business action, like refund:txn_4821. A retry, a second worker and a restarted process all carry the same key.

-
02 / IT CHECKS BEFORE THE CALL

Four questions, every time

Is this agent entitled to act at all? Does policy allow these exact arguments? If a human approved something, was it this exact action? Could this action already have happened? Anything unclear stops the call.

-
03 / IT RESERVES FIRST

The claim is written before the effect

The effect is reserved in a database before your function runs, not after it returns. If the process dies mid-call, the reservation is still there, so the retry meets a claim instead of an empty table.

-
04 / IT RECORDS WHAT HAPPENED

Including “we do not know”

Committed, failed, or unknown. A timeout is not a failure, and CTRLRun holds it that way until something confirms it. A receipt records the action, the decision, the approval and the result.

+
+

HOW IT WORKS

Set limits. Check actions.
Know what happened.

+

A library inside your process.
Not a service in front of it.

+
+

Models will be wrong sometimes. CTRLRun decides whether being wrong can reach your systems.

+
+
+ 01 / SET LIMITS

Your rules define what AI may reach

+
  • Action rulesAllow, require approval or block, before an action runs.
  • Agent permissionsEach agent's limits. Delegating work cannot widen them.
  • Data-use rulesControl actions by the data labels your team supplies.
+
+
+ 02 / CHECK ACTIONS

A wrong request need not become a costly action

+
  • Exact approvalsApprove the precise action. Changed details need a new one.
  • Duplicate preventionBlock repeats, even when different agents try.
  • Outcome checksHold retries until the outcome is confirmed.
+
+
+ 03 / KNOW WHAT HAPPENED

Trust backed by a record of what AI did

+
  • Action historyTrace requests, decisions and results, including refusals.
  • Control testingPreview what rules would block before enforcing them.
+

What it is not. It does not read prompts, choose models, rank tools, or judge whether the agent's reasoning was any good. It is not an LLM, and it does not call one. It checks the action, not the thinking.

Read the architecture →
-

FOR THE PEOPLE BUILDING IT

Start at the function
that takes action.

Wrap the call that causes the real-world effect. Policy decides whether it can run, needs approval, or must stop. CTRLRun protects the effect and records what happened.

Python, agent frameworks, or an MCP gateway — the execution boundary stays the same. Execution state survives restarts, so an uncertain outcome still blocks a blind retry. How recovery works →

Start integrating
-
pip install ctrlrun
{"import ctrlrun\n\n@ctrlrun.protect(\n    \"stripe.refund\",\n    effect=\"refund:{payment_id}\"\n)\ndef refund(payment_id, amount):\n    ..."}

Illustrative wrapper. The quickstart adds your policy, identity, and approval flow.

  • allow → execute
  • approve → wait for exact approval
  • deny → do not execute
  • unknown outcome → do not retry blindly
-

Not only agents. The same execution boundary protects workers, webhooks, scheduled jobs, and other systems that can retry a consequential action. Learn why →

+

FOR THE PEOPLE BUILDING IT

Start at the function
that takes action.

Wrap the call that causes the real-world effect. Policy decides whether it can run, needs approval, or must stop. Execution state survives restarts, so an uncertain outcome still blocks a blind retry.

Start integrating
+
pip install ctrlrun
{"import ctrlrun\n\n@ctrlrun.protect(\n    \"stripe.refund\",\n    effect=\"refund:{payment_id}\"\n)\ndef refund(payment_id, amount):\n    ..."}
  • allow → execute
  • approve → wait for exact approval
  • deny → do not execute
  • unknown outcome → do not retry blindly
+

Python, agent frameworks, or an MCP gateway — the execution boundary stays the same, and it protects workers, webhooks and scheduled jobs too. Not only agents →

-

FOR AGENTS MOVING INTO PRODUCTION

Know which actions are safe to automate.

Get a focused review of your agent's actions, permissions, approval paths, duplicate-execution risks, and retry behavior. Leave with a concrete execution-control map: what can run automatically, what needs a person, and what must stop until the outcome is known.

Get an execution-safety review Take the free risk check →Or just email me: contact@arpanghoshal.com ↗

Architecture reviews · Integration sprints · Production support
CTRLRun OSS remains Apache-2.0 and free.

+

FOR AGENTS MOVING INTO PRODUCTION

Know which actions are safe to automate.

Leave with a concrete execution-control map: what can run automatically, what needs a person, and what must stop until the outcome is known.

Get an execution-safety review Take the free risk check →Or just email me: contact@arpanghoshal.com ↗

Architecture reviews · Integration sprints · Production support
CTRLRun OSS remains Apache-2.0 and free.

Give agents autonomy.
Keep control of their actions.
diff --git a/docs/style.css b/docs/style.css index 0b2285c..2cac55d 100644 --- a/docs/style.css +++ b/docs/style.css @@ -27,14 +27,6 @@ .cr-site .cr-github-badge { gap:10px; color:var(--cr-accent); background:var(--cr-panel); border-color:var(--cr-accent); } .cr-github-badge svg { flex-shrink:0; } .cr-site .cr-caption { font-size:12px; line-height:1.7; color:var(--cr-muted); } -.cr-boundary { display:grid; grid-template-columns:1fr 40px 1.3fr 40px 1fr; gap:15px; align-items:center; padding:28px 32px; background:var(--cr-paper); border-top:1px solid var(--cr-line); border-bottom:1px solid var(--cr-line); } -.cr-boundary-node { display:flex; flex-direction:column; gap:8px; } -.cr-boundary-node strong { font-size:25px; font-weight:550; letter-spacing:-.03em; } -.cr-boundary-node>span:last-child { font-size:13px; color:var(--cr-muted); line-height:1.7; } -.cr-control { padding:22px 24px; background:var(--cr-panel); border-left:2px solid var(--cr-accent); } -.cr-control>span:nth-child(3) { font-size:11px; color:var(--cr-muted); } -.cr-control ul { list-style:none; padding:12px 0 0; font-size:12px; line-height:1.85; } -.cr-control li { margin:0; padding:0; } .cr-flow-arrow { font-size:26px; color:var(--cr-muted); text-align:center; } .cr-section { padding:64px 0; border-bottom:1px solid var(--cr-line); scroll-margin-top:100px; } .cr-section h2+p { margin-top:18px; } @@ -46,9 +38,6 @@ .cr-safe strong { color:var(--cr-ink); } .cr-section-heading { display:flex; justify-content:space-between; gap:20px; align-items:flex-end; margin-bottom:28px; } .cr-section-heading .cr-eyebrow { margin-bottom:12px; } -.cr-try-card { display:flex; justify-content:space-between; align-items:center; gap:44px; padding:30px 32px; border:1px solid var(--cr-line); border-radius:8px; background:var(--cr-paper); } -.cr-site .cr-try-card p { font-size:14px; line-height:1.75; max-width:620px; } -.cr-try-card .cr-caption { margin-top:12px; } .cr-try-actions { display:flex; flex-direction:column; align-items:flex-start; gap:12px; flex-shrink:0; } .cr-explorer { border:1px solid var(--cr-line); border-radius:8px; background:var(--cr-paper); } .cr-demo-toolbar { display:grid; grid-template-columns:1fr 1.1fr 1.1fr; gap:16px; padding:22px; border-bottom:1px solid var(--cr-line); } @@ -149,11 +138,7 @@ @media (max-width:800px) { .cr-site { padding-left:24px; padding-right:24px; } .cr-hero { padding-top:55px; } - .cr-boundary { gap:10px; padding:22px 16px; grid-template-columns:1fr 22px 1.4fr 22px 1fr; } - .cr-control { padding:18px 14px; } - .cr-boundary-node strong { font-size:21px; } .cr-failure,.cr-integration,.cr-commercial { gap:30px; } - .cr-try-card { gap:28px; } .cr-demo-toolbar { grid-template-columns:1fr 1fr; } .cr-demo-toolbar>.cr-field:last-child { grid-column:1/-1; } .cr-section-heading { align-items:flex-start; flex-direction:column; gap:10px; } @@ -163,14 +148,8 @@ .cr-hero { padding:42px 0 35px; } .cr-site .cr-lede { font-size:18px; } .cr-site .cr-intro { font-size:15px; } - .cr-boundary { grid-template-columns:1fr; gap:12px; text-align:center; padding:25px 20px; } - .cr-boundary-node { align-items:center; gap:4px; } - .cr-control { width:100%; padding:20px; } - .cr-control ul { display:grid; grid-template-columns:1fr 1fr; text-align:left; column-gap:16px; font-size:11px; } .cr-flow-arrow { transform:rotate(90deg); line-height:1; } - .cr-boundary-node>span:last-child { font-size:12px; } .cr-failure,.cr-integration,.cr-commercial,.cr-demo-stage { grid-template-columns:1fr; } - .cr-try-card { flex-direction:column; align-items:flex-start; gap:22px; padding:24px; } .cr-section { padding:40px 0; } .cr-demo-toolbar { grid-template-columns:1fr; padding:18px; gap:14px; } .cr-demo-toolbar>.cr-field:last-child { grid-column:auto; } @@ -348,3 +327,39 @@ body:has(.cr-site) { --topbar-tabs-height:0px; } .cr-checks { gap:11px; } } .cr-site .cr-demo-note { padding:14px 22px; font-size:11.5px; line-height:1.7; } + +/* Homepage: the same failure as two chains, and the three control pillars. */ +.cr-failure-chain .cr-section-heading { margin-bottom:32px; } +.cr-chain { display:flex; flex-direction:column; gap:14px; } +.cr-chain-row { display:grid; grid-template-columns:132px 1fr; align-items:stretch; gap:14px; } +.cr-site .cr-chain-label { display:flex; align-items:center; font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:10px; font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--cr-muted); } +.cr-site .cr-chain-steps { list-style:none; display:flex; align-items:stretch; gap:22px; padding:0; margin:0; } +.cr-chain-steps li { position:relative; flex:1; display:flex; align-items:center; justify-content:center; text-align:center; padding:14px 10px; border:1px solid var(--cr-line); background:var(--cr-paper); font-size:13px; line-height:1.4; color:var(--cr-muted); } +.cr-chain-steps li:not(:last-child)::after { content:"→"; position:absolute; right:-17px; top:50%; transform:translateY(-50%); color:var(--cr-accent); font-size:13px; line-height:1; } +.cr-chain-steps .cr-end { font-weight:600; border-width:2px; } +.cr-chain-steps .cr-end.cr-bad { color:#a13d32; border-color:#a13d32; background:transparent; } +.cr-chain-steps .cr-end.cr-good,.cr-site .cr-chain-label.cr-good { color:#3c7041; } +.cr-chain-steps .cr-end.cr-good { border-color:#3c7041; background:transparent; } +.dark .cr-chain-steps .cr-end.cr-bad { color:#f5a198; border-color:#f5a198; } +.dark .cr-chain-steps .cr-end.cr-good,.dark .cr-site .cr-chain-label.cr-good { color:#9ec793; } +.dark .cr-chain-steps .cr-end.cr-good { border-color:#9ec793; } +.cr-chain-cta { display:flex; justify-content:space-between; align-items:flex-end; gap:44px; margin-top:32px; padding-top:26px; border-top:1px solid var(--cr-line); } +.cr-site .cr-chain-cta p { font-size:13.5px; line-height:1.75; max-width:600px; } +.cr-pillars { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:26px; margin-top:34px; } +.cr-pillar { display:flex; flex-direction:column; gap:12px; padding:24px 22px 26px; border:1px solid var(--cr-line); border-top:2px solid var(--cr-accent); border-radius:6px; background:var(--cr-paper); } +.cr-pillar h3 { font-size:17px; letter-spacing:-.02em; } +.cr-site .cr-pillar ul { list-style:none; display:flex; flex-direction:column; gap:14px; padding:0; margin-top:4px; } +.cr-pillar li { position:relative; padding-left:16px; font-size:13px; line-height:1.65; color:var(--cr-muted); } +.cr-pillar li::before { content:""; position:absolute; left:0; top:.62em; width:6px; height:6px; border-radius:50%; background:var(--cr-accent); } +.cr-pillar li b { display:block; font-size:13.5px; font-weight:600; color:var(--cr-ink); } +@media (max-width:900px) { + .cr-pillars { grid-template-columns:1fr; gap:18px; } + .cr-chain-row { grid-template-columns:1fr; gap:8px; } + .cr-chain { gap:26px; } + .cr-chain-cta { flex-direction:column; align-items:flex-start; gap:22px; } +} +@media (max-width:600px) { + .cr-site .cr-chain-steps { flex-direction:column; gap:16px; } + .cr-chain-steps li { padding:11px 10px; font-size:12.5px; } + .cr-chain-steps li:not(:last-child)::after { content:"↓"; right:auto; left:50%; top:auto; bottom:-14px; transform:translateX(-50%); } +} From 856d424a9ccc89da20f0ccec458cf9f3b5b947f4 Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 9 Sep 2026 05:57:15 +0530 Subject: [PATCH 3/6] Tell the hallucination story in the chain, not the lost reply The lost-response chain asks the reader to already hold a distributed-systems mental model: a call succeeded, its reply vanished, the retry double-pays. That is CTRLRun's most distinctive guarantee and its least legible one on a first visit. The chain now shows the failure people actually fear. The agent invents a $50,000 payout, the tool is in its list, and without controls the call goes through. With CTRLRun policy denies those arguments and the call never leaves the process. It is the demo's existing `blocked` scenario, so "Walk through this refusal" deep-links to the same refusal the chain describes, and the eyebrow is that scenario's own kernel line: the tool was in its list, that is not permission. The caption states the limit rather than letting the story imply more than it should. CTRLRun cannot tell that the agent made it up; it can tell the action is not one this agent may take. The AMBIGUOUS case is not dropped, only demoted: it stays as pillar 02's outcome checks, as `unknown outcome` in the outcomes list, and it is now named in the demo CTA alongside the edited approval and the duplicate. --- docs/index.mdx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/index.mdx b/docs/index.mdx index e5908ed..c3c7dfd 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -24,27 +24,27 @@ canonical: "https://ctrlrun.dev/"
-

ONE LOST RESPONSE. TWO REFUNDS.

The refund succeeded.
The agent thinks it failed.

-

Nothing below is a bug in the agent.
Every step is a system behaving normally.

+

THE TOOL WAS IN ITS LIST. THAT IS NOT PERMISSION.

The agent was certain.
The agent was wrong.

+

CTRLRun cannot tell that the agent made it up.
It can tell the action is not one this agent may take.

Without controls
    -
  1. $500 refund issued
  2. Succeeds at the provider
  3. The response is lost
  4. The agent retries
  5. $1,000 paid
  6. +
  7. The ticket says: check a refund
  8. The agent invents a $50,000 payout
  9. The tool is in its list
  10. The call goes through
  11. $50,000 gone
With CTRLRun
    -
  1. $500 refund issued
  2. Succeeds at the provider
  3. The response is lost
  4. CTRLRun holds the retry
  5. $500 paid, once
  6. +
  7. The ticket says: check a refund
  8. The agent invents a $50,000 payout
  9. Policy denies these arguments
  10. The call never leaves
  11. $0 paid. Nobody is asked.
-

Your model chooses the action. CTRLRun decides whether it may execute. Pick a domain — there are 48 — then pick the moment it goes wrong. No login. No API key. No setup.

+

Your model chooses the action. CTRLRun decides whether it may execute. Pick a domain — there are 48 — then pick the moment it goes wrong: an approval the agent edited, a retry of something that already happened, a reply that never came back. No login. No API key. No setup.

From d01f0d86c35f3f01e4f99bab16e6afba519e70eb Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 9 Sep 2026 06:04:32 +0530 Subject: [PATCH 4/6] Carry the hallucination story to the README and the site copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The homepage chain changed story last commit; the README and the rest of the site still led with the lost reply. Three changes, in order of size. README: "The refund that happened twice" becomes "The refund nobody approved". It opens where a reader already is, an agent inventing a €50,000 payout that policy refuses, notes that an approval for €2,000 authorises nothing when €5,000 is executed, and only then turns to the half people do not expect, the correct €500 refund whose reply is lost. Both halves are what `ctrlrun demo` actually prints, and the recorded GIF shows both, so the narrative and the evidence still agree. The ambiguity guarantee is not softened anywhere it is documented: concepts, cookbook and the specs are untouched. Chain labels are symmetric now, "Without CTRLRun" and "With CTRLRun", on the homepage and in both decks. The section caption said what CTRLRun cannot do. It now says what it does: it checks the action rather than the reasoning, reads the arguments about to leave the process, and answers whether they may go. Em dashes are out of the site copy: index.mdx, try.mdx, docs.mdx and the two page snippets. The four left in docs.mdx are two lines of `ctrlrun demo` output a test asserts byte for byte, and two build comments that never render. tests/test_readme_assets.py pinned the old first-H2 title; it pins the new one. The assertion's point is that the README opens on a story rather than a feature table, and it still does. --- README.md | 30 ++++++++++++++++----------- docs/docs.mdx | 6 +++--- docs/index.mdx | 12 +++++------ docs/snippets/architecture-review.jsx | 4 ++-- docs/snippets/scenario-explorer.jsx | 6 +++--- docs/try.mdx | 8 +++---- tests/test_readme_assets.py | 2 +- 7 files changed, 37 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index fd18583..46f9ec6 100644 --- a/README.md +++ b/README.md @@ -41,18 +41,24 @@ transfers.

-## The refund that happened twice - -An agent refunds €500. The call reaches the provider and commits. The reply is lost on the way -back, so the agent sees an error — and does what every retrying client does. The customer is -refunded twice, and nothing in the stack noticed. - -The bug is not the retry. It is that the agent had no way to tell *this failed* from *I do not -know what happened*. Retry libraries, agent frameworks and tool loops collapse those two into -one, and a write that may already have committed is retried as though it certainly had not. - -CTRLRun does not collapse them. 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. +## The refund nobody approved + +An agent reads a ticket and decides the customer is owed €50,000. The tool is in its list, the +arguments are well-formed, and the model is completely confident. Nothing above the call +disagrees, because nothing above the call is a check: a tool being callable is not permission to +call it with those arguments. + +CTRLRun is that check. It reads the arguments about to leave your process and answers whether +this agent may send them. €50,000 is past the ceiling policy gives the agent, so the call never +leaves. Have a human approve €2,000 and then execute €5,000, and the approval authorises +nothing: it was bound to the action the human actually read. + +That is the half people expect. The other half is the same agent making a *correct* €500 refund +that commits at the provider while the reply is lost coming back. The agent sees an error and +retries, because retry libraries, agent frameworks and tool loops collapse *this failed* into *I +do not know what happened*. CTRLRun keeps them apart: 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. ```bash pip install ctrlrun && ctrlrun demo diff --git a/docs/docs.mdx b/docs/docs.mdx index 817cdc2..c545e99 100644 --- a/docs/docs.mdx +++ b/docs/docs.mdx @@ -3,13 +3,13 @@ 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" +"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 +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 diff --git a/docs/index.mdx b/docs/index.mdx index c3c7dfd..d538219 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -3,8 +3,8 @@ title: "Execution safety for AI agents" sidebarTitle: "CTRLRun" description: "Execution safety for AI agents: prevent duplicate effects, bind approval to the exact action, and stop blind retries when the outcome is uncertain." mode: "custom" -"og:title": "CTRLRun — Execution safety for AI agents" -"twitter:title": "CTRLRun — Execution safety for AI agents" +"og:title": "CTRLRun: execution safety for AI agents" +"twitter:title": "CTRLRun: execution safety for AI agents" canonical: "https://ctrlrun.dev/" "og:url": "https://ctrlrun.dev/" --- @@ -25,11 +25,11 @@ canonical: "https://ctrlrun.dev/"

THE TOOL WAS IN ITS LIST. THAT IS NOT PERMISSION.

The agent was certain.
The agent was wrong.

-

CTRLRun cannot tell that the agent made it up.
It can tell the action is not one this agent may take.

+

CTRLRun checks the action, not the reasoning.
It reads the arguments about to leave your process, and answers whether they may go.

- Without controls + Without CTRLRun
  1. The ticket says: check a refund
  2. The agent invents a $50,000 payout
  3. The tool is in its list
  4. The call goes through
  5. $50,000 gone
@@ -42,7 +42,7 @@ canonical: "https://ctrlrun.dev/"
-

Your model chooses the action. CTRLRun decides whether it may execute. Pick a domain — there are 48 — then pick the moment it goes wrong: an approval the agent edited, a retry of something that already happened, a reply that never came back. No login. No API key. No setup.

+

Your model chooses the action. CTRLRun decides whether it may execute. Pick one of 48 domains, then pick the moment it goes wrong: an approval the agent edited, a retry of something that already happened, a reply that never came back. No login. No API key. No setup.

Walk through this refusal Or pick your own domain ↗ @@ -76,7 +76,7 @@ canonical: "https://ctrlrun.dev/"

FOR THE PEOPLE BUILDING IT

Start at the function
that takes action.

Wrap the call that causes the real-world effect. Policy decides whether it can run, needs approval, or must stop. Execution state survives restarts, so an uncertain outcome still blocks a blind retry.

Start integrating
pip install ctrlrun
{"import ctrlrun\n\n@ctrlrun.protect(\n    \"stripe.refund\",\n    effect=\"refund:{payment_id}\"\n)\ndef refund(payment_id, amount):\n    ..."}
  • allow → execute
  • approve → wait for exact approval
  • deny → do not execute
  • unknown outcome → do not retry blindly
-

Python, agent frameworks, or an MCP gateway — the execution boundary stays the same, and it protects workers, webhooks and scheduled jobs too. Not only agents →

+

Python, agent frameworks, or an MCP gateway: the execution boundary stays the same, and it protects workers, webhooks and scheduled jobs too. Not only agents →

diff --git a/docs/snippets/architecture-review.jsx b/docs/snippets/architecture-review.jsx index 2a2e73b..971e516 100644 --- a/docs/snippets/architecture-review.jsx +++ b/docs/snippets/architecture-review.jsx @@ -26,12 +26,12 @@ export const ArchitectureReview = () => { if (['High', 'Needs review', 'Lower indicated risk', 'Planning'].includes(level)) { const count = Math.max(0, Math.min(5, Number(params.get('patterns')) || 0)); const unknowns = Math.max(0, Math.min(5, Number(params.get('unknowns')) || 0)); - setRisk(level + ' — ' + count + ' patterns, ' + unknowns + ' unknowns (self-reported)'); + setRisk(level + ': ' + count + ' patterns, ' + unknowns + ' unknowns (self-reported)'); } }, []); 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(''); diff --git a/docs/snippets/scenario-explorer.jsx b/docs/snippets/scenario-explorer.jsx index 41691d0..07bdf9a 100644 --- a/docs/snippets/scenario-explorer.jsx +++ b/docs/snippets/scenario-explorer.jsx @@ -81,7 +81,7 @@ export const ScenarioExplorer = ({ standalone = false }) => { tone: 'green', reason: 'This action is inside the autonomy you gave the agent, so no person is asked.', llm: 'This is within my limits, so I will call the tool.', - kernel: 'Agreed — and the effect is reserved first, so a second worker proposing the same thing now waits.', + kernel: 'Agreed, and the effect is reserved first, so a second worker proposing the same thing now waits.', rule: money ? 'Example rule: this agent may act autonomously up to $1,000.' : 'Example rule: this agent may perform this action on the selected target.', sees: 'decision=allow → your function runs', doc: { href: '/docs/concepts/decisions', label: 'Decisions' }, @@ -186,7 +186,7 @@ export const ScenarioExplorer = ({ standalone = false }) => { reason: 'The real system confirmed the action. The outcome and the decision behind it are written to a receipt.', llm: 'Done. I will report success.', kernel: 'Committed. Any later attempt at the same business action now has something to be refused against.', - rule: 'A receipt records the action, the decision and the outcome — including who approved it.', + rule: 'A receipt records the action, the decision and the outcome, including who approved it.', sees: 'outcome=committed · receipt rcp_4c2a…', doc: { href: '/docs/concepts/receipts-and-evidence', label: 'Receipts and evidence' }, marks: ['pass', 'pass', viaApproval ? 'pass' : 'skip', 'pass', 'pass'], @@ -308,7 +308,7 @@ export const ScenarioExplorer = ({ standalone = false }) => { {!standalone && Open on its own page ↗}
-

Nothing here executes: this is an illustration of the decisions, with example rules that are not industry defaults. To watch the real library refuse a real call, run the released wheel in your browser — Python and CTRLRun load into the tab and every refusal there is the library's own.{physical ? ' CTRLRun governs the software authorization and execution workflow; physical safety controls remain separate.' : ''}{domain === 'Healthcare Operations' ? ' Administrative workflows only; no diagnosis or clinical decisions.' : ''}

+

Nothing here executes: this is an illustration of the decisions, with example rules that are not industry defaults. To watch the real library refuse a real call, run the released wheel in your browser: Python and CTRLRun load into the tab and every refusal there is the library's own.{physical ? ' CTRLRun governs the software authorization and execution workflow; physical safety controls remain separate.' : ''}{domain === 'Healthcare Operations' ? ' Administrative workflows only; no diagnosis or clinical decisions.' : ''}

{reviewQuestion}

Review where execution controls belong in your architecture.

track('protect_clicked')}>Get a safety review ↗
); diff --git a/docs/try.mdx b/docs/try.mdx index e6d38a6..ba0649b 100644 --- a/docs/try.mdx +++ b/docs/try.mdx @@ -3,8 +3,8 @@ title: "Try the execution boundary" description: "Pick an agent action in your own domain and watch which check refuses it: policy, approval binding, effect reservation, or an unknown outcome." canonical: "https://ctrlrun.dev/try" "og:url": "https://ctrlrun.dev/try" -"og:title": "Try the execution boundary — CTRLRun" -"twitter:title": "Try the execution boundary — CTRLRun" +"og:title": "Try the execution boundary | CTRLRun" +"twitter:title": "Try the execution boundary | CTRLRun" mode: "custom" --- @@ -16,7 +16,7 @@ import { ScenarioExplorer } from "/snippets/scenario-explorer.jsx";

TRY THE EXECUTION BOUNDARY

Choose an action.
See what CTRLRun stops.

Your model chooses the action. CTRLRun decides whether it may execute.

-

Pick your domain — there are 48 — then pick the moment it goes wrong: a lost reply, an approval the agent edited, a retry of something that already happened. You get the decision, the check that produced it, and the line your own code would see.

+

Pick one of 48 domains, then pick the moment it goes wrong: an action the agent was never allowed to take, an approval the agent edited, a retry of something that already happened, a reply that never came back. You get the decision, the check that produced it, and the line your own code would see.

No login. No API key. No setup. Nothing here executes.
Every scenario has its own link, so you can send someone straight to the one that matters.

@@ -37,7 +37,7 @@ import { ScenarioExplorer } from "/snippets/scenario-explorer.jsx";
UNDERSTAND IT

How it works

Where CTRLRun sits, what it checks, and what it deliberately does not do. The plain-English version, then the architecture.

RUN IT

Protect your first action

pip install ctrlrun, then wrap the function that causes the effect. The quickstart adds your policy, identity and approval flow.

-
READ THE CASES

Concepts behind each refusal

Approval binding, effect keys, and outcomes and AMBIGUOUS — the three the demo leans on hardest.

+
READ THE CASES

Concepts behind each refusal

Approval binding, effect keys, and outcomes and AMBIGUOUS: the three the demo leans on hardest.

SEE IT IN A DOMAIN

A worked application

The medical affairs workbench puts the same kernel under a document release instead of a payment.

Not sure where the controls belong in your architecture? A focused review maps which of your agent's actions can run automatically, which need a person, and which must stop until the outcome is known.

Get an execution-safety review →
diff --git a/tests/test_readme_assets.py b/tests/test_readme_assets.py index 6ec3ac1..2c5c4b2 100644 --- a/tests/test_readme_assets.py +++ b/tests/test_readme_assets.py @@ -170,4 +170,4 @@ def test_the_header_carries_the_fixed_copy_and_the_five_badges(): assert marker not in head, "the capability matrix is not the first screen" assert marker in text, "the capability matrix was moved, not dropped" assert head.count("\n|---|") == 0, "no table above the first H2" - assert text.split("\n## ", 2)[1].startswith("The refund that happened twice") + assert text.split("\n## ", 2)[1].startswith("The refund nobody approved") From 594feec082b25a31478a0746fe0e3dfdc149f0be Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 9 Sep 2026 06:16:07 +0530 Subject: [PATCH 5/6] Turn the homepage into a three-slide deck The page was one continuous scroll. It is now three slides, one viewport each, grouped as: hero plus the failure chain; the demo CTA plus the three pillars; integration, commercial and the footer. Mechanics. The slides are plain markup in index.mdx inside a scroll-snap container, so with no JavaScript the page still reads, indexes, and snaps one slide at a time. snippets/deck-nav.jsx adds the position dots, the keyboard (arrows, page keys, space, Home, End) and the fit pass. It reads the theme's navbar height rather than assuming one, since the deck has to start below whatever chrome the docs theme renders. Fit is one scale for the whole deck, not one per slide. Scaling each slide against its own content puts neighbouring slides at different left edges, and arrowing between them reads as a mistake; a shared scale cannot drift. Slide 3 was the tall one, so its code block, outcome list and internal gaps came down until nothing needs scaling at all: at 1440x900 the shared scale is exactly 1 and all three slides start at the same x. No arrow buttons on screen. The dots are the only visible control, so they carry real labels and take focus rather than being decorative. Below 800px wide or 620px tall the deck is not the right shape, so it reverts to an ordinary scrolling page with the slides as sections and the dots hidden. Also: the navbar wordmark goes from 28px to 40px, 32px on small screens. --- docs/index.mdx | 37 +++++++++--- docs/snippets/deck-nav.jsx | 121 +++++++++++++++++++++++++++++++++++++ docs/style.css | 42 +++++++++++++ 3 files changed, 190 insertions(+), 10 deletions(-) create mode 100644 docs/snippets/deck-nav.jsx diff --git a/docs/index.mdx b/docs/index.mdx index d538219..c2615bf 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -9,8 +9,12 @@ canonical: "https://ctrlrun.dev/" "og:url": "https://ctrlrun.dev/" --- -
-
+import { DeckNav } from "/snippets/deck-nav.jsx"; + +
+
+
+

CONTROL THE ACTION. KEEP THE AUTONOMY.

Execution safety
for AI agents.

Control what AI agents can do in your systems, what needs human approval, and what must never happen.

@@ -20,9 +24,9 @@ canonical: "https://ctrlrun.dev/" Open source on GitHub

Apache-2.0 · Runs in your environment
SQLite on one host · Postgres across hosts

-
+
-
+

THE TOOL WAS IN ITS LIST. THAT IS NOT PERMISSION.

The agent was certain.
The agent was wrong.

CTRLRun checks the action, not the reasoning.
It reads the arguments about to leave your process, and answers whether they may go.

@@ -41,6 +45,12 @@ canonical: "https://ctrlrun.dev/"
+
+ + + +
+

Your model chooses the action. CTRLRun decides whether it may execute. Pick one of 48 domains, then pick the moment it goes wrong: an approval the agent edited, a retry of something that already happened, a reply that never came back. No login. No API key. No setup.

@@ -48,9 +58,8 @@ canonical: "https://ctrlrun.dev/" Or pick your own domain ↗
-
-
+

HOW IT WORKS

Set limits. Check actions.
Know what happened.

A library inside your process.
Not a service in front of it.

@@ -71,17 +80,25 @@ canonical: "https://ctrlrun.dev/"

What it is not. It does not read prompts, choose models, rank tools, or judge whether the agent's reasoning was any good. It is not an LLM, and it does not call one. It checks the action, not the thinking.

Read the architecture →
+ +
-
+
+
+

FOR THE PEOPLE BUILDING IT

Start at the function
that takes action.

Wrap the call that causes the real-world effect. Policy decides whether it can run, needs approval, or must stop. Execution state survives restarts, so an uncertain outcome still blocks a blind retry.

Start integrating
pip install ctrlrun
{"import ctrlrun\n\n@ctrlrun.protect(\n    \"stripe.refund\",\n    effect=\"refund:{payment_id}\"\n)\ndef refund(payment_id, amount):\n    ..."}
  • allow → execute
  • approve → wait for exact approval
  • deny → do not execute
  • unknown outcome → do not retry blindly

Python, agent frameworks, or an MCP gateway: the execution boundary stays the same, and it protects workers, webhooks and scheduled jobs too. Not only agents →

-
+ -
+

FOR AGENTS MOVING INTO PRODUCTION

Know which actions are safe to automate.

Leave with a concrete execution-control map: what can run automatically, what needs a person, and what must stop until the outcome is known.

Get an execution-safety review Take the free risk check →Or just email me: contact@arpanghoshal.com ↗

Architecture reviews · Integration sprints · Production support
CTRLRun OSS remains Apache-2.0 and free.

+
+ +
Give agents autonomy.
Keep control of their actions.
+
-
Give agents autonomy.
Keep control of their actions.
+ diff --git a/docs/snippets/deck-nav.jsx b/docs/snippets/deck-nav.jsx new file mode 100644 index 0000000..abf5b80 --- /dev/null +++ b/docs/snippets/deck-nav.jsx @@ -0,0 +1,121 @@ +/* Controls for the homepage deck. The slides themselves are plain markup in + index.mdx, so the page still reads and indexes with no JavaScript: without + this component it is a scroll-snap deck, and with it the arrows, the dots + and the keyboard work. */ +export const DeckNav = ({ labels = [] }) => { + const [active, setActive] = useState(0); + const [count, setCount] = useState(labels.length); + const deckRef = useRef(null); + const slidesRef = useRef([]); + const activeRef = useRef(0); + + const goTo = (index) => { + const deck = deckRef.current; + const slides = slidesRef.current; + if (!deck || !slides.length) return; + const target = slides[Math.max(0, Math.min(slides.length - 1, index))]; + if (!target) return; + const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + deck.scrollTo({ top: target.offsetTop, behavior: reduce ? 'instant' : 'smooth' }); + }; + + useEffect(() => { + const deck = document.querySelector('.cr-deck'); + if (!deck) return undefined; + deckRef.current = deck; + slidesRef.current = Array.from(deck.querySelectorAll('.cr-slide')); + setCount(slidesRef.current.length); + + // The deck sits under whatever chrome the docs theme renders above it, and + // every slide is exactly one viewport. Where a composition is taller than + // the room it has, scale it down rather than let it scroll or clip. + const fit = () => { + const navbar = document.getElementById('navbar'); + const top = navbar ? navbar.getBoundingClientRect().height : 0; + deck.style.setProperty('--cr-chrome', Math.round(top) + 'px'); + // One scale for the whole deck, not one per slide: a slide scaled on its + // own would sit at a different left edge from its neighbours, and the + // misalignment reads as a mistake when you arrow between them. + let scale = 1; + slidesRef.current.forEach((slide) => { + const inner = slide.querySelector('.cr-slide-inner'); + if (!inner) return; + const style = getComputedStyle(slide); + const roomY = slide.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom); + const roomX = slide.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); + // offsetHeight is layout height, so it does not compound with the scale + // already applied on the previous pass. + scale = Math.min(scale, roomY / Math.max(1, inner.offsetHeight), roomX / Math.max(1, inner.offsetWidth)); + }); + deck.style.setProperty('--cr-slide-scale', String(Math.max(0.5, scale))); + }; + + let frame = 0; + const update = () => { + const slides = slidesRef.current; + if (!slides.length) return; + let current = 0; + slides.forEach((slide, i) => { + const near = Math.abs(slide.offsetTop - deck.scrollTop); + if (near < Math.abs(slides[current].offsetTop - deck.scrollTop)) current = i; + }); + activeRef.current = current; + setActive(current); + }; + const schedule = () => { + cancelAnimationFrame(frame); + frame = requestAnimationFrame(() => { fit(); update(); }); + }; + + fit(); + update(); + deck.addEventListener('scroll', schedule, { passive: true }); + window.addEventListener('resize', schedule); + return () => { + cancelAnimationFrame(frame); + deck.removeEventListener('scroll', schedule); + window.removeEventListener('resize', schedule); + }; + }, []); + + useEffect(() => { + const onKey = (event) => { + // Leave typing, search and control activation alone. + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return; + if (event.target instanceof Element && event.target.closest('input, textarea, select, [contenteditable]')) return; + if (event.key === ' ' && event.target instanceof Element && event.target.closest('button, a')) return; + const steps = { ArrowDown: 1, PageDown: 1, ' ': 1, ArrowUp: -1, PageUp: -1 }; + if (event.key in steps) { + event.preventDefault(); + if (!event.repeat) goTo(activeRef.current + steps[event.key]); + } else if (event.key === 'Home') { + event.preventDefault(); + goTo(0); + } else if (event.key === 'End') { + event.preventDefault(); + goTo(slidesRef.current.length - 1); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, []); + + const names = count === labels.length ? labels : Array.from({ length: count }, (_, i) => 'Slide ' + (i + 1)); + + // Dots only. They are the deck's whole visible chrome, so unlike a decorative + // indicator they carry real labels and take focus. + return ( + + ); +}; diff --git a/docs/style.css b/docs/style.css index 2cac55d..be7e386 100644 --- a/docs/style.css +++ b/docs/style.css @@ -29,6 +29,44 @@ .cr-site .cr-caption { font-size:12px; line-height:1.7; color:var(--cr-muted); } .cr-flow-arrow { font-size:26px; color:var(--cr-muted); text-align:center; } .cr-section { padding:64px 0; border-bottom:1px solid var(--cr-line); scroll-margin-top:100px; } +/* Homepage deck: three slides, one viewport each, no resting point between. */ +.cr-deck { --cr-chrome:64px; height:calc(100dvh - var(--cr-chrome)); overflow-y:auto; overflow-x:hidden; scroll-snap-type:y mandatory; scroll-behavior:smooth; overscroll-behavior:contain; scrollbar-width:none; max-width:none; padding:0; } +.cr-deck::-webkit-scrollbar { display:none; } +.cr-slide { height:calc(100dvh - var(--cr-chrome)); overflow:hidden; scroll-snap-align:start; scroll-snap-stop:always; display:flex; align-items:center; padding:34px 32px 44px; } +.cr-slide-inner { width:100%; max-width:1120px; margin:0 auto; display:flex; flex-direction:column; justify-content:center; gap:clamp(26px,3.8vh,48px); transform:scale(var(--cr-slide-scale,1)); transform-origin:center center; } +.cr-deck .cr-hero { padding:0; max-width:830px; } +.cr-deck .cr-how,.cr-deck .cr-integration,.cr-deck .cr-commercial { padding:0; border:0; } +.cr-deck .cr-chain-cta { margin-top:0; padding-top:0; border-top:0; } +.cr-deck .cr-how-close { margin-top:0; } +.cr-deck .cr-pillars { margin-top:0; } +.cr-deck .cr-failure-chain .cr-section-heading { margin-bottom:clamp(22px,3.2vh,34px); } +.cr-deck .cr-integration { display:grid; grid-template-columns:1fr 1fr; gap:clamp(34px,4vw,64px); align-items:center; } +.cr-deck .cr-commercial { display:grid; grid-template-columns:1.3fr 1fr; gap:clamp(34px,4vw,64px); align-items:center; padding-top:clamp(20px,2.6vh,30px); border-top:1px solid var(--cr-line); } +.cr-deck .cr-footer { padding:0; border-top:1px solid var(--cr-line); padding-top:20px; } +.cr-deck .cr-slide-3 .cr-slide-inner { gap:clamp(18px,2.4vh,28px); } +.cr-deck .cr-hero h1 { font-size:clamp(40px,5.4vw,68px); margin-bottom:20px; } +.cr-deck h2+p { margin-top:14px; } +.cr-deck .cr-section-heading h2+p { margin-top:0; } +.cr-deck .cr-code pre { padding:16px 22px; line-height:1.6; } +.cr-deck .cr-code .cr-outcomes { padding:12px 22px; line-height:1.65; } +.cr-deck .cr-install { padding:12px 22px; } + +/* The deck's only visible control: position dots down the right edge. */ +.cr-deck-dots { position:fixed; right:24px; top:50%; transform:translateY(-50%); z-index:40; display:flex; flex-direction:column; gap:12px; } +.cr-dot-btn { width:9px; height:9px; padding:0; border:1px solid var(--cr-muted); border-radius:50%; background:transparent; opacity:.5; transition:opacity .15s,background .15s; } +.cr-dot-btn[aria-current=true] { background:var(--cr-accent); border-color:var(--cr-accent); opacity:1; } +@media (max-width:900px) { + .cr-deck .cr-integration,.cr-deck .cr-commercial { grid-template-columns:1fr; } +} +/* Below the deck's comfortable size, fall back to an ordinary page. */ +@media (max-width:800px),(max-height:620px) { + .cr-deck { height:auto; overflow:visible; scroll-snap-type:none; } + .cr-slide { height:auto; min-height:0; overflow:visible; display:block; padding:38px 24px; } + .cr-slide-inner { transform:none; } + .cr-slide+.cr-slide { border-top:1px solid var(--cr-line); } + .cr-slide-inner { gap:32px; } + .cr-deck-dots { display:none; } +} .cr-section h2+p { margin-top:18px; } .cr-failure { display:grid; grid-template-columns:1fr 1fr; gap:64px; align-items:center; } .cr-comparison { display:grid; gap:22px; } @@ -363,3 +401,7 @@ body:has(.cr-site) { --topbar-tabs-height:0px; } .cr-chain-steps li { padding:11px 10px; font-size:12.5px; } .cr-chain-steps li:not(:last-child)::after { content:"↓"; right:auto; left:50%; top:auto; bottom:-14px; transform:translateX(-50%); } } + +/* The wordmark carries the brand on every page; give it room to read. */ +#navbar img.nav-logo { height:40px; } +@media (max-width:600px) { #navbar img.nav-logo { height:32px; } } From f60cd6dd9149c852dd1d68d74b0ee934dab3287a Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 9 Sep 2026 06:22:42 +0530 Subject: [PATCH 6/6] Make the invented amount an extra zero, and put the sites in dollars "The agent invents a $50,000 payout" never said what the right number was, so the reader had to take the wrongness on trust. The chain now shows the divergence: the ticket says refund $500, the agent asks for $5,000. One extra zero is the error people recognise, and it is the believable one -- two extra zeros reads as contrived, and a chain nobody believes stops working. End cells show the loss rather than the gross, so the two rows compare like for like: "$4,500 too much" against "$0 wrongly paid". Currency: ctrlrun.dev and adopt.ctrlrun.dev are dollars throughout. The automotive use case keeps its own scale, $8,000 asked as $80,000, because a dealer incentive is not a consumer refund; the shape is the same. The README stays in euros, and that is not an oversight. It quotes `ctrlrun demo` byte for byte, the demo prints euros from src/ctrlrun/cli/demo.py, and docs/assets/demo.gif is a recording of that output. Prose in dollars above a transcript and a GIF in euros would make the README contradict its own evidence. Its story is aligned instead: 500 asked, 5,000 called, one extra zero. Under the demo's own policy 500 is autonomous and 5,000 is not, so the sentence says the extra zero meets a human rather than claiming a denial the demo would not produce. --- README.md | 19 ++++++++++--------- docs/index.mdx | 4 ++-- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 46f9ec6..0553c66 100644 --- a/README.md +++ b/README.md @@ -43,15 +43,16 @@ ## The refund nobody approved -An agent reads a ticket and decides the customer is owed €50,000. The tool is in its list, the -arguments are well-formed, and the model is completely confident. Nothing above the call -disagrees, because nothing above the call is a check: a tool being callable is not permission to -call it with those arguments. - -CTRLRun is that check. It reads the arguments about to leave your process and answers whether -this agent may send them. €50,000 is past the ceiling policy gives the agent, so the call never -leaves. Have a human approve €2,000 and then execute €5,000, and the approval authorises -nothing: it was bound to the action the human actually read. +A ticket asks for a €500 refund. The agent calls the refund tool with €5,000 — one extra zero. +The tool is in its list, the arguments are well-formed, and the model is completely confident. +Nothing above the call disagrees, because nothing above the call is a check: a tool being +callable is not permission to call it with those arguments. + +CTRLRun is that check. It reads the arguments about to leave your process and answers what may +happen to them. Under the demo's policy €500 is autonomous and €5,000 is not, so the extra zero +meets a human instead of the provider. Nor does an approval travel: the demo's second scenario +approves €2,000, executes €5,000 under that approval, and it authorises nothing, because it was +bound to the action the human actually read. That is the half people expect. The other half is the same agent making a *correct* €500 refund that commits at the provider while the reply is lost coming back. The agent sees an error and diff --git a/docs/index.mdx b/docs/index.mdx index c2615bf..a7a31d2 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -35,13 +35,13 @@ import { DeckNav } from "/snippets/deck-nav.jsx";
Without CTRLRun
    -
  1. The ticket says: check a refund
  2. The agent invents a $50,000 payout
  3. The tool is in its list
  4. The call goes through
  5. $50,000 gone
  6. +
  7. The ticket says: refund $500
  8. The agent asks for $5,000
  9. The tool is in its list
  10. The call goes through
  11. $4,500 too much
With CTRLRun
    -
  1. The ticket says: check a refund
  2. The agent invents a $50,000 payout
  3. Policy denies these arguments
  4. The call never leaves
  5. $0 paid. Nobody is asked.
  6. +
  7. The ticket says: refund $500
  8. The agent asks for $5,000
  9. Policy denies these arguments
  10. The call never leaves
  11. $0 wrongly paid