From a023f297339bb9a354bbd685528b648c202be0ab Mon Sep 17 00:00:00 2001 From: arpan Date: Tue, 8 Sep 2026 17:30:23 +0530 Subject: [PATCH 1/2] Rebuild the execution boundary demo and give it its own page The demo said what CTRLRun decided and never said what it was deciding instead of. A reader arriving from an agent framework could not tell which part of the picture was their model and which part was this library, so the whole thing read as one more layer of the same kind. Three changes fix that. The explorer opens with three lanes -- the LLM decides what to do, CTRLRun decides whether it may run, the real system is where it becomes real -- and every outcome now pairs what the model believed against what the kernel did. Under the request sits the ladder of checks in order, marking which one stopped the action, so a refusal is attributable to a check rather than appearing as a verdict from nowhere. Each outcome carries the line the caller's own code would see and a link to the concept page behind it. Every scenario is now addressable. `?domain=&action=&situation=` is read on mount, the share row hands over the link, and `/try` hosts the same explorer as a page of its own -- the home page section stays where it is. The lost response section links into the case it describes rather than only naming it. A new home page section says where CTRLRun sits in plain English and hands off to the architecture for the internals, and the medical affairs build map opens rather than hiding behind a summary. The link checker was reporting `/try?situation=uncertain` as a missing page: a query string is not part of a path, and the strip is tested in both directions so a genuinely missing page carrying a query still fails. --- docs/SEO.md | 1 + docs/docs.json | 1 + docs/docs/demos/index.mdx | 1 + docs/docs/try-it.mdx | 6 + docs/index.mdx | 16 +- docs/medical-workbench.js | 2 +- docs/snippets/scenario-explorer.jsx | 270 +++++++++++++++++++++++++--- docs/style.css | 71 ++++++++ docs/try.mdx | 47 +++++ tests/test_docs_audit.py | 15 ++ tools/docs_audit/links.py | 6 +- 11 files changed, 405 insertions(+), 31 deletions(-) create mode 100644 docs/try.mdx 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. + +
+
+ 48 DOMAINS. START WITH ONE + {quick.map(name => )} +
-

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) =>
  1. + + {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]} +
  2. )} +
+
-

✓ 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) =>
  1. {item}
  2. )}
}
- {stateKey === 'approval' && } - {(stateKey === 'allowed' || stateKey === 'approved') && } - {stateKey === 'approved' && } - {stateKey === 'completed' && } - {stateKey === 'uncertain' && } - {stateKey === 'reconcile' && } - {stage !== 'initial' && } + {stateKey === 'approval' && } + {(stateKey === 'allowed' || stateKey === 'approved') && } + {stateKey === 'approved' && } + {stateKey === 'completed' && } + {stateKey === 'uncertain' && } + {stateKey === 'reconcile' && } + {stage !== 'initial' && }
-

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()} /> +
+
+ + {!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.' : ''}

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

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

+
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.
CTRLRun ↗Read the docs →GitHub ↗
+
diff --git a/tests/test_docs_audit.py b/tests/test_docs_audit.py index 1682b33..3e8e62b 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") From 6543faafd96a0a32f46d5cdd95629c1f62deed2d Mon Sep 17 00:00:00 2001 From: arpan Date: Tue, 8 Sep 2026 17:43:58 +0530 Subject: [PATCH 2/2] Format the query-string link test to ruff's quote style --- tests/test_docs_audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_docs_audit.py b/tests/test_docs_audit.py index 3e8e62b..4c3cbd5 100644 --- a/tests/test_docs_audit.py +++ b/tests/test_docs_audit.py @@ -391,7 +391,7 @@ def test_a_query_string_is_not_part_of_the_page_path(tmp_path, monkeypatch): (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') + page.write_text("[a](/try?situation=uncertain) [b](/nope?situation=uncertain)\n") broken = links.check_text(page.read_text(), page)