This page does not exist.
The address may be wrong, or the page has moved to the documentation.
diff --git a/docs.json b/docs.json index 2a3554c..f15ffe2 100644 --- a/docs.json +++ b/docs.json @@ -25,18 +25,13 @@ "label": "Docs" }, { - "href": "/#pro-and-enterprise", - "label": "Free, Pro, Enterprise" - }, - { - "href": "https://github.com/CTRLRun/ctrlrun", - "label": "GitHub" + "href": "/#verify", + "label": "Verify" } ], "primary": { - "type": "button", - "label": "Protect my agent", - "href": "/protect-my-agent" + "type": "github", + "href": "https://github.com/CTRLRun/ctrlrun" } }, "navigation": { diff --git a/fonts/GeistMono-400.woff2 b/fonts/GeistMono-400.woff2 new file mode 100644 index 0000000..1ee228f Binary files /dev/null and b/fonts/GeistMono-400.woff2 differ diff --git a/fonts/Satoshi-400.woff2 b/fonts/Satoshi-400.woff2 new file mode 100644 index 0000000..81c40ab Binary files /dev/null and b/fonts/Satoshi-400.woff2 differ diff --git a/fonts/Satoshi-500.woff2 b/fonts/Satoshi-500.woff2 new file mode 100644 index 0000000..ffd0ac9 Binary files /dev/null and b/fonts/Satoshi-500.woff2 differ diff --git a/fonts/Satoshi-700.woff2 b/fonts/Satoshi-700.woff2 new file mode 100644 index 0000000..0a8db7a Binary files /dev/null and b/fonts/Satoshi-700.woff2 differ diff --git a/images/demo.gif b/images/demo.gif new file mode 100644 index 0000000..f3e8f9b Binary files /dev/null and b/images/demo.gif differ diff --git a/images/og-home.png b/images/og-home.png new file mode 100644 index 0000000..2f53480 Binary files /dev/null and b/images/og-home.png differ diff --git a/index.mdx b/index.mdx index d6d67e3..11aa0f9 100644 --- a/index.mdx +++ b/index.mdx @@ -9,98 +9,216 @@ canonical: "https://ctrlrun.dev/" "og:url": "https://ctrlrun.dev/" --- -import { HomeSlides } from "/snippets/home-slides.jsx"; import { HowDiagram } from "/snippets/how-diagram.jsx"; -import { CommercialTiers } from "/snippets/commercial-tiers.jsx"; - -
THE AGENT IS PROBABILISTIC. THE ACTION IS NOT.
-Every action is checked against your rules before it runs. Allowed actions go through. Sensitive ones wait for a person. Forbidden ones are blocked.
-Open source · one line, any action{'@ctrlrun.protect("your.action")'}
ONE EXAMPLE / THE MODEL HALLUCINATES AN AMOUNT
-The ticket saysrefund $500
-The agent asks for$5,000
-Every action is checked against your rules before it runs. Allowed actions go through. Sensitive ones wait for a person. Forbidden ones are blocked.
+Execution safety for AI agents. A Python library that sits between the decision to act and the call that acts. A consequential action happens at most once, exactly as approved, and leaves a receipt. When the outcome is unknown, CTRLRun says so instead of guessing.
+Runs in production on a single file, or on Postgres across hosts. Apache-2.0.
+ -WhatsApp, Slack, Teams, Claude Code, Cursor, Codex, ChatGPT, OpenAI Agents. Any AI agent you have. If it acts through your systems, it is checked.
- How it works +pip install ctrlrun && ctrlrun demo
+ CTRLRun is the kernel. What we are building on it is almost ready. Leave your email and you will hear first, once, when it lands.
HOW CTRLRUN WORKS
-CTRLRun stops an agent from taking an action your rules do not allow. Every action that leaves your agents, tools and workflows is normalized into one action, decided against your policy, held for a person where you require it, reserved so it cannot run twice, executed, resolved and recorded. An action with no rule is blocked, arguments changed after sign-off void the approval, and an outcome nobody knows is never retried on a guess.
+The model guesses. CTRLRun does not. The ticket says refund €500. The agent asks for €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.
+Nothing checks the amount. The call goes through. €4,500 too much.
Your rule checks the amount. The call never leaves. €0 wrongly paid.
CTRLRun is that check. It reads the arguments about to leave your process and answers what may happen to them: let it run, ask a human, or stop it cold. Four rules do the work, and each one is a test in this repository before it is a sentence here.
+The animation above is this section, recorded against the real library: one policy file, two short programs, four commands, nothing staged.
+ +One file, ctrlrun.yaml. Amounts are integer minor units, so 50000 is €500. Both ends of every band are bound, because an upper bound alone lets a negative amount through, and a refund of a negative amount is a charge. Anything not listed is denied; there is no default-allow.
The decorator names the action, the effect key names the consequence it has in the world, and the context names who is acting. stripe here is a stand-in that records calls instead of making them.
The €500 refund runs on its own. The €5,000 one stops and names the request a human answers.
+A human answers from the shell. The grant names the hash of the exact action the human read, and when it lapses.
+ +```console +$ ctrlrun approve "$(cat request_id.txt)" +granted apr_63e80076f2cccfee52b17491a4b2e125 for sha256:22ec1c39…6af752 +expires 2026-09-13T20:10:11.367Z +``` + +The agent presents it, then tries to spend it on something else. The first call is exactly what the human approved, and it runs. The second is the same approval with one digit changed, and it matches nothing.
+ +```console +€5,000 with the approval -> succeeded +€9,000 on that same approval -> refused +calls that reached the provider: 1 (the €9,000 never left) +``` + +Every attempt, refusals included, left a receipt, and ctrlrun receipts lists them. That is the whole integration: a policy file, a decorator, a context, and with_approval to present a grant. Money is the example, not the scope: the same policy language reads role_in or replicas_lte as easily as amount_lte.
You probably do not need an adapter. @protect covers anything running in this process: a raw model call, a LangChain tool, a hand-rolled loop, a cron job. The gateway covers anything that reaches its tools over MCP, in any language.
| You have | Use | Needs |
|---|---|---|
| Python in this process | the @protect decorator, shown above | nothing beyond pip install ctrlrun |
| Tools behind an MCP server, in any language | the gateway: pip install "ctrlrun[gateway]" | one command, no change to agent or server code |
| A framework with its own approval interrupt | an adapter | the framework to have a human-in-the-loop primitive |
It works with agents you can and can't modify. WhatsApp, Slack and Teams bots, ChatGPT, Cursor, Codex, OpenAI Agents: any AI agent you have. CTRLRun checks the action, not the agent, so if the agent acts through a tool server or an API you run, the action is checked, and the agent is not rebuilt, redeployed or told. Agents you can't modify says where the boundary goes for each kind.
+Every protected call, whichever way it arrives, goes through the same seven steps. Only then does it reach your systems.
+Interactive walkthrough: follow one agent action through every check →
- - -FREE, PRO, ENTERPRISE
-Free. Run it yourself.
-The boundary itself, at no cost. No account, no card, no call with us. Every rule that decides is code you can read, and every action leaves a receipt you keep. Free to use and free to change, under the Apache-2.0 licence.
-Who does the work Your team, on your machines.
- Install it -Integrate, analyze, protect. One dashboard.
-Connect your agents and workflows and see what each connection covers. Search every action, decision and outcome across your agents. Manage policies and approvals from one place, test a rule in observe mode, then turn enforcement on. Built on the open-source foundation, run by us.
-Who does the work Your team, on our dashboard.
-The same product, shaped to your company.
-Everything in Pro, with CTRLRun engineers who design, integrate and maintain the controls your business needs: custom policies and approval chains, connectors to your internal systems, your deployment options, the reports you ask for. Scoped to your requirements, delivered with your team.
-Who does the work Our engineers, with your team.
-State lives in SQLite by default, a file with no server and no ops, and the reservation holds across processes rather than merely across threads. Point it at Postgres when more than one host writes: one URL, the same guarantees graded by the same suite. Prove it in your own setup with ctrlrun verify, which runs the kernel's own failure scenarios against your policy in a scratch store. It reaches no network.
What verify provesThreat modelEvery sentence mapped to its test
+docs.ctrlrun.dev is the documentation: concepts, guides, a cookbook, the full reference, and a browser demo that runs with no install.
+Issues and pull requests are welcome: CONTRIBUTING.md and CODE_OF_CONDUCT.md have the working agreement, and SECURITY.md is how to report a vulnerability. Every claim on this page has a test behind it, so a change to the prose usually means a change to the suite. CHANGELOG.md and the roadmap say where it is going. Releases carry PyPI provenance attestations from GitHub Actions.
+ +Thanks. One email when it lands, nothing else.
; + return ( + + ); +}; diff --git a/style.css b/style.css index 63266fb..3956ff9 100644 --- a/style.css +++ b/style.css @@ -779,3 +779,151 @@ body:has(.cr-subpage) #search-bar-entry-mobile,body:has(.cr-subpage) #assistant- .cr-explorer { gap:44px; } .cr-dg-scroll { padding:18px 0 4px; } } + +/* --------------------------------------------------------------------------------------------- + ct-home: the ctrlrun.dev project page, rebuilt 2026-09-15 from internal/design/DESIGNS.md. + New namespace on purpose: the cr-* rules above still style the other custom pages until they + are redone. Tokens, type scale, spacing, radius and motion are the design file's. Monochrome. + --------------------------------------------------------------------------------------------- */ +@font-face { font-family: "Satoshi"; font-weight: 400; font-style: normal; font-display: swap; src: url("/fonts/Satoshi-400.woff2") format("woff2"), url("https://cdn.fontshare.com/wf/TTX2Z3BF3P6Y5BQT3IV2VNOK6FL22KUT/7QYRJOI3JIMYHGY6CH7SOIFRQLZOLNJ6/KFIAZD4RUMEZIYV6FQ3T3GP5PDBDB6JY.woff2") format("woff2"); } +@font-face { font-family: "Satoshi"; font-weight: 500; font-style: normal; font-display: swap; src: url("/fonts/Satoshi-500.woff2") format("woff2"); } +@font-face { font-family: "Satoshi"; font-weight: 700; font-style: normal; font-display: swap; src: url("/fonts/Satoshi-700.woff2") format("woff2"); } +@font-face { font-family: "Geist Mono"; font-weight: 400; font-style: normal; font-display: swap; src: url("/fonts/GeistMono-400.woff2") format("woff2"); } + +.ct-home { + --ct-bg:#F8F8F7; --ct-surface:#FFFFFF; --ct-surface-2:#F1F1EF; --ct-border:#E4E4E0; --ct-border-strong:#C9C9C3; + --ct-text:#14161A; --ct-text-2:#5C6068; --ct-text-3:#8B8F96; + --ct-accent:#B8730A; --ct-accent-hover:#8A5606; --ct-accent-soft:#FBF1DE; --ct-on-accent:#FFFFFF; --ct-button:#14161A; --ct-button-hover:#2A2D33; --ct-on-button:#FFFFFF; + --ct-danger:#D63A3A; --ct-success:#1F8A4C; + --ct-sans:"Satoshi",-apple-system,"Segoe UI",sans-serif; --ct-mono:"Geist Mono",ui-monospace,SFMono-Regular,Menlo,monospace; + --ct-ease-in:cubic-bezier(0.2,0,0,1); --ct-ease-out:cubic-bezier(0.4,0,1,1); + background:var(--ct-bg); color:var(--ct-text); font-family:var(--ct-sans); font-size:15px; line-height:22px; + -webkit-font-smoothing:antialiased; margin:0 calc(50% - 50vw); padding:0; width:100vw; overflow-x:clip; +} +.dark .ct-home { + --ct-bg:#111214; --ct-surface:#18191C; --ct-surface-2:#1F2024; --ct-border:#2A2B30; --ct-border-strong:#3A3B42; + --ct-text:#ECECEA; --ct-text-2:#A2A5AD; --ct-text-3:#6F737B; + --ct-accent:#F5A623; --ct-accent-hover:#FFC163; --ct-accent-soft:#3B3020; --ct-on-accent:#14161A; --ct-button:#ECECEA; --ct-button-hover:#FFFFFF; --ct-on-button:#14161A; +} +.ct-home *, .ct-home *::before, .ct-home *::after { box-sizing:border-box; } +.ct-wrap { max-width:1120px; margin:0 auto; padding:0 24px; } +@media (min-width:900px) { .ct-wrap { padding:0 48px; } } +.ct-home h1, .ct-home h2, .ct-home h3 { font-family:var(--ct-sans); font-weight:700; color:var(--ct-text); margin:0; text-wrap:balance; } +.ct-home h1 { font-size:36px; line-height:40px; letter-spacing:-0.02em; } +@media (min-width:720px) { .ct-home h1 { font-size:52px; line-height:54px; } } +.ct-home h2 { font-size:26px; line-height:32px; letter-spacing:-0.01em; } +@media (min-width:720px) { .ct-home h2 { font-size:36px; line-height:40px; letter-spacing:-0.02em; } } +.ct-home h3 { font-size:20px; line-height:28px; margin-top:32px; } +.ct-home p { margin:0; color:var(--ct-text-2); max-width:64ch; } +.ct-home p strong { color:var(--ct-text); font-weight:700; } +.ct-home a { color:var(--ct-text); text-decoration:underline; text-underline-offset:3px; text-decoration-thickness:1px; text-decoration-color:var(--ct-accent); } +.ct-home a:hover { color:var(--ct-accent-hover); text-decoration-color:var(--ct-accent-hover); } +.ct-home :is(a,button,input):focus-visible { outline:2px solid var(--ct-accent); outline-offset:2px; border-radius:6px; } +.ct-home code { font-family:var(--ct-mono); font-size:13px; background:var(--ct-surface-2); border:1px solid var(--ct-border); border-radius:6px; padding:1px 5px; color:var(--ct-text); } +.ct-home pre, .ct-home pre code { font-family:var(--ct-mono); font-size:13px; line-height:20px; } +.ct-home pre { background:var(--ct-surface); border:1px solid var(--ct-border); border-radius:10px; padding:16px 20px; overflow-x:auto; margin:16px 0 0; } +.ct-home pre code { background:transparent; border:0; padding:0; } +.ct-home em { font-style:italic; } + +/* hero */ +.ct-hero { padding:64px 0 0; } +@media (min-width:900px) { .ct-hero { padding-top:96px; } } +.ct-hero-inner { text-align:center; display:flex; flex-direction:column; align-items:center; gap:0; } +.ct-hero-inner h1 { max-width:22ch; } +.ct-hero-inner .cr-lede { font-size:17px; line-height:24px; max-width:56ch; margin-top:20px; color:var(--ct-text-2); } +.ct-sub { max-width:62ch; margin-top:16px !important; } +.ct-meta { font-size:13px; line-height:18px; color:var(--ct-text-3) !important; margin-top:12px !important; } +.ct-actions { display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:20px; margin-top:28px; } +.ct-button { display:inline-flex; align-items:center; justify-content:center; height:36px; padding:0 14px; border-radius:6px; background:var(--ct-button); color:var(--ct-on-button) !important; font-weight:500; font-size:15px; text-decoration:none !important; border:1px solid transparent; cursor:pointer; white-space:nowrap; transition:background-color 150ms var(--ct-ease-in), transform 150ms var(--ct-ease-in); } +.ct-button:hover { background:var(--ct-button-hover); } +.ct-button:active { transform:translateY(1px); } +.ct-button:disabled, .ct-home fieldset:disabled .ct-button { opacity:.6; cursor:default; } +.ct-link { font-weight:500; } +.ct-install { margin-top:32px; } +.ct-install code { font-size:14px; padding:10px 16px; border-radius:6px; background:var(--ct-surface); } +.ct-recording { margin:48px 0 0; padding:0; } +.ct-recording img { display:block; width:100%; height:auto; max-width:960px; margin:0 auto; border:1px solid var(--ct-border); border-radius:10px; background:var(--ct-surface); } +@media (min-width:900px) { .ct-recording { margin-top:64px; } } +@keyframes ct-rise { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:none; } } +.ct-hero-inner > * { animation:ct-rise 250ms var(--ct-ease-in) both; } +.ct-hero-inner > *:nth-child(2) { animation-delay:40ms; } .ct-hero-inner > *:nth-child(3) { animation-delay:80ms; } .ct-hero-inner > *:nth-child(n+4) { animation-delay:120ms; } +@media (prefers-reduced-motion:reduce) { .ct-hero-inner > * { animation:none; } .ct-button { transition:none; } } + +/* the notify band */ +.ct-notify { padding:64px 0 0; } +@media (min-width:900px) { .ct-notify { padding-top:96px; } } +.ct-notify-inner { background:var(--ct-surface); border:1px solid var(--ct-border); border-radius:10px; padding:32px 24px; display:grid; gap:24px; } +@media (min-width:900px) { .ct-notify-inner { grid-template-columns:1fr 1fr; gap:48px; padding:40px 48px; align-items:center; } } +.ct-notify-copy h2 { font-size:26px; line-height:32px; letter-spacing:-0.01em; } +.ct-notify-copy p { margin-top:12px; max-width:44ch; } +.ct-notify-form { display:block; } +.ct-notify-fields { border:0; margin:0; padding:0; min-width:0; } +.ct-notify-label { display:block; font-size:13px; line-height:18px; color:var(--ct-text-2); margin-bottom:6px; } +.ct-notify-row { display:flex; gap:8px; flex-wrap:wrap; } +.ct-input { flex:1 1 220px; height:36px; padding:0 12px; border:1px solid var(--ct-border-strong); border-radius:6px; background:var(--ct-surface); color:var(--ct-text); font:inherit; font-size:15px; min-width:0; } +.ct-input::placeholder { color:var(--ct-text-3); } +.ct-input:focus { outline:2px solid var(--ct-accent); outline-offset:2px; border-color:var(--ct-border-strong); } +.ct-home fieldset:disabled .ct-input { opacity:.7; } +.ct-honeypot { position:absolute; left:-10000px; width:1px; height:1px; overflow:hidden; } +.ct-notify-error { margin-top:8px !important; font-size:13px; line-height:18px; color:var(--ct-danger) !important; } +.ct-notify-done { font-size:15px; color:var(--ct-text) !important; } + +/* sections */ +.ct-section { padding:64px 0 0; } +@media (min-width:900px) { .ct-section { padding-top:96px; } } +.ct-prose > p, .ct-prose > dl, .ct-prose > .ct-compare, .ct-prose > table { margin-top:16px; } +.ct-prose > h2 + p { margin-top:16px; } +.ct-lead { font-size:17px; line-height:24px; max-width:66ch; } +.ct-compare { display:grid; border:1px solid var(--ct-border); border-radius:10px; overflow:hidden; background:var(--ct-surface); max-width:880px; } +@media (min-width:720px) { .ct-compare { grid-template-columns:1fr 1fr; } } +.ct-compare > div { padding:16px 20px; } +.ct-compare > div + div { border-top:1px solid var(--ct-border); } +@media (min-width:720px) { .ct-compare > div + div { border-top:0; border-left:1px solid var(--ct-border); } } +.ct-compare-label { display:block; font-size:13px; line-height:18px; color:var(--ct-text-3); margin-bottom:6px; } +.ct-compare p { font-size:14px; line-height:20px; } +.ct-rules { display:grid; gap:12px 32px; margin:24px 0 0; max-width:880px; } +@media (min-width:720px) { .ct-rules { grid-template-columns:1fr 1fr; } } +.ct-rules div { padding:12px 0; border-top:1px solid var(--ct-border); } +.ct-rules dt { font-weight:700; color:var(--ct-text); } +.ct-rules dd { margin:2px 0 0; font-size:14px; line-height:20px; color:var(--ct-text-2); } +.ct-links { display:flex; flex-wrap:wrap; gap:8px 24px; margin-top:24px !important; } +.ct-table { width:100%; border-collapse:collapse; font-size:14px; line-height:20px; max-width:880px; } +.ct-table th { text-align:left; font-weight:500; color:var(--ct-text-3); font-size:13px; padding:8px 12px 8px 0; border-bottom:1px solid var(--ct-border); } +.ct-table td { padding:10px 12px 10px 0; border-bottom:1px solid var(--ct-border); vertical-align:top; color:var(--ct-text-2); } +.ct-table td:first-child { color:var(--ct-text); } +.ct-diagram { margin-top:32px; } +.ct-diagram svg { width:100%; height:auto; display:block; } +.ct-doclist { margin:24px 0 0; display:grid; gap:0; max-width:880px; } +.ct-doclist div { display:grid; gap:4px; padding:12px 0; border-top:1px solid var(--ct-border); } +@media (min-width:720px) { .ct-doclist div { grid-template-columns:260px 1fr; gap:24px; } } +.ct-doclist dt { color:var(--ct-text); font-weight:500; } +.ct-doclist dd { margin:0; display:flex; flex-wrap:wrap; gap:6px 20px; } +.ct-badges { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:24px !important; } +.ct-badges img { height:20px; width:auto; display:block; } +.ct-open { padding-bottom:64px; } +.ct-footer { border-top:1px solid var(--ct-border); padding:40px 0 48px; margin-top:32px; } +.ct-footer-inner { display:flex; flex-direction:column; gap:16px; } +@media (min-width:720px) { .ct-footer-inner { flex-direction:row; justify-content:space-between; align-items:baseline; } } +.ct-footer p { color:var(--ct-text-2); } +.ct-footer nav { display:flex; gap:24px; flex-wrap:wrap; } + + +/* one diagram at a time: the snippet ships a wide and a narrow drawing */ +.ct-home .cr-dia-narrow { display:none; } +@media (max-width:719px) { .ct-home .cr-dia-wide { display:none; } .ct-home .cr-dia-narrow { display:block; } } +/* the diagram, restyled monochrome inside this page */ +.ct-home .cr-diagram text { font-family:var(--ct-sans); } +.ct-home .cr-dia-label, .ct-home .cr-dia-num { fill:var(--ct-accent); font-family:var(--ct-sans); font-size:13px; font-weight:500; letter-spacing:0; } +.ct-home .cr-dia-name { fill:var(--ct-text); font-weight:700; } +.ct-home .cr-dia-gloss, .ct-home .cr-dia-note { fill:var(--ct-text-2); } +.ct-home .cr-dia-claim { fill:var(--ct-text); font-weight:500; } +.ct-home .cr-dia-strip, .ct-home .cr-dia-box { fill:var(--ct-surface); stroke:var(--ct-border); } +.ct-home .cr-dia-node { fill:var(--ct-surface-2); stroke:var(--ct-border); } +.ct-home .cr-dia-arrow { stroke:var(--ct-border-strong); } .ct-home .cr-dia-head { fill:var(--ct-border-strong); } +.ct-home .cr-dia-tier-label { fill:var(--ct-accent); font-family:var(--ct-sans); letter-spacing:0; } + +/* the project's own accent: amber and black, kept on purpose (2026-09-15) */ +.ct-home .cr-dot { color:var(--ct-accent); } +.ct-home .cr-mark { color:inherit; font-weight:inherit; } +.ct-wordmark img { height:28px; width:auto; display:block; } +.ct-input:focus { outline-color:var(--ct-accent); } diff --git a/tests/test_home_and_readme_agree.py b/tests/test_home_and_readme_agree.py index 0f59280..d7d7663 100644 --- a/tests/test_home_and_readme_agree.py +++ b/tests/test_home_and_readme_agree.py @@ -20,6 +20,7 @@ REPO_ROOT = Path(__file__).resolve().parents[1] HOME = REPO_ROOT / "index.mdx" +WWW = REPO_ROOT / "www" / "index.html" DIAGRAM = REPO_ROOT / "snippets" / "how-diagram.jsx" README = CORE_ROOT / "README.md" @@ -79,3 +80,17 @@ def test_the_readme_walks_the_homepage_seven_steps_in_order(): positions = [section.find(f"**{step}:") for step in steps] assert all(p >= 0 for p in positions), dict(zip(steps, positions, strict=True)) assert positions == sorted(positions), "the README walks the steps in the diagram's order" + + +def test_the_static_site_opens_with_the_same_h1_and_lede(): + """ctrlrun.dev is served from `www/` (Vercel) and docs.ctrlrun.dev from Mintlify; both + carry the homepage. The static page must open with the same H1 and lede the README does, + or the README sync above is only half true.""" + h1 = _homepage(r'(.*?)
') + page = WWW.read_text(encoding="utf-8") + assert h1 in _prose(page), "www/index.html does not carry the homepage H1" + assert lede in _prose(page), "www/index.html does not carry the homepage lede" + assert "ctrlaiagents" not in page and "ctrlpayments" not in page.lower(), ( + "the project site names a commercial site; it is not supposed to" + ) diff --git a/website-form/api/interest.mjs b/website-form/api/interest.mjs index 46aa613..5dc2382 100644 --- a/website-form/api/interest.mjs +++ b/website-form/api/interest.mjs @@ -6,8 +6,10 @@ const RECIPIENT = 'contact@arpanghoshal.com'; // Pro takes a waiting-list place and needs nothing else; Enterprise is a conversation, so it // asks what the deployment needs. The intent decides which, and nothing else in the request does. const INTENTS = { - 'pro-waitlist': { subject: 'CTRLRun Pro waiting list', heading: 'CTRLRun Pro waiting list request', requiresMessage: false }, - 'enterprise-contact': { subject: 'CTRLRun Enterprise enquiry', heading: 'CTRLRun Enterprise enquiry', requiresMessage: true } + 'pro-waitlist': { subject: 'CTRLRun Pro waiting list', heading: 'CTRLRun Pro waiting list request', requiresMessage: false, requiresCompany: true }, + 'enterprise-contact': { subject: 'CTRLRun Enterprise enquiry', heading: 'CTRLRun Enterprise enquiry', requiresMessage: true, requiresCompany: true }, + // The homepage's "Something bigger is coming" form: an email and nothing else. + 'launch-updates': { subject: 'CTRLRun launch updates', heading: 'CTRLRun launch updates: someone asked to hear first', requiresMessage: false, requiresCompany: false } }; const requests = new Map(); const WINDOW_MS = 600_000; @@ -26,7 +28,7 @@ export function validate(input) { if (!shape) throw new Error('Unknown request type.'); const email = field('email', 254); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email) || /[\r\n]/.test(email)) throw new Error('Enter a valid work email.'); - const company = field('company', 100); + const company = field('company', 100, shape.requiresCompany); const message = field('message', 600, shape.requiresMessage); const agents = field('agents', 200, false); const requestId = field('requestId', 36); @@ -64,12 +66,12 @@ export function createHandler({ env = process.env, fetcher = fetch, now = Date.n const entry = rateStore.get(rateKey) || { start: time, count: 0 }; if (entry.count >= MAX_REQUESTS || rateStore.size >= 10_000) { res.setHeader('Retry-After', '600'); return res.status(429).json({ error: 'Too many requests. Please try again later or email us directly.' }); } rateStore.set(rateKey, { start: entry.start, count: entry.count + 1 }); - const text = [data.shape.heading, '', 'Company: ' + data.company, 'Reply email: ' + data.email, data.agents && 'Agents and actions: ' + data.agents, data.message && 'What they need: ' + data.message].filter(Boolean).join('\n'); + const text = [data.shape.heading, '', data.company && 'Company: ' + data.company, 'Reply email: ' + data.email, data.agents && 'Agents and actions: ' + data.agents, data.message && 'What they need: ' + data.message].filter(Boolean).join('\n'); try { const response = await fetcher('https://api.resend.com/emails', { method: 'POST', headers: { Authorization: 'Bearer ' + env.RESEND_API_KEY, 'Content-Type': 'application/json', 'Idempotency-Key': data.intent + '/' + data.requestId }, - body: JSON.stringify({ from: 'CTRLRunThe address may be wrong, or the page has moved to the documentation.
Every action is checked against your rules before it runs. Allowed actions go through. Sensitive ones wait for a person. Forbidden ones are blocked.
+Execution safety for AI agents. A Python library that sits between the decision to act and the call that acts. A consequential action happens at most once, exactly as approved, and leaves a receipt. When the outcome is unknown, CTRLRun says so instead of guessing.
+ + +pip install ctrlrun && ctrlrun demo
+ CTRLRun is the kernel. What we are building on it is almost ready. Leave your email and you will hear first, once, when it lands.
+The model guesses. CTRLRun does not. The ticket says refund €500. The agent asks for €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.
+Nothing checks the amount. The call goes through. €4,500 too much.
Your rule checks the amount. The call never leaves. €0 wrongly paid.
CTRLRun is that check. It reads the arguments about to leave your process and answers what may happen to them: let it run, ask a human, or stop it cold. Four rules do the work, and each one is a test in this repository before it is a sentence here.
+The animation above is this section, recorded against the real library: one policy file, two short programs, four commands, nothing staged.
+ +pip install ctrlrun
+
+ One file, ctrlrun.yaml. Amounts are integer minor units, so 50000 is €500. Both ends of every band are bound, because an upper bound alone lets a negative amount through, and a refund of a negative amount is a charge. Anything not listed is denied; there is no default-allow.
schema: ctrlrun.policy/v2
+
+actions:
+ stripe.refund:
+ effect: "refund:{payment_id}"
+ rules:
+ - when: { amount_gte: 0, amount_lte: 50000 }
+ decision: allow # up to €500: the agent acts alone
+ - when: { amount_gte: 0, amount_lte: 1000000 }
+ decision: approve # up to €10,000: a human decides
+ - decision: deny # above that: never
+
+ The decorator names the action, the effect key names the consequence it has in the world, and the context names who is acting. stripe here is a stand-in that records calls instead of making them.
+
+
+@ctrlrun.protect("stripe.refund", effect="refund:{payment_id}")
+def refund(payment_id: str, amount: int) -> dict:
+ return stripe.refund(payment_id, amount)
+
+
+with ctrlrun.context(agent="support-agent"):
+ refund(payment_id="txn_1", amount=50_000) # €500 -> succeeded
+ refund(payment_id="txn_2", amount=500_000) # €5,000 -> ApprovalRequired, a human decides
+
+ The €500 refund runs on its own. The €5,000 one stops and names the request a human answers.
+A human answers from the shell. The grant names the hash of the exact action the human read, and when it lapses.
+ +$ ctrlrun approve "$(cat request_id.txt)"
+granted apr_63e80076f2cccfee52b17491a4b2e125 for sha256:22ec1c39…6af752
+expires 2026-09-13T20:10:11.367Z
+
+ The agent presents it, then tries to spend it on something else. The first call is exactly what the human approved, and it runs. The second is the same approval with one digit changed, and it matches nothing.
+ +€5,000 with the approval -> succeeded
+€9,000 on that same approval -> refused
+calls that reached the provider: 1 (the €9,000 never left)
+
+ Every attempt, refusals included, left a receipt, and ctrlrun receipts lists them. That is the whole integration: a policy file, a decorator, a context, and with_approval to present a grant. Money is the example, not the scope: the same policy language reads role_in or replicas_lte as easily as amount_lte.
You probably do not need an adapter. @protect covers anything running in this process: a raw model call, a LangChain tool, a hand-rolled loop, a cron job. The gateway covers anything that reaches its tools over MCP, in any language.
| You have | Use | Needs |
|---|---|---|
| Python in this process | the @protect decorator, shown above | nothing beyond pip install ctrlrun |
| Tools behind an MCP server, in any language | the gateway: pip install "ctrlrun[gateway]" | one command, no change to agent or server code |
| A framework with its own approval interrupt | an adapter | the framework to have a human-in-the-loop primitive |
It works with agents you can and can't modify. WhatsApp, Slack and Teams bots, ChatGPT, Cursor, Codex, OpenAI Agents: any AI agent you have. CTRLRun checks the action, not the agent, so if the agent acts through a tool server or an API you run, the action is checked, and the agent is not rebuilt, redeployed or told. Agents you can't modify says where the boundary goes for each kind.
+Every protected call, whichever way it arrives, goes through the same seven steps. Only then does it reach your systems.
+State lives in SQLite by default, a file with no server and no ops, and the reservation holds across processes rather than merely across threads. Point it at Postgres when more than one host writes: one URL, the same guarantees graded by the same suite. Prove it in your own setup with ctrlrun verify, which runs the kernel's own failure scenarios against your policy in a scratch store. It reaches no network.
$ ctrlrun verify
+CTRLRun verify — ctrlrun 0.9.0, catalogue ctrlrun.guarantees/v6
+policy ctrlrun.yaml (ctrlrun.policy/v2, mode: enforce)
+store sqlite, scratch (created and destroyed for this run)
+
+G1 mutated approval refused PASS stripe.refund
+G2 replayed approval refused PASS stripe.refund
+G3 duplicate effect refused PASS stripe.refund
+G4 one winner under concurrency PASS stripe.refund (8 processes)
+G5 ambiguous blocks a blind retry PASS stripe.refund
+G6 unknown action refused PASS
+G7 no principal refused PASS stripe.refund
+G10 unknown exception is ambiguous PASS stripe.refund
+G11 an altered receipt is detected PASS stripe.refund
+… 24 guarantees graded in the full run
+
+ What verify provesThreat modelEvery sentence mapped to its test
+docs.ctrlrun.dev is the documentation: concepts, guides, a cookbook, the full reference, and a browser demo that runs with no install.
+Issues and pull requests are welcome: CONTRIBUTING.md and CODE_OF_CONDUCT.md have the working agreement, and SECURITY.md is how to report a vulnerability. Every claim on this page has a test behind it, so a change to the prose usually means a change to the suite. CHANGELOG.md and the roadmap say where it is going. Releases carry PyPI provenance attestations from GitHub Actions.
+ +