From c35e56aadb4c31c33f144c05add1d0785b9aab32 Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 16 Sep 2026 17:14:06 +0530 Subject: [PATCH 1/2] ctrlrun.dev is the documentation: the root page joins the docs, the teaser goes, the wordmark is the lowercase ctrlrun The marketing tab and its slides are gone; the root page keeps the kicker, the headline, the lede, the one example, the any-agent section and how it works, now as the first page of Documentation with a sidebar. The What comes next section and its form are removed with their snippet and styles. The wordmark is the family's lowercase drawing with the amber full stop; the library's copies change in CTRLRun/ctrlrun on the same-named branch. --- docs.json | 11 +-- images/wordmark-dark.svg | 7 +- images/wordmark-light.svg | 7 +- index.mdx | 21 +++--- snippets/home-slides.jsx | 138 ------------------------------------ snippets/launch-updates.jsx | 68 ------------------ style.css | 8 --- 7 files changed, 13 insertions(+), 247 deletions(-) delete mode 100644 snippets/home-slides.jsx delete mode 100644 snippets/launch-updates.jsx diff --git a/docs.json b/docs.json index ced1ba3..906318c 100644 --- a/docs.json +++ b/docs.json @@ -37,23 +37,18 @@ }, "navigation": { "tabs": [ - { - "tab": "CTRLRun", - "pages": [ - "index", - "execution-boundary" - ] - }, { "tab": "Documentation", "groups": [ { "group": "Start", "pages": [ + "index", "docs", "docs/why", "docs/not-only-agents", - "docs/agents-you-cant-modify" + "docs/agents-you-cant-modify", + "execution-boundary" ] }, { diff --git a/images/wordmark-dark.svg b/images/wordmark-dark.svg index fd0b506..d4f2a50 100644 --- a/images/wordmark-dark.svg +++ b/images/wordmark-dark.svg @@ -1,6 +1 @@ - - - - - CTRLRun - + diff --git a/images/wordmark-light.svg b/images/wordmark-light.svg index 5cd37c3..11dc86d 100644 --- a/images/wordmark-light.svg +++ b/images/wordmark-light.svg @@ -1,6 +1 @@ - - - - - CTRLRun - + diff --git a/index.mdx b/index.mdx index 0428a41..1b07e9a 100644 --- a/index.mdx +++ b/index.mdx @@ -1,20 +1,17 @@ --- title: "Stop wrong, restricted, or malicious AI agent actions" -sidebarTitle: "CTRLRun" +sidebarTitle: "Overview" description: "Every AI agent action is checked against your rules before it runs. Allowed ones go through, sensitive ones wait for a person, forbidden ones are blocked." -mode: "custom" +mode: "wide" "og:title": "CTRLRun: stop wrong, restricted, or malicious agent actions" "twitter:title": "CTRLRun: stop wrong, restricted, or malicious agent actions" canonical: "https://ctrlrun.dev/" "og:url": "https://ctrlrun.dev/" --- -import { HomeSlides } from "/snippets/home-slides.jsx"; import { HowDiagram } from "/snippets/how-diagram.jsx"; -import { LaunchUpdates } from "/snippets/launch-updates.jsx";
-

THE AGENT IS PROBABILISTIC. THE ACTION IS NOT.

@@ -67,13 +64,11 @@ import { LaunchUpdates } from "/snippets/launch-updates.jsx";

Interactive walkthrough: follow one agent action through every check →

-
-
-

WHAT COMES NEXT

-

Something is coming that makes controlling agents easy for your business.

-

CTRLRun today is a library you install and wire in yourself. What we are building next takes the same boundary and makes it something a team runs without writing the wiring. Leave an address and we will tell you when it is ready. Nothing else, and no one else gets it.

- -
-
+ +## Next + +- [Why](/docs/why): what CTRLRun believes and why. +- [Quickstart](/docs/get-started/quickstart): protect your first action. +- [The execution boundary](/execution-boundary): follow one agent action through every check. diff --git a/snippets/home-slides.jsx b/snippets/home-slides.jsx deleted file mode 100644 index 53336d5..0000000 --- a/snippets/home-slides.jsx +++ /dev/null @@ -1,138 +0,0 @@ -// Slides are a desktop presentation. Below this query the page is a plain document: on a -// phone the sections are taller than the screen, and `scroll-snap-stop: always` together with -// a slide height pinned to `innerHeight` (which moves every time the address bar collapses) -// made each flick re-snap mid-scroll. -export const HomeSlides = () => { - const SLIDE_MEDIA = '(min-width: 801px) and (hover: hover)'; - const SLIDES = [ - { id: 'overview', label: 'Overview' }, - { id: 'how-it-works', label: 'How it works' }, - { id: 'updates', label: 'What comes next' }, - ]; - const LAST = SLIDES.length - 1; - const [active, setActive] = useState(0); - const [ready, setReady] = useState(false); - const activeRef = useRef(0); - - // Where a scroll is *heading*, and when to stop believing it. `activeRef` tracks where the - // page is, and for the first half of an 800ms smooth scroll that is still the slide being - // left -- so a second arrow press mid-animation recomputed the same destination and - // re-issued the same trip, which reads as a keypress the page ignored. - const targetRef = useRef(-1); - const targetExpiry = useRef(0); - const currentIndex = () => - (targetRef.current >= 0 && performance.now() < targetExpiry.current ? targetRef.current : activeRef.current); - - const goTo = (index) => { - const slide = document.getElementById(SLIDES[index].id); - if (!slide) return; - const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; - targetRef.current = index; - // Cleared on arrival below; this only bounds a trip that never lands. - targetExpiry.current = performance.now() + 1200; - slide.scrollIntoView({ behavior: reduced ? 'instant' : 'smooth', block: 'start' }); - }; - - // Scrolling is the browser's: `scroll-snap-type` in style.css does the snapping, for wheel, - // trackpad and touch alike. This adds keyboard navigation and the dots, and tracks which - // section is in view. Nothing here intercepts a scroll -- a wheel handler that stepped one - // slide per gesture fought every scroll it did not recognise. - useEffect(() => { - const root = document.documentElement; - const home = document.querySelector('.cr-home'); - if (!home) return; - const sections = SLIDES.map(slide => document.getElementById(slide.id)); - let frame = 0; - let headerHeight = 64; - const media = window.matchMedia(SLIDE_MEDIA); - - const updateActive = () => { - frame = 0; - let best = 0; - let visible = -1; - sections.forEach((section, index) => { - if (!section) return; - const rect = section.getBoundingClientRect(); - const amount = Math.max(0, Math.min(rect.bottom, innerHeight) - Math.max(rect.top, headerHeight)); - if (amount > visible) { visible = amount; best = index; } - }); - activeRef.current = best; - if (best === targetRef.current) { targetRef.current = -1; targetExpiry.current = 0; } - setActive(best); - }; - const onScroll = () => { - if (!frame) frame = requestAnimationFrame(updateActive); - }; - const onResize = () => { - headerHeight = Math.max(0, Math.round(home.getBoundingClientRect().top + window.scrollY)); - if (media.matches) { - root.style.setProperty('--cr-slide-top', `${headerHeight}px`); - root.style.setProperty('--cr-slide-height', `${Math.max(240, window.innerHeight - headerHeight)}px`); - } - onScroll(); - }; - const onKeyDown = (event) => { - if (!media.matches) return; - if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return; - const target = event.target; - if (target instanceof Element && target.closest('input, textarea, select, video, a, [contenteditable], [role="dialog"], [role="slider"]')) return; - if (target instanceof Element && target.closest('button') && !target.closest('.cr-slide-dots')) return; - const current = currentIndex(); - let next; - if (['ArrowDown', 'ArrowRight', 'PageDown'].includes(event.key)) next = Math.min(LAST, current + 1); - else if (['ArrowUp', 'ArrowLeft', 'PageUp'].includes(event.key)) next = Math.max(0, current - 1); - else if (event.key === 'Home') next = 0; - else if (event.key === 'End') next = LAST; - else return; - event.preventDefault(); - if (!event.repeat) goTo(next); - }; - - const applyMedia = () => { - if (media.matches) { - onResize(); - root.classList.add('cr-slides-active'); - } else { - root.classList.remove('cr-slides-active'); - root.style.removeProperty('--cr-slide-top'); - root.style.removeProperty('--cr-slide-height'); - } - setReady(media.matches); - }; - applyMedia(); - media.addEventListener('change', applyMedia); - window.addEventListener('scroll', onScroll, { passive: true }); - window.addEventListener('resize', onResize); - window.addEventListener('keydown', onKeyDown); - const header = document.getElementById('navbar'); - const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(onResize); - if (header && observer) observer.observe(header); - const hashIndex = SLIDES.findIndex(slide => `#${slide.id}` === window.location.hash); - if (hashIndex >= 0) requestAnimationFrame(() => goTo(hashIndex)); - - return () => { - root.classList.remove('cr-slides-active'); - root.style.removeProperty('--cr-slide-top'); - root.style.removeProperty('--cr-slide-height'); - window.removeEventListener('scroll', onScroll); - window.removeEventListener('resize', onResize); - window.removeEventListener('keydown', onKeyDown); - media.removeEventListener('change', applyMedia); - observer?.disconnect(); - cancelAnimationFrame(frame); - }; - }, []); - - return ( - - ); -}; diff --git a/snippets/launch-updates.jsx b/snippets/launch-updates.jsx deleted file mode 100644 index 4476780..0000000 --- a/snippets/launch-updates.jsx +++ /dev/null @@ -1,68 +0,0 @@ -// The one form on the site: an address, and nothing else. -// -// ctrlrun.dev is a technical site for developers. It carries no pricing, no tiers and no sales -// path, and this is the single place it asks a reader for anything. That constraint is what -// makes the form's shape obvious: email, a honeypot, a button. No company field, no "what do -// your agents do", no qualification. Asking a stranger to describe their deployment before they -// have run `pip install` is how a project that wants users behaves like a project that wants -// leads. -// -// It posts to the same endpoint the site has always used, with `intent: 'launch-updates'`. The -// endpoint decides the subject line and the required fields from that intent and trusts nothing -// else the browser sends, so a new intent is a change in `website-form/api/interest.mjs` and not -// something a page can assert into existence. -// -// **The prose lives in `index.mdx`, not here.** Mintlify renders a snippet on the client, so -// anything drawn in this file is absent from the server-rendered HTML and invisible to a crawler -// or to any fetcher that does not run JavaScript. `commercial-tiers.jsx` learned that the -// expensive way: roughly 190 words of the page's plainest prose were missing from the HTML. Only -// what needs state is in here. -export const LaunchUpdates = () => { - const ENDPOINT = 'https://ctrlrun-review-form.vercel.app/api/interest'; - - const [sent, setSent] = useState(false); - const [email, setEmail] = useState(''); - const [website, setWebsite] = useState(''); - const [sending, setSending] = useState(false); - const [error, setError] = useState(''); - const requestId = useRef(null); - const sendingRef = useRef(false); - const track = name => window.dispatchEvent(new CustomEvent('ctrlrun:conversion', { detail: { name } })); - - const send = async event => { - event.preventDefault(); - if (sendingRef.current || sent) return; - // One id per filled-in form: an uncertain response can be retried without a second email. - if (!requestId.current) requestId.current = crypto.randomUUID(); - sendingRef.current = true; setSending(true); setError(''); - try { - const response = await fetch(ENDPOINT, { - method: 'POST', credentials: 'omit', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ intent: 'launch-updates', email, website, requestId: requestId.current }), - signal: AbortSignal.timeout(15000) - }); - const data = await response.json(); - if (!response.ok || data.ok !== true || typeof data.id !== 'string') throw new Error(data.error || 'We could not confirm submission. Please retry or email us directly.'); - setSent(true); track('launch_updates_submitted'); - } catch (failure) { - setError(failure.name === 'TimeoutError' || failure.name === 'TypeError' ? 'We could not confirm submission. You can retry the same request safely, or email us directly.' : failure.message); - } finally { sendingRef.current = false; setSending(false); } - }; - - if (sent) return

You are on the list. We will write to that address once, when it is ready.

; - - return ( -
-
- - - -
- {error &&

{error}

} -

One address, one email when it ships. Sent through Resend and used for nothing else.

-
- ); -}; diff --git a/style.css b/style.css index 50d55a7..c0e5945 100644 --- a/style.css +++ b/style.css @@ -784,14 +784,6 @@ body:has(.cr-subpage) #search-bar-entry-mobile,body:has(.cr-subpage) #assistant- `.cr-tier-error` and `.cr-button` rather than restating them: the signup is a narrower version of a form that already existed here, and a second copy of the field styling would be a second thing to keep in step with the type scale. */ -.cr-home-updates { border-top:1px solid var(--cr-line); } -.cr-updates-inner { max-width:640px; } -.cr-updates-body { margin:18px 0 28px; color:var(--cr-muted); } -.cr-updates-form fieldset { display:flex; flex-wrap:wrap; gap:12px; border:0; padding:0; margin:0; align-items:flex-end; } -.cr-updates-field { flex:1 1 260px; } -.cr-updates-field input { width:100%; } -.cr-updates-done { border-top:2px solid var(--cr-accent); padding:20px 0 0; font-weight:550; } -.cr-updates-form .cr-caption { margin-top:12px; } /* The label is present for a screen reader and absent for everyone else: one field with a placeholder needs no visible label, and a form with no label at all is not navigable. */ .cr-visually-hidden { position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; } From 049c5b51b92cccb98086aa655246f3d86ba9ae90 Mon Sep 17 00:00:00 2001 From: arpan Date: Wed, 16 Sep 2026 17:14:56 +0530 Subject: [PATCH 2/2] The root page says which products are built on the kernel ctrl ai agents and ctrl payments, named once, with what each does and the line that the kernel deciding under them is this one. --- index.mdx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/index.mdx b/index.mdx index 1b07e9a..0e8175d 100644 --- a/index.mdx +++ b/index.mdx @@ -64,9 +64,12 @@ import { HowDiagram } from "/snippets/how-diagram.jsx";

Interactive walkthrough: follow one agent action through every check →

- +## Built on this kernel + +Two products run on CTRLRun and credit it on every page. [ctrl ai agents](https://ctrlaiagents.com), the hosted product for a person or a team, puts this boundary under any agent you buy or build, with the inbox, the receipts and the analysis in one dashboard. [ctrl payments](https://ctrlpayments.com) is the same boundary for money: every payment an agent attempts is allowed, held for a person, or refused before it leaves, and there is a receipt either way. The kernel that decides and refuses is this one, Apache-2.0, and the receipt format they write is the one documented here. + ## Next - [Why](/docs/why): what CTRLRun believes and why.