diff --git a/README.md b/README.md index fd18583..0553c66 100644 --- a/README.md +++ b/README.md @@ -41,18 +41,25 @@ 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 + +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 +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 1b4769e..a7a31d2 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -3,67 +3,102 @@ 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/" --- -import { ScenarioExplorer } from "/snippets/scenario-explorer.jsx"; +import { DeckNav } from "/snippets/deck-nav.jsx"; -
-
+
+
+
+

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.

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

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.

+
+
+
+ Without CTRLRun +
    +
  1. The ticket says: refund $500
  2. The agent asks for $5,000
  3. The tool is in its list
  4. The call goes through
  5. $4,500 too much
  6. +
+
+
+ With CTRLRun +
    +
  1. The ticket says: refund $500
  2. The agent asks for $5,000
  3. Policy denies these arguments
  4. The call never leaves
  5. $0 wrongly paid
  6. +
+
+
+
+
-
-

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 →

-
+
+
+
+

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 ↗ +
+
-
-

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 ↗

- +
+
+

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 →
+
+
-
-

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.

+
+
+
+

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 →

-

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

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.

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.

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.

+
Give agents autonomy.
Keep control of their actions.
Try CTRLRun ↗Read the docs →GitHub ↗Contact ↗
+
-
Give agents autonomy.
Keep control of their actions.
Try CTRLRun ↗Read the docs →GitHub ↗
+ 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/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/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/style.css b/docs/style.css index 51097af..be7e386 100644 --- a/docs/style.css +++ b/docs/style.css @@ -27,16 +27,46 @@ .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; } +/* 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; } @@ -46,6 +76,7 @@ .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-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; } @@ -145,9 +176,6 @@ @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-demo-toolbar { grid-template-columns:1fr 1fr; } .cr-demo-toolbar>.cr-field:last-child { grid-column:1/-1; } @@ -158,12 +186,7 @@ .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-section { padding:40px 0; } .cr-demo-toolbar { grid-template-columns:1fr; padding:18px; gap:14px; } @@ -342,3 +365,43 @@ 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%); } +} + +/* 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; } } 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")