diff --git a/docs/SEO.md b/docs/SEO.md
index 766e6c4..e141e8b 100644
--- a/docs/SEO.md
+++ b/docs/SEO.md
@@ -150,5 +150,6 @@ not needed; the assistants that do read `llms.txt` get Mintlify's.
| Page | Search intent |
|---|---|
| `index` | Execution safety for AI agents |
+| `try` | try AI agent guardrails demo · what stops an agent action |
| `risk-check` | AI agent execution risk assessment |
| `protect-my-agent` | AI agent architecture review and integration help |
diff --git a/docs/docs.json b/docs/docs.json
index fc3d5b2..0fa16ed 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -37,6 +37,7 @@
"tab": "CTRLRun",
"pages": [
"index",
+ "try",
"risk-check",
"protect-my-agent"
]
diff --git a/docs/docs/demos/index.mdx b/docs/docs/demos/index.mdx
index 6ea40db..ab26478 100644
--- a/docs/docs/demos/index.mdx
+++ b/docs/docs/demos/index.mdx
@@ -36,3 +36,4 @@ on the first press and cache after that; a laptop offline can run the same thing
- [Why](/docs/why) — what these demos are demonstrating, in 700 words
- [Get started](/docs/get-started/quickstart) — the same guarantees around your own function
- [Cookbook](/docs/cookbook/index) — nineteen wirings you can copy
+- [The scenario demo](/try) — the same checks across 48 domains, drawn rather than run
diff --git a/docs/docs/try-it.mdx b/docs/docs/try-it.mdx
index 4c8e4f9..3fda2d0 100644
--- a/docs/docs/try-it.mdx
+++ b/docs/docs/try-it.mdx
@@ -8,6 +8,11 @@ One protected refund, under one policy, with a fake Stripe that commits before i
missing. Change the amount, lose the reply, approve one amount and execute another, and read
what refused you.
+
+ Coming from the [scenario demo](/try)? That page draws the decisions across 48 domains.
+ This one runs them: real Python, the released wheel, and every exception is the library's own.
+
+
@@ -264,5 +269,6 @@ this page failing, not the library.
## Next
- [Protect your first action](/docs/get-started/quickstart): the same thing, in your own process.
+- [The scenario demo](/try): the same checks, in your domain, with nothing running.
- [Outcomes and AMBIGUOUS](/docs/concepts/outcomes-and-ambiguous): what scenario 1 means.
- [Get started](/docs/get-started/install) · [Why](/docs/why).
diff --git a/docs/index.mdx b/docs/index.mdx
index 3d66a3e..1b4769e 100644
--- a/docs/index.mdx
+++ b/docs/index.mdx
@@ -35,14 +35,26 @@ 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.
+
- TRY THE EXECUTION BOUNDARY
Choose an action. See what CTRLRun stops. No login. No API key. No setup.
+ 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
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. 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 → executeapprove → wait for exact approvaldeny → do not executeunknown outcome → do not retry blindly
diff --git a/docs/medical-workbench.js b/docs/medical-workbench.js
index ed36653..d2afa0d 100644
--- a/docs/medical-workbench.js
+++ b/docs/medical-workbench.js
@@ -1,7 +1,7 @@
/* Medical evidence workflow. Synthetic fixtures; real CTRLRun release decisions. */
(function(){
"use strict";
-var TEMPLATE="\n \n
\n
\n
\n
EVIDENCE WORKBENCH / DEMO-001
From evidence to a reviewed brief Compound X \u00b7 Condition Y \u00b7 Fictional products, studies and findings
\n
Draft \u00b7 v1 Start a new case
\n
\n
\n 01 Retrieve \n 02 Reason \n 03 Cite \n 04 Validate \n 05 Review \n 06 Release \n \n
\n
Reason over evidence
\n
Download review brief Download evidence & audit record
\n
\n
\n \n
";
+var TEMPLATE="\n \n
\n
\n
\n
EVIDENCE WORKBENCH / DEMO-001
From evidence to a reviewed brief Compound X \u00b7 Condition Y \u00b7 Fictional products, studies and findings
\n
Draft \u00b7 v1 Start a new case
\n
\n
\n 01 Retrieve \n 02 Reason \n 03 Cite \n 04 Validate \n 05 Review \n 06 Release \n \n
\n
Reason over evidence
\n
Download review brief Download evidence & audit record
\n
\n
\n \n
";
var MODULE = [
"\"\"\"Synthetic evidence workbench: real policy, approval, effects, and receipts.",
"",
diff --git a/docs/snippets/scenario-explorer.jsx b/docs/snippets/scenario-explorer.jsx
index 910d4b2..41691d0 100644
--- a/docs/snippets/scenario-explorer.jsx
+++ b/docs/snippets/scenario-explorer.jsx
@@ -1,25 +1,59 @@
-export const ScenarioExplorer = () => {
+export const ScenarioExplorer = ({ standalone = false }) => {
const domains = [{"name": "Finance", "actions": ["Refund customer", "Transfer funds", "Issue payout", "Approve withdrawal"]}, {"name": "Banking", "actions": ["Transfer funds", "Freeze account", "Change credit limit", "Release transaction"]}, {"name": "Payments", "actions": ["Charge customer", "Refund payment", "Issue payout", "Capture payment", "Retry payment"]}, {"name": "FinTech", "actions": ["Move funds", "Approve withdrawal", "Suspend account", "Release payment"]}, {"name": "Insurance", "actions": ["Issue claim payment", "Change policy", "Approve claim", "Cancel policy", "Escalate fraud review"]}, {"name": "E-commerce", "actions": ["Refund order", "Cancel order", "Modify shipment", "Issue store credit", "Change delivery address"]}, {"name": "Retail", "actions": ["Modify order", "Issue refund", "Apply discount", "Cancel fulfillment", "Replace order"]}, {"name": "Customer Support", "actions": ["Refund customer", "Cancel subscription", "Apply account credit", "Modify account", "Reset access"]}, {"name": "SaaS", "actions": ["Delete account", "Suspend account", "Change plan", "Change permissions", "Grant feature access"]}, {"name": "Enterprise Software", "actions": ["Update ERP record", "Modify CRM record", "Approve workflow", "Change master data", "Create vendor record"]}, {"name": "Sales", "actions": ["Approve discount", "Send proposal", "Create order", "Change opportunity stage", "Approve pricing"]}, {"name": "CRM and Revenue Operations", "actions": ["Modify customer record", "Reassign account", "Change lifecycle status", "Trigger outbound communication", "Create renewal"]}, {"name": "Marketing", "actions": ["Launch campaign", "Pause campaign", "Change ad budget", "Send bulk communication", "Modify audience"]}, {"name": "Advertising", "actions": ["Increase spend", "Pause ad set", "Change targeting", "Publish creative", "Change bidding configuration"]}, {"name": "HR", "actions": ["Send offer", "Withdraw offer", "Start onboarding", "Start offboarding", "Change employee status"]}, {"name": "Payroll", "actions": ["Issue bonus", "Adjust payroll", "Approve reimbursement", "Reverse payment", "Change deduction"]}, {"name": "Legal", "actions": ["Send contract", "Trigger signature", "Submit filing", "Send legal notice", "Change contract status"]}, {"name": "Compliance", "actions": ["Approve exception", "Block transaction", "Escalate review", "Release restricted workflow", "Change risk status"]}, {"name": "Cybersecurity", "actions": ["Disable account", "Revoke session", "Block IP", "Isolate endpoint", "Rotate credential"]}, {"name": "Identity and Access", "actions": ["Grant role", "Revoke role", "Elevate privilege", "Create privileged account", "Disable service account"]}, {"name": "IT Operations", "actions": ["Reset password", "Provision access", "Revoke access", "Restart service", "Execute remediation"]}, {"name": "DevOps", "actions": ["Deploy production", "Roll back deployment", "Delete infrastructure", "Restart production service", "Modify production configuration"]}, {"name": "Cloud Infrastructure", "actions": ["Terminate instance", "Modify firewall", "Scale infrastructure", "Rotate secret", "Change cloud resource"]}, {"name": "Software Engineering", "actions": ["Merge pull request", "Publish release", "Delete branch", "Modify repository settings", "Rotate secret"]}, {"name": "CI/CD", "actions": ["Deploy release", "Publish package", "Roll back release", "Modify pipeline", "Change protected branch settings"]}, {"name": "Data Engineering", "actions": ["Delete dataset", "Modify production table", "Execute migration", "Trigger production pipeline", "Grant database access"]}, {"name": "Machine Learning", "actions": ["Deploy model", "Roll back model", "Change serving configuration", "Promote model to production", "Trigger retraining"]}, {"name": "Healthcare Operations", "actions": ["Schedule appointment", "Cancel appointment", "Send patient communication", "Submit authorization workflow", "Update administrative workflow"]}, {"name": "Pharmaceuticals and Life Sciences", "actions": ["Release controlled workflow", "Update operational record", "Trigger regulatory process", "Update trial workflow status", "Escalate adverse-event workflow"]}, {"name": "Manufacturing", "actions": ["Stop production workflow", "Release batch", "Change machine configuration", "Trigger maintenance", "Change production order"]}, {"name": "Supply Chain", "actions": ["Reroute shipment", "Release inventory", "Modify supplier order", "Change warehouse allocation", "Cancel shipment"]}, {"name": "Logistics", "actions": ["Dispatch vehicle", "Cancel delivery", "Change destination", "Reroute shipment", "Release shipment"]}, {"name": "Travel", "actions": ["Cancel booking", "Issue refund", "Modify reservation", "Upgrade booking", "Issue travel credit"]}, {"name": "Airlines", "actions": ["Rebook passenger", "Refund ticket", "Modify itinerary", "Change seat", "Release compensation"]}, {"name": "Hospitality", "actions": ["Cancel reservation", "Issue credit", "Change booking", "Modify guest record", "Upgrade reservation"]}, {"name": "Telecom", "actions": ["Activate service", "Disable SIM", "Change customer plan", "Apply account credit", "Modify network configuration"]}, {"name": "Energy and Utilities", "actions": ["Restore service", "Disconnect service", "Trigger billing adjustment", "Dispatch field service", "Change customer tariff"]}, {"name": "Automotive", "actions": ["Unlock vehicle", "Change fleet assignment", "Trigger roadside workflow", "Approve repair", "Modify vehicle configuration"]}, {"name": "Robotics", "actions": ["Start machine", "Stop machine", "Unlock physical access", "Move robot", "Trigger physical process"]}, {"name": "Real Estate", "actions": ["Send lease", "Change listing status", "Release deposit workflow", "Modify tenant record", "Submit offer"]}, {"name": "Education", "actions": ["Enroll student", "Withdraw student", "Change registration", "Release certificate", "Modify administrative access"]}, {"name": "Public Sector", "actions": ["Approve application", "Change case status", "Release payment workflow", "Update citizen record", "Trigger permit workflow"]}, {"name": "Procurement", "actions": ["Create purchase order", "Approve purchase", "Change supplier", "Release payment workflow", "Cancel purchase order"]}, {"name": "Accounting", "actions": ["Approve invoice", "Issue payment", "Reverse journal entry", "Modify vendor details", "Approve reimbursement"]}, {"name": "Marketplace", "actions": ["Release seller payout", "Suspend seller", "Refund buyer", "Cancel transaction", "Change seller permissions"]}, {"name": "Fraud Operations", "actions": ["Freeze transaction", "Block account", "Release transaction", "Disable payment method", "Escalate investigation"]}, {"name": "Communication", "actions": ["Send email", "Send SMS", "Send Slack message", "Publish notification", "Send bulk communication"]}, {"name": "General Business Operations", "actions": ["Approve request", "Update business record", "Trigger payment", "Delete record", "Grant access", "Trigger downstream automation"]}];
- const [domain, setDomain] = useState('Finance');
+ const quick = ['Payments', 'Customer Support', 'DevOps', 'Identity and Access', 'HR', 'Healthcare Operations'];
+ const situations = [
+ { value: 'approval', label: 'A person has to say yes first' },
+ { value: 'allowed', label: 'It is inside the agent’s limits' },
+ { value: 'blocked', label: 'The agent is not allowed to do it' },
+ { value: 'mismatch', label: 'The agent changed it after approval' },
+ { value: 'duplicate', label: 'It already happened once' },
+ { value: 'uncertain', label: 'The reply was lost' },
+ { value: 'reconcile', label: 'The agent retries without knowing' }
+ ];
+ const checks = [
+ { key: 'authority', label: 'Authority', asks: 'Is this agent entitled to act at all?' },
+ { key: 'policy', label: 'Policy', asks: 'For these exact arguments: allow, approve, or deny?' },
+ { key: 'approval', label: 'Approval binding', asks: 'Does a human decision match this exact action?' },
+ { key: 'effect', label: 'Effect reservation', asks: 'Could this action already have happened?' },
+ { key: 'outcome', label: 'Outcome', asks: 'Did the real system act, and do we know for certain?' }
+ ];
+ const [domain, setDomain] = useState('Payments');
const [actionIndex, setActionIndex] = useState(0);
const [query, setQuery] = useState('');
const [pickerOpen, setPickerOpen] = useState(false);
const [condition, setCondition] = useState('approval');
const [stage, setStage] = useState('initial');
+ const [trail, setTrail] = useState([]);
+ const [copied, setCopied] = useState(false);
const [activeOption, setActiveOption] = useState(0);
const searchRef = useRef(null);
const pickerRef = useRef(null);
const buttonRef = useRef(null);
- const selected = domains.find(item => item.name === domain);
+ const selected = domains.find(item => item.name === domain) || domains[0];
const filtered = domains.filter(item => item.name.toLowerCase().includes(query.toLowerCase()));
- const action = selected.actions[actionIndex];
+ const action = selected.actions[Math.min(actionIndex, selected.actions.length - 1)];
const money = /refund|funds|payout|payment|charge|bonus|reimbursement|credit|withdrawal|invoice|discount|spend|budget/i.test(action);
const physical = ['Robotics', 'Automotive', 'Manufacturing', 'Energy and Utilities'].includes(domain);
const original = money ? '$500' : 'one approved target';
const changed = money ? '$5,000' : 'all targets';
+ const reference = money ? 'txn_4821' : 'rec_4821';
+ const effectKey = action.toLowerCase().replace(/[^a-z0-9]+/g, '_') + ':' + reference;
const track = (name, extra = {}) => {
if (typeof window !== 'undefined') window.dispatchEvent(new CustomEvent('ctrlrun:conversion', { detail: { name, domain, action, ...extra } }));
};
+ useEffect(() => {
+ if (typeof window === 'undefined') return;
+ const params = new URLSearchParams(window.location.search);
+ const wanted = (params.get('domain') || '').toLowerCase();
+ const match = domains.find(item => item.name.toLowerCase() === wanted);
+ if (match) {
+ setDomain(match.name);
+ const index = match.actions.findIndex(item => item.toLowerCase() === (params.get('action') || '').toLowerCase());
+ setActionIndex(index >= 0 ? index : 0);
+ }
+ const situation = params.get('situation');
+ if (situations.some(item => item.value === situation)) setCondition(situation);
+ }, []);
useEffect(() => {
if (pickerOpen && searchRef.current) searchRef.current.focus();
}, [pickerOpen]);
@@ -33,28 +67,171 @@ export const ScenarioExplorer = () => {
document.addEventListener('pointerdown', outside);
return () => document.removeEventListener('pointerdown', outside);
}, []);
+ const restart = () => { setStage('initial'); setTrail([]); };
const chooseDomain = name => {
- setDomain(name); setActionIndex(0); setStage('initial'); setQuery(''); setPickerOpen(false); setActiveOption(0);
+ setDomain(name); setActionIndex(0); setQuery(''); setPickerOpen(false); setActiveOption(0); restart();
track('domain_selected', { domain: name, action: undefined });
if (buttonRef.current) buttonRef.current.focus();
};
+ const viaApproval = trail.some(item => item.indexOf('A person approved') === 0);
const states = {
- allowed: { title: 'Allowed', code: 'ALLOW', tone: 'green', reason: 'This action is within the autonomy you gave the agent.', 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.' },
- approval: { title: 'Human approval required', code: 'APPROVE', tone: 'amber', reason: 'This action is higher risk, so someone needs to approve it first.', rule: money ? 'Example rule: amounts above $250 require approval.' : 'Example rule: a person must approve this action and its exact target.' },
- blocked: { title: 'Blocked', code: 'DENY', tone: 'red', reason: 'The agent does not have permission to perform this action.', rule: 'Example rule: this action is outside the permissions assigned to this agent.' },
- mismatch: { title: 'Changed action blocked', code: 'ApprovalMismatch', tone: 'red', reason: 'Approval was for ' + original + ', not ' + changed + '.', rule: 'Changing the amount or target requires a new approval.' },
- duplicate: { title: 'Duplicate blocked', code: 'DuplicateEffect', tone: 'green', reason: 'This action appears to have already happened, so CTRLRun will not execute it again.', rule: 'The same business action is recognized across retries and workers.' },
- uncertain: { title: 'Outcome uncertain', code: 'AMBIGUOUS', tone: 'amber', reason: 'The external system may have completed the action. A missing response does not mean it failed.', rule: 'Confirm what happened at the provider before continuing.' },
- reconcile: { title: 'Reconciliation required', code: 'AMBIGUOUS', tone: 'amber', reason: 'CTRLRun will not retry until the original outcome is confirmed.', rule: 'Ask the external system, or have an operator check the original action.' },
- approved: { title: 'Exact action approved', code: 'ALLOW', tone: 'green', reason: 'A person approved ' + action.toLowerCase() + ' for ' + original + '. Only this exact request may continue.', rule: 'Approval does not grant permission to change the amount or target.' },
- completed: { title: 'Action completed', code: 'COMMITTED', tone: 'green', reason: 'The external system confirmed the action. Its outcome is recorded.', rule: 'A retry of the same business action must not execute it again.' }
+ allowed: {
+ title: 'Allowed, and reserved before the call',
+ code: 'ALLOW',
+ 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.',
+ 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' },
+ marks: ['pass', 'pass', 'skip', 'pass', 'wait'],
+ notes: ['The agent holds a grant covering this action.', 'Allowed at this size.', 'Not required at this size.', 'Reserved: ' + effectKey, 'Nothing has been executed yet.']
+ },
+ approval: {
+ title: 'A person has to answer first',
+ code: 'ApprovalRequired',
+ tone: 'amber',
+ reason: 'This action is higher risk than the agent may take on its own, so execution stops until a human answers.',
+ llm: 'I am confident this is right, so I will do it now.',
+ kernel: 'Confidence is not authorisation. Nothing is called until a person answers this exact request.',
+ rule: money ? 'Example rule: amounts above $250 require approval.' : 'Example rule: a person must approve this action and its exact target.',
+ sees: 'raise ApprovalRequired(request_id="apr_7f31…")',
+ doc: { href: '/docs/get-started/three-ways-in', label: 'Human in the loop' },
+ marks: ['pass', 'stop', 'wait', 'wait', 'wait'],
+ notes: ['The agent holds a grant covering this action.', 'Decision: approve. A request is created for a human.', 'Waiting for an answer.', 'Nothing is reserved.', 'Nothing is executed.']
+ },
+ blocked: {
+ title: 'Refused before anything ran',
+ code: 'ActionDenied',
+ tone: 'red',
+ reason: 'The agent does not have permission to perform this action, so nobody is asked and nothing is called.',
+ llm: 'The tool is in my list, so I am allowed to call it.',
+ kernel: 'A tool being callable is not permission. Policy denies this action, and no approval request is created.',
+ rule: 'Example rule: this action is outside the permissions assigned to this agent.',
+ sees: 'raise ActionDenied("policy: deny")',
+ doc: { href: '/docs/concepts/fail-closed', label: 'Fail closed' },
+ marks: ['pass', 'stop', 'wait', 'wait', 'wait'],
+ notes: ['The agent is a known principal.', 'Decision: deny.', 'No human is asked about an action policy refuses.', 'Nothing is reserved.', 'Nothing is executed.']
+ },
+ mismatch: {
+ title: 'The approval no longer matches',
+ code: 'ApprovalMismatch',
+ tone: 'red',
+ reason: 'A person approved ' + original + '. The agent is now presenting that same approval for ' + changed + '.',
+ llm: 'Same action, same approval, only the number changed.',
+ kernel: 'The approval was bound to the arguments the human read. Change one and it authorises nothing.',
+ rule: 'Changing the amount or the target requires a new approval.',
+ sees: 'raise ApprovalMismatch("approved hash ≠ requested hash")',
+ doc: { href: '/docs/concepts/approval-binding', label: 'Approval binding' },
+ marks: ['pass', 'pass', 'stop', 'wait', 'wait'],
+ notes: ['The agent holds a grant covering this action.', 'Decision: approve, and one was obtained.', 'The approved action hash does not match the requested one.', 'Nothing is reserved.', 'Nothing is executed.']
+ },
+ duplicate: {
+ title: 'The second attempt is refused',
+ code: 'DuplicateEffect',
+ tone: 'green',
+ reason: 'This is the same business action as one already committed, so CTRLRun will not let it run twice.',
+ llm: 'I did not see a confirmation, so I will run it again.',
+ kernel: 'The effect key is already committed. The retry is refused, and the original receipt is returned.',
+ rule: 'One business action, one effect key, across retries, workers and restarts.',
+ sees: 'raise DuplicateEffect("' + effectKey + ' already committed")',
+ doc: { href: '/docs/concepts/effect-keys', label: 'Effect keys' },
+ marks: ['pass', 'pass', 'skip', 'stop', 'wait'],
+ notes: ['The agent holds a grant covering this action.', 'Decision unchanged.', 'Not the check that stopped this.', effectKey + ' is already committed.', 'The first outcome stands. Nothing runs again.']
+ },
+ uncertain: {
+ title: 'The outcome is not known',
+ code: 'AMBIGUOUS',
+ tone: 'amber',
+ reason: 'The call left your process and the reply never came back. The real system may have done it.',
+ llm: 'The call raised an error, so it failed. I will try again.',
+ kernel: 'An error is not evidence of failure. Unless the executor proves nothing happened, the effect is AMBIGUOUS and stays reserved.',
+ rule: 'Only a definite “it did not happen” allows a retry. Everything else is unknown.',
+ sees: 'effect ' + effectKey + ' → AMBIGUOUS (held, not failed)',
+ doc: { href: '/docs/concepts/outcomes-and-ambiguous', label: 'Outcomes and AMBIGUOUS' },
+ marks: ['pass', 'pass', 'skip', 'pass', 'stop'],
+ notes: ['The agent holds a grant covering this action.', 'Decision: allow.', 'Not the check that stopped this.', 'Reserved before the call, which is why the retry can be caught.', 'No reply. The outcome is unknown, so it is recorded as unknown.']
+ },
+ reconcile: {
+ title: 'The blind retry is refused',
+ code: 'AmbiguousEffect',
+ tone: 'amber',
+ reason: 'The first attempt may already have succeeded. CTRLRun will not run it again until that is settled.',
+ llm: 'Retrying is harmless. It probably failed.',
+ kernel: 'Probably is not good enough for an action that moves something real. Confirm at the provider, or have a person resolve it.',
+ rule: 'Reconciliation retries the observation, never the effect.',
+ sees: 'raise AmbiguousEffect("outcome unknown; blind retry refused")',
+ doc: { href: '/docs/guides/resolve-an-ambiguous-effect', label: 'Resolve an ambiguous effect' },
+ marks: ['pass', 'pass', 'skip', 'stop', 'wait'],
+ notes: ['The agent holds a grant covering this action.', 'Decision unchanged.', 'Not the check that stopped this.', effectKey + ' is held in an unknown state.', 'Settled by asking the provider, or by an operator.']
+ },
+ approved: {
+ title: 'This exact action is approved',
+ code: 'ALLOW',
+ tone: 'green',
+ reason: 'A person approved ' + action.toLowerCase() + ' for ' + original + '. Only this exact request may continue.',
+ llm: 'I have an approval, so I can proceed.',
+ kernel: 'With these arguments, yes. The approval is single-use and bound to them.',
+ rule: 'Approval does not grant permission to change the amount or the target.',
+ sees: 'approval apr_7f31… matches → your function runs',
+ doc: { href: '/docs/concepts/approval-binding', label: 'Approval binding' },
+ marks: ['pass', 'pass', 'pass', 'pass', 'wait'],
+ notes: ['The agent holds a grant covering this action.', 'Decision: approve.', 'The approved hash matches the requested one.', 'Reserved: ' + effectKey, 'Not executed yet.']
+ },
+ completed: {
+ title: 'Done once, and recorded',
+ code: 'COMMITTED',
+ tone: 'green',
+ 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.',
+ 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'],
+ notes: ['The agent holds a grant covering this action.', 'Decision recorded.', viaApproval ? 'Approval consumed. It cannot be replayed.' : 'Not required at this size, so none was consumed.', effectKey + ' committed.', 'Confirmed by the real system.']
+ }
};
const stateKey = stage === 'initial' ? condition : stage;
const result = states[stateKey];
- const advance = next => { setStage(next); track('scenario_completed', { outcome: next }); };
+ const advance = (next, note) => { setStage(next); setTrail(trail.concat(note)); track('scenario_completed', { outcome: next }); };
+ const symbols = { pass: '✓', stop: '✕', wait: '·', skip: '–' };
+ const shareUrl = () => {
+ const origin = typeof window !== 'undefined' ? window.location.origin : 'https://ctrlrun.dev';
+ return origin + '/try?domain=' + encodeURIComponent(domain) + '&action=' + encodeURIComponent(action) + '&situation=' + encodeURIComponent(condition);
+ };
+ const copyLink = () => {
+ track('share_copied', { situation: condition });
+ if (typeof navigator === 'undefined' || !navigator.clipboard) { setCopied(false); return; }
+ navigator.clipboard.writeText(shareUrl()).then(() => {
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2500);
+ }, () => setCopied(false));
+ };
const reviewQuestion = money ? 'Letting agents move or refund money?' : domain === 'DevOps' || domain === 'Cloud Infrastructure' || domain === 'CI/CD' ? 'Letting agents change production infrastructure?' : domain === 'Customer Support' ? 'Letting agents refund, cancel, or change customer accounts?' : domain === 'HR' || domain === 'Payroll' ? 'Letting agents change employee records or payroll workflows?' : domain === 'SaaS' || domain === 'Identity and Access' ? 'Letting agents modify accounts, permissions, or customer data?' : 'Letting agents take action in ' + domain.toLowerCase() + '?';
return (
+
+
+
YOUR AGENT · THE LLM
+
Decides what to do
+
Reads the ticket, picks the tool, chooses the arguments, and decides to try again when a call errors. This is the part you built, and CTRLRun never touches it.
+
+
→
+
+
CTRLRUN · THIS TOOL
+
Decides whether it may run
+
Sees no prompt, no reasoning, no chat. It sees one thing: the action about to leave your process, with its exact arguments. It answers whether that may execute now, and records what happened.
+
+
→
+
+
THE REAL SYSTEM
+
Where it becomes real
+
Stripe, your database, the Kubernetes API, an email server. None of them can tell a first attempt from a retry, and a lost reply looks exactly like a failure.
+
+
+
CTRLRun is not a model, a prompt layer, or a guardrail on what the agent says . It is the check on what the agent does , in the last moment before the effect is real.
Choose your domain
@@ -74,25 +251,64 @@ export const ScenarioExplorer = () => {
}
- Choose an action { const index = Number(event.target.value); setActionIndex(index); setStage('initial'); track('use_case_selected', { action: selected.actions[index] }); }}>{selected.actions.map((item, index) => {item} )}
- Explore a situation { setCondition(event.target.value); setStage('initial'); track('scenario_completed', { outcome: event.target.value }); }}>Needs human approval Within the agent’s limits Outside its permissions Changed after approval Already completed Provider response lost Retry before confirmation
+ Choose an action { const index = Number(event.target.value); setActionIndex(index); restart(); track('use_case_selected', { action: selected.actions[index] }); }}>{selected.actions.map((item, index) => {item} )}
+ Choose what goes wrong { setCondition(event.target.value); restart(); track('scenario_completed', { outcome: event.target.value }); }}>{situations.map(item => {item.label} )}
+
+
+ 48 DOMAINS. START WITH ONE
+ {quick.map(name => chooseDomain(name)}>{name} )}
+ { setPickerOpen(true); setActiveOption(0); }}>Browse all 48 →
-
AGENT REQUESTS
{action} {stateKey === 'mismatch' ? changed : original}
{stateKey === 'mismatch' ? 'A person approved: ' + original : 'One consequential action. Checked before execution.'}
↓
CTRLRun_
+
+
THE AGENT ASKS TO
+
{action}
+
{stateKey === 'mismatch' ? changed : original}
+
{stateKey === 'mismatch' ? 'A person approved: ' + original : 'One consequential action, in ' + domain.toLowerCase() + '.'}
+
EFFECT KEY {effectKey}
+
↓
+
CTRLRun_
+
+ {checks.map((item, index) =>
+ {symbols[result.marks[index]]}
+ {item.label} : {result.marks[index] === 'stop' ? 'stopped here' : result.marks[index] === 'pass' ? 'passed' : result.marks[index] === 'skip' ? 'not applicable' : 'not reached'}. {item.asks} {result.notes[index]}
+ )}
+
+
-
✓ Action recognized
{result.title} {result.reason}
{result.rule}
{result.code}
+
✓ Action recognized
+
{result.title}
+
{result.reason}
+
+
WHAT THE LLM BELIEVED {result.llm}
+
WHAT CTRLRUN DID {result.kernel}
+
+
{result.rule}
+
WHAT YOUR CODE SEES {result.sees}
+
{result.code} {result.doc.label} →
+ {trail.length > 0 &&
WHAT HAPPENED SO FAR {trail.map((item, index) => {item} )} }
- {stateKey === 'approval' && advance('approved')}>Approve this exact action → }
- {(stateKey === 'allowed' || stateKey === 'approved') && advance('completed')}>Execute action → }
- {stateKey === 'approved' && advance('mismatch')}>Change {money ? 'to $5,000' : 'to all targets'} }
- {stateKey === 'completed' && advance('duplicate')}>Retry the same action → }
- {stateKey === 'uncertain' && advance('reconcile')}>Try again → }
- {stateKey === 'reconcile' && advance('completed')}>Simulate provider confirming success → }
- {stage !== 'initial' && setStage('initial')}>Reset scenario }
+ {stateKey === 'approval' && advance('approved', 'A person approved ' + action.toLowerCase() + ' for ' + original + '.')}>Approve this exact action → }
+ {(stateKey === 'allowed' || stateKey === 'approved') && advance('completed', 'The action executed once and the real system confirmed it.')}>Execute action → }
+ {stateKey === 'approved' && advance('mismatch', 'The agent changed the request to ' + changed + ' and presented the same approval.')}>Change {money ? 'to $5,000' : 'to all targets'} }
+ {stateKey === 'completed' && advance('duplicate', 'The agent retried the same business action.')}>Retry the same action → }
+ {stateKey === 'uncertain' && advance('reconcile', 'The agent tried again without confirming the first outcome.')}>Try again → }
+ {stateKey === 'reconcile' && advance('completed', 'The provider was asked. The first attempt had succeeded, so the effect is resolved, not repeated.')}>Ask the provider what happened → }
+ {stage !== 'initial' && Reset scenario }
-
Interactive simulation · Example rules, not industry defaults. No real actions are taken.{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.' : ''}
+
+
+ SHARE THIS SCENARIO
+ event.target.select()} />
+
+
+
+
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.' : ''}
);
diff --git a/docs/style.css b/docs/style.css
index b1fd611..51097af 100644
--- a/docs/style.css
+++ b/docs/style.css
@@ -271,3 +271,74 @@ body:has(.cr-site) { --topbar-tabs-height:0px; }
@media(pointer:coarse){#cr-medical-workbench button{min-height:44px}#cr-medical-workbench .cr-label{min-height:44px}}
#cr-medical-workbench [hidden]{display:none!important}
+
+/* Scenario explorer: the three lanes, the check ladder, the share row. */
+.cr-lanes { display:grid; grid-template-columns:1fr 26px 1.25fr 26px 1fr; gap:12px; align-items:stretch; padding:22px; background:var(--cr-panel); border-bottom:1px solid var(--cr-line); border-radius:8px 8px 0 0; }
+.cr-lane { display:flex; flex-direction:column; gap:6px; padding:14px 16px; background:var(--cr-paper); border:1px solid var(--cr-line); border-radius:6px; }
+.cr-lane strong { font-size:16px; font-weight:550; letter-spacing:-.02em; color:var(--cr-ink); }
+.cr-site .cr-lane p { font-size:12px; line-height:1.65; }
+.cr-lane-control { border-color:var(--cr-accent); border-left-width:2px; }
+.cr-lanes .cr-flow-arrow { align-self:center; font-size:20px; }
+.cr-site .cr-lane-note { padding:14px 22px; font-size:12.5px; line-height:1.7; border-bottom:1px solid var(--cr-line); color:var(--cr-muted); }
+.cr-lane-note em { color:var(--cr-ink); font-style:normal; font-weight:600; }
+.cr-chips { display:flex; flex-wrap:wrap; align-items:center; gap:8px; padding:0 22px 18px; border-bottom:1px solid var(--cr-line); }
+.cr-chips .cr-step { width:100%; margin-bottom:2px; }
+.cr-site .cr-chip { min-height:34px; padding:6px 13px; border:1px solid var(--cr-line); border-radius:999px; background:transparent; color:var(--cr-ink); font-size:12px; font-weight:550; }
+.cr-site .cr-chip:hover { border-color:var(--cr-accent); }
+.cr-site .cr-chip-on { background:var(--cr-ink); color:var(--cr-paper); border-color:var(--cr-ink); }
+.cr-site .cr-chip-more { border-style:dashed; color:var(--cr-muted); }
+.cr-effect-key { display:flex; flex-direction:column; gap:4px; margin-top:16px; }
+.cr-effect-key code { font-size:12px; color:var(--cr-accent); }
+.cr-checks { list-style:none; margin:22px 0 0; padding:18px 0 0; border-top:1px solid var(--cr-line); display:grid; gap:13px; }
+.cr-checks li { display:grid; grid-template-columns:18px 1fr; gap:10px; align-items:start; }
+.cr-check-mark { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:12px; line-height:1.5; color:var(--cr-muted); }
+.cr-checks strong { display:block; font-size:12.5px; font-weight:550; color:var(--cr-ink); }
+.cr-checks em { display:block; font-style:normal; font-size:11.5px; line-height:1.6; color:var(--cr-muted); }
+.cr-checks li>span>span:not(.cr-sr-only) { display:block; margin-top:3px; font-size:11.5px; line-height:1.6; color:var(--cr-ink); }
+.cr-check-pass .cr-check-mark { color:#3c7041; }
+.cr-check-stop .cr-check-mark { color:#a13d32; font-weight:700; }
+.cr-check-stop strong { color:#a13d32; }
+.dark .cr-check-pass .cr-check-mark { color:#9ec793; }
+.dark .cr-check-stop .cr-check-mark,.dark .cr-check-stop strong { color:#f5a198; }
+.cr-check-wait,.cr-check-skip { opacity:.5; }
+.cr-split { display:grid; grid-template-columns:1fr 1fr; gap:20px; margin-top:20px; padding-top:18px; border-top:1px solid var(--cr-line); }
+.cr-split>div { padding-left:14px; border-left:2px solid var(--cr-line); }
+.cr-split .cr-safe { border-color:var(--cr-accent); }
+.cr-site .cr-split p { margin-top:6px; font-size:12.5px; line-height:1.65; }
+.cr-site .cr-split .cr-safe p { color:var(--cr-ink); }
+.cr-sees { display:flex; flex-direction:column; gap:6px; margin-top:18px; }
+.cr-sees code { display:block; padding:10px 12px; border:1px solid var(--cr-line); border-radius:4px; background:var(--cr-panel); color:var(--cr-ink); font-size:11.5px; line-height:1.6; overflow-x:auto; }
+.cr-result .cr-code-label { display:flex; flex-wrap:wrap; align-items:center; gap:6px 16px; margin-top:14px; }
+.cr-code-label>span { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:10px; color:var(--cr-muted); }
+.cr-code-label a { text-decoration:underline; text-underline-offset:3px; font-size:12px; }
+.cr-trail { margin:18px 0 0; padding:14px 0 0; border-top:1px dashed var(--cr-line); }
+.cr-trail ol { margin:8px 0 0; padding-left:20px; display:grid; gap:6px; font-size:11.5px; line-height:1.6; color:var(--cr-muted); }
+.cr-trail li { list-style:decimal; }
+.cr-trail li::marker { color:var(--cr-accent); font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:10px; }
+.cr-demo-share { display:flex; flex-wrap:wrap; justify-content:space-between; align-items:flex-end; gap:14px 20px; padding:16px 22px; border-top:1px solid var(--cr-line); }
+.cr-demo-share>div:first-child { flex:1 1 340px; display:flex; flex-direction:column; gap:6px; }
+.cr-site .cr-demo-share input { font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:11.5px; min-height:40px; }
+.cr-share-actions { display:flex; flex-wrap:wrap; align-items:center; gap:16px; }
+.cr-share-actions .cr-button { min-height:40px; font-size:12px; }
+.cr-demo-note a { text-decoration:underline; text-underline-offset:3px; }
+.cr-how { border-bottom:1px solid var(--cr-line); }
+.cr-how-steps { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:26px 44px; margin-top:30px; }
+.cr-how-steps>div { padding-top:16px; border-top:1px solid var(--cr-line); }
+.cr-how-steps h3 { font-size:17px; margin:8px 0 8px; }
+.cr-site .cr-how-steps p { font-size:13.5px; line-height:1.7; }
+.cr-how-close { display:flex; flex-wrap:wrap; align-items:center; gap:12px 26px; margin-top:34px; padding-top:22px; border-top:1px solid var(--cr-line); }
+.cr-site .cr-how-close p { flex:1 1 420px; font-size:13.5px; }
+.cr-how-close a { text-decoration:underline; text-underline-offset:4px; }
+@media (max-width:900px) {
+ .cr-lanes { grid-template-columns:1fr; }
+ .cr-lanes .cr-flow-arrow { transform:rotate(90deg); line-height:1; }
+ .cr-how-steps { grid-template-columns:1fr; gap:22px; }
+}
+@media (max-width:600px) {
+ .cr-lanes { padding:16px; }
+ .cr-chips { padding:0 18px 16px; }
+ .cr-split { grid-template-columns:1fr; gap:16px; }
+ .cr-demo-share { padding:16px 18px; }
+ .cr-checks { gap:11px; }
+}
+.cr-site .cr-demo-note { padding:14px 22px; font-size:11.5px; line-height:1.7; }
diff --git a/docs/try.mdx b/docs/try.mdx
new file mode 100644
index 0000000..e6d38a6
--- /dev/null
+++ b/docs/try.mdx
@@ -0,0 +1,47 @@
+---
+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"
+mode: "custom"
+---
+
+import { ScenarioExplorer } from "/snippets/scenario-explorer.jsx";
+
+
+
+ ← CTRLRun
+ 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.
+ 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.
+
+
+
+
+
+ THIS PAGE IS AN ILLUSTRATION
Then go and break the real thing. Nothing on this page runs CTRLRun. It draws the decisions so you can see where they land in a domain you recognise. One page further, real Python and the released wheel load into your tab, and every refusal you read there is the library's own.
+
+
HERE An illustration of the checks, with example rules that are not industry defaults. No code runs, no request is sent.
+
ONE PAGE FURTHER Real Python, the released ctrlrun wheel, in your browser. Change a refund after approving it and read the exception the library raises. Run it in your browser →
+
+
+
+
+ WHERE TO GO NEXT
From the demo to your own code. Apache-2.0 · Runs in your environment
+
+
+
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.
+
+
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 →
+
+
+
Give agents autonomy.Keep control of their actions.
+
diff --git a/tests/test_docs_audit.py b/tests/test_docs_audit.py
index 1682b33..4c3cbd5 100644
--- a/tests/test_docs_audit.py
+++ b/tests/test_docs_audit.py
@@ -383,6 +383,21 @@ def test_a_root_relative_docs_path_resolves_under_docs(tmp_path, monkeypatch):
assert [b.target for b in broken] == ["/concepts/missing"]
+def test_a_query_string_is_not_part_of_the_page_path(tmp_path, monkeypatch):
+ """`/try?situation=uncertain` is the `/try` page. The checker looked for a file named after
+ the whole string and called a working link broken -- and the strip must not swallow a
+ genuinely missing page that happens to carry a query."""
+ monkeypatch.setattr(links, "REPO_ROOT", tmp_path)
+ (tmp_path / "docs").mkdir()
+ (tmp_path / "docs" / "try.mdx").write_text("---\ntitle: Try\n---\n")
+ page = tmp_path / "docs" / "index.mdx"
+ page.write_text("[a](/try?situation=uncertain) [b](/nope?situation=uncertain)\n")
+
+ broken = links.check_text(page.read_text(), page)
+
+ assert [b.target for b in broken] == ["/nope?situation=uncertain"]
+
+
@checkout_only
def test_a_link_to_a_page_the_ia_plans_is_planned_not_broken(tmp_path, monkeypatch):
monkeypatch.setattr(links, "REPO_ROOT", tmp_path)
diff --git a/tools/docs_audit/links.py b/tools/docs_audit/links.py
index 492cf5d..4dd6c5f 100644
--- a/tools/docs_audit/links.py
+++ b/tools/docs_audit/links.py
@@ -119,6 +119,10 @@ def _resolve(target: str, source: Path) -> tuple[Path | None, str | None] | None
return None
path_part, _, anchor = target.partition("#")
anchor = unquote(anchor) or None
+ # A query string is not part of the path. `/try?situation=uncertain` is the same page as
+ # `/try`, and the site serves it that way; a checker that kept the query looked for a file
+ # named after the whole string and reported a working link as broken.
+ path_part = path_part.partition("?")[0]
if path_part.startswith(("http://", "https://")):
matched = _GITHUB.match(path_part)
if matched is None:
@@ -157,7 +161,7 @@ def check_text(text: str, source: Path) -> list[Broken]:
if path is None:
continue
if not path.exists():
- site_path = target.partition("#")[0].lstrip("/")
+ site_path = target.partition("#")[0].partition("?")[0].lstrip("/")
if target.startswith("/") and site_path in planned_pages():
planned.append(
Broken(name, number, target, "planned in docs/IA.md, not written yet")