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..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")