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.
-
-
-
-
+
+## 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.
+- [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 (
-
- );
-};
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; }