From 7fd3822040cc17cebdb8c0b45105029a40c56e91 Mon Sep 17 00:00:00 2001 From: RyuseiTaniguchi Date: Tue, 4 Aug 2026 17:42:34 -0700 Subject: [PATCH] landing: rebuild the page below the hero as one continuous night The hero and the body were two design systems glued at a seam. The hero is a material: mesh gradient, grain, glass, a terminal card on a tinted shadow. The body was a document: ink on paper, hairlines, no depth. Nothing carried across except the typeface, so the spec bar read as the first row of a different site. The hero and the closing CTA are unchanged. Everything between them is rebuilt. - The sky now runs the whole document and deepens as you descend. The spec strip closes the hero in the hero's own material instead of opening the body in white, and the footer goes dark so the page never flips theme mid-scroll. - A sticky rail replaces the section tab bar, which sat at top:0 under a 66px fixed nav and was invisible the entire time it was stuck. - Artifacts do the explaining. The call renders as a real
 transcript,
  including the tool the guard refused; the grant surface renders as the
  SKILL.md it actually is. Both are selectable text, not styled divs.
- Content is rewritten to four beats: why this exists, what you control, what it
  costs the callee, where it stands. The wire sequence, repo layout and address
  anatomy move to the docs, which is where that depth belongs.
- The four-layer permission model is three plain lines plus a docs link. Opening
  with "IT sets the ceiling" put a procurement doc in front of the individual
  developer who is the one actually installing this.

Accessibility, measured on rendered pixels rather than assumed: secondary text
was 1.35:1 on the eyebrows and 1.54:1 in the rail footer, now 4.5:1 or better
throughout. Adds a skip link, a visible focus ring (the browser default is dark
blue and vanished against the sky), prefers-reduced-motion, 100dvh, and 44px
minimum tap targets.

Page height drops from 12,639px to 5,733px at desktop.
---
 apps/landing/index.html | 871 +++++++++++++++++-----------------------
 1 file changed, 367 insertions(+), 504 deletions(-)

diff --git a/apps/landing/index.html b/apps/landing/index.html
index 4da02be0..ea498114 100644
--- a/apps/landing/index.html
+++ b/apps/landing/index.html
@@ -51,15 +51,45 @@
   --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
   --wrap:1180px;
   --pad:clamp(20px,5vw,56px);
+
+  /* The page is one continuous night. The hero's sky does not stop at the
+     terminal — it carries the whole document, and every surface below is glass
+     lit over it rather than paper laid beside it. */
+  --glass:rgba(255,255,255,.055);
+  --glass-2:rgba(255,255,255,.035);
+  --edge:rgba(255,255,255,.12);
+  --tx:rgba(255,255,255,.8);
+  /* .5 measured 1.35:1 over this sky. Secondary text needs .72 to clear AA. */
+  --tx-dim:rgba(255,255,255,.72);
+  --accent:#8FC0FF;
+  --ok:#7BD8A6;
+  --warn:#E4B363;
+  --bad:#FF9C8A;
+  --r-lg:14px;
+  --r-sm:12px;
 }
 
 *{box-sizing:border-box}
 html{scroll-behavior:smooth}
 body{
-  margin:0;background:var(--paper);color:var(--ink);
+  margin:0;background:#060B1B;color:#fff;
   font-family:var(--sans);font-size:16px;line-height:1.65;font-weight:400;
   -webkit-font-smoothing:antialiased;
 }
+/* One sky behind the whole document, deepening as you descend. The hero paints
+   its own on top of this, so the hero itself is unchanged. */
+.page-sky{position:fixed;inset:0;z-index:-3;background:
+  radial-gradient(58% 42% at 3% 93%,rgba(236,165,168,.26) 0%,rgba(171,93,116,.17) 38%,rgba(120,70,110,0) 72%),
+  radial-gradient(46% 52% at 99% 66%,rgba(244,180,168,.22) 0%,rgba(198,144,170,.15) 40%,rgba(140,110,170,0) 74%),
+  radial-gradient(66% 40% at 1% 44%,rgba(150,120,192,.24) 0%,rgba(113,94,164,.13) 45%,rgba(80,80,150,0) 74%),
+  linear-gradient(158deg,#0D2C75 0%,#123F97 24%,#123468 46%,#141C48 70%,#080D22 100%)}
+.page-grain{position:fixed;inset:0;z-index:-2;opacity:.15;pointer-events:none;mix-blend-mode:overlay;
+  background-image:radial-gradient(rgba(255,255,255,.5) .5px,transparent .5px);background-size:3px 3px}
+.page-veil{position:fixed;inset:0;z-index:-1;pointer-events:none;
+  background:linear-gradient(180deg,rgba(4,9,24,0),rgba(4,9,24,.62))}
+.skip{position:absolute;left:-9999px;top:0;z-index:80;padding:.8rem 1.2rem;background:#fff;color:var(--ink);
+  font-size:.9rem;font-weight:500;border-radius:0 0 8px 0}
+.skip:focus{left:0}
 a{color:inherit;text-decoration:none}
 :focus-visible{outline:2px solid var(--blue);outline-offset:3px;border-radius:2px}
 
@@ -74,15 +104,20 @@
 h1 em,h2 em{font-style:italic;font-weight:500}
 p{margin:0}
 
+/* Only the hero and the closing CTA use an eyebrow now. The body sections
+   dropped theirs: the rail already names each one, so a label above the
+   headline was saying the same thing twice. */
 .eyebrow{
-  font-family:var(--mono);font-size:.665rem;letter-spacing:.22em;text-transform:uppercase;
-  color:var(--ink-3);display:inline-flex;align-items:center;gap:.6em;margin-bottom:1.1rem;
+  font-family:var(--mono);font-size:.665rem;letter-spacing:.16em;text-transform:uppercase;
+  color:rgba(255,255,255,.82);display:inline-flex;align-items:center;gap:.6em;margin-bottom:1.1rem;
+  border:1px solid rgba(255,255,255,.24);border-radius:999px;padding:.42rem 1rem;
+  background:rgba(255,255,255,.07);backdrop-filter:blur(6px);
 }
-.eyebrow::before{content:"";width:5px;height:5px;border-radius:50%;background:var(--blue)}
+.eyebrow::before{content:"";width:5px;height:5px;border-radius:50%;background:#8FC0FF;box-shadow:0 0 10px #8FC0FF}
 .lede{
-  margin-top:1.1rem;max-width:56ch;color:var(--ink-2);font-size:1.02rem;line-height:1.72;font-weight:300;
+  margin-top:1.2rem;max-width:64ch;color:var(--tx);font-size:1.01rem;line-height:1.78;font-weight:300;
+  text-wrap:pretty;
 }
-.sec-head{margin-bottom:clamp(2.4rem,5vw,3.6rem)}
 
 /* ---------- nav ---------- */
 .nav{
@@ -108,11 +143,48 @@
 .nav .btn-sm:hover{transform:translateY(-1px);box-shadow:0 6px 18px rgba(0,0,0,.16)}
 .nav.over-hero{color:#fff}
 .nav.pinned{
-  background:rgba(250,249,247,.9);backdrop-filter:saturate(180%) blur(14px);
+  background:rgba(250,249,247,.97);backdrop-filter:saturate(180%) blur(20px);
   border-bottom-color:var(--line);color:var(--ink);
 }
 .nav.pinned .btn-sm{background:var(--ink);color:var(--paper)}
-@media(max-width:860px){.nav-links{display:none}}
+
+/* mobile menu */
+.nav-toggle{
+  display:none;position:relative;margin-left:auto;width:44px;height:44px;
+  background:none;border:0;color:inherit;cursor:pointer;
+}
+/* absolute bars, not stacked flow — the close transform needs each bar's
+   resting offset to be exact or the X opens as a chevron. */
+.nav-toggle span{
+  position:absolute;left:12px;width:20px;height:1.5px;background:currentColor;border-radius:2px;
+  transition:transform .28s ease,opacity .2s ease;
+}
+.nav-toggle span:nth-child(1){top:16px}
+.nav-toggle span:nth-child(2){top:21px}
+.nav-toggle span:nth-child(3){top:26px}
+.nav-toggle[aria-expanded="true"] span:nth-child(1){transform:translateY(5px) rotate(45deg)}
+.nav-toggle[aria-expanded="true"] span:nth-child(2){opacity:0}
+.nav-toggle[aria-expanded="true"] span:nth-child(3){transform:translateY(-5px) rotate(-45deg)}
+.nav-menu{
+  position:fixed;inset:66px 0 auto 0;z-index:59;display:none;flex-direction:column;
+  padding:.5rem var(--pad) 1.6rem;background:var(--paper);border-bottom:1px solid var(--line);
+  box-shadow:0 24px 48px -24px rgba(0,6,25,.28);
+}
+.nav-menu.open{display:flex}
+.nav-menu a{
+  display:flex;align-items:center;min-height:48px;color:var(--ink);
+  font-size:1rem;border-bottom:1px solid var(--line-soft);
+}
+.nav-menu a:last-of-type{border-bottom:0}
+.nav-menu .btn-sm{
+  justify-content:center;margin-top:1rem;min-height:48px;border-bottom:0;
+  background:var(--ink);color:var(--paper);font-size:.9rem;
+}
+body.menu-open{overflow:hidden}
+@media(max-width:860px){
+  .nav-links,.nav-right{display:none}
+  .nav-toggle{display:block}
+}
 
 /* ---------- hero ---------- */
 .hero{
@@ -139,12 +211,7 @@
   background-image:radial-gradient(rgba(255,255,255,.5) .5px, transparent .5px);
   background-size:3px 3px;mix-blend-mode:overlay;
 }
-.hero .eyebrow{
-  color:rgba(255,255,255,.82);border:1px solid rgba(255,255,255,.24);
-  border-radius:999px;padding:.42rem 1rem;background:rgba(255,255,255,.07);
-  backdrop-filter:blur(6px);letter-spacing:.16em;
-}
-.hero .eyebrow::before{background:#8FC0FF;box-shadow:0 0 10px #8FC0FF}
+.hero .eyebrow{letter-spacing:.16em}
 .hero h1{
   max-width:15ch;margin:1.4rem auto 0;
   text-shadow:0 2px 40px rgba(0,10,30,.5);
@@ -163,7 +230,7 @@
 .btn:hover{transform:translateY(-2px);box-shadow:0 10px 30px rgba(0,0,0,.28)}
 .hero-ticks{
   display:flex;flex-wrap:wrap;gap:1.4rem;justify-content:center;margin-top:1.7rem;
-  font-family:var(--mono);font-size:.7rem;letter-spacing:.1em;color:rgba(255,255,255,.6);
+  font-family:var(--mono);font-size:.78rem;letter-spacing:.1em;color:rgba(255,255,255,.82);
 }
 .hero-ticks span::before{content:"✓ ";color:#8FC0FF}
 
@@ -180,12 +247,12 @@
 }
 .dot{width:9px;height:9px;border-radius:50%;background:rgba(255,255,255,.2)}
 .term-title{
-  flex:1;text-align:center;font-family:var(--mono);font-size:.63rem;letter-spacing:.2em;
-  color:rgba(255,255,255,.38);
+  flex:1;text-align:center;font-family:var(--mono);font-size:.68rem;letter-spacing:.2em;
+  color:rgba(255,255,255,.62);
 }
 .term-replay{
-  font-family:var(--mono);font-size:.63rem;letter-spacing:.1em;color:rgba(255,255,255,.5);
-  background:none;border:1px solid rgba(255,255,255,.16);border-radius:5px;padding:.24rem .6rem;
+  font-family:var(--mono);font-size:.68rem;letter-spacing:.1em;color:rgba(255,255,255,.72);
+  background:none;border:1px solid rgba(255,255,255,.16);border-radius:5px;padding:.5rem .8rem;
   cursor:pointer;transition:all .2s;
 }
 .term-replay:hover{color:#fff;border-color:rgba(255,255,255,.4)}
@@ -218,7 +285,7 @@
 .pane-out .ln{display:block}
 .pane-out .cmd{color:#fff}
 .pane-out .ps{color:#4C9BFF}
-.pane-out .dim{color:rgba(255,255,255,.36)}
+.pane-out .dim{color:rgba(255,255,255,.56)}
 .pane-out .ok{color:#6FCF97}
 .pane-out .warn{color:#E4B363}
 .pane-out .bad{color:#E8836B}
@@ -248,9 +315,10 @@
   border-top:1px solid rgba(255,255,255,.08);background:#0D1016;
 }
 .chip{
-  font-family:var(--mono);font-size:.68rem;color:rgba(255,255,255,.6);cursor:pointer;
+  display:inline-flex;align-items:center;min-height:44px;
+  font-family:var(--mono);font-size:.7rem;color:rgba(255,255,255,.76);cursor:pointer;
   background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.11);
-  border-radius:999px;padding:.28rem .7rem;transition:all .18s;
+  border-radius:999px;padding:.28rem .85rem;transition:all .18s;
 }
 .chip:hover{background:rgba(76,155,255,.16);border-color:rgba(76,155,255,.4);color:#cfe3ff}
 
@@ -262,157 +330,138 @@
 @media(prefers-reduced-motion:reduce){.caret{animation:none}}
 .term-cap{
   max-width:66ch;margin:1.4rem auto 0;padding:0 var(--pad) clamp(3.5rem,8vw,5.5rem);
-  font-size:.82rem;line-height:1.7;font-weight:300;color:rgba(255,255,255,.5);text-align:center;
+  font-size:.82rem;line-height:1.7;font-weight:300;color:rgba(255,255,255,.72);text-align:center;
 }
 
-/* ---------- spec bar ---------- */
+/* ---------- spec strip (closes the hero, in the hero's material) ---------- */
 .specs{
-  background:var(--white);border-top:1px solid var(--line);border-bottom:1px solid var(--line);
+  position:relative;border-top:1px solid var(--edge);border-bottom:1px solid var(--edge);
   display:grid;grid-template-columns:repeat(5,1fr);
+  background:rgba(5,10,26,.34);backdrop-filter:blur(14px);
 }
-.spec{padding:1.35rem var(--pad);border-right:1px solid var(--line-soft)}
+.spec{padding:1.25rem var(--pad);border-right:1px solid rgba(255,255,255,.08)}
 .spec:last-child{border-right:none}
-.spec dt{font-family:var(--mono);font-size:.58rem;letter-spacing:.2em;color:var(--ink-3);text-transform:uppercase}
-.spec dd{margin:.4rem 0 0;font-family:var(--mono);font-size:.82rem;color:var(--ink)}
-@media(max-width:900px){.specs{grid-template-columns:repeat(2,1fr)}.spec{border-bottom:1px solid var(--line-soft)}}
+.spec dt{font-family:var(--mono);font-size:.57rem;letter-spacing:.2em;color:var(--tx-dim);text-transform:uppercase}
+.spec dd{margin:.4rem 0 0;font-family:var(--mono);font-size:.82rem;color:#fff;font-variant-numeric:tabular-nums}
+@media(max-width:900px){.specs{grid-template-columns:repeat(2,1fr)}}
 @media(max-width:520px){.specs{grid-template-columns:1fr}}
 
-/* ---------- sticky section tabs ---------- */
-.tabs{position:sticky;top:0;z-index:40;background:var(--paper);border-bottom:1px solid var(--line)}
-.tabs-in{display:grid;grid-template-columns:repeat(4,1fr);gap:.5rem;padding:.85rem 0}
-.tab{
-  display:flex;gap:.6rem;align-items:center;padding:.55rem .85rem;border-radius:6px;
-  border:1px solid var(--line);font-family:var(--mono);font-size:.63rem;letter-spacing:.18em;
-  color:var(--ink-3);text-transform:uppercase;transition:all .25s ease;
+/* ---------- rail + body ---------- */
+/* Replaces the sticky tab bar, which parked itself behind the fixed nav.
+   262px with its own padding: at var(--pad) every label wrapped to two lines. */
+.body{display:grid;grid-template-columns:262px 1fr;max-width:1460px;margin:0 auto}
+/* 100dvh, not 100vh, so it does not jump with the iOS address bar */
+.rail{
+  position:sticky;top:66px;align-self:start;height:calc(100dvh - 66px);overflow-y:auto;
+  padding:2.6rem 0 2rem;border-right:1px solid var(--edge);
+}
+.rail-h{
+  font-family:var(--mono);font-size:.62rem;letter-spacing:.2em;text-transform:uppercase;
+  color:var(--tx-dim);padding:0 1.5rem;margin-bottom:1.1rem;
 }
-.tab:hover{border-color:rgba(11,11,13,.3);color:var(--ink)}
-.tab.active{background:var(--ink);border-color:var(--ink);color:var(--paper)}
-.tab i{font-style:normal;opacity:.5}
-@media(max-width:760px){.tabs-in{grid-template-columns:repeat(2,1fr)}}
-
-/* ---------- sections ---------- */
-section{padding:clamp(4.5rem,10vw,8rem) 0}
-.band-white{background:var(--white);border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
-.band-paper{background:var(--paper-2)}
-
-/* cards */
-.grid{display:grid;gap:1px;background:var(--line);border:1px solid var(--line)}
-.g3{grid-template-columns:repeat(3,1fr)}
-.g2{grid-template-columns:repeat(2,1fr)}
-.card{background:var(--white);padding:1.6rem 1.5rem}
-.band-white .card{background:var(--paper)}
-.card h3{margin-bottom:.7rem}
-.card p{font-size:.88rem;color:var(--ink-2);line-height:1.68;font-weight:300}
-.card .k{font-family:var(--mono);font-size:.75rem;color:var(--blue-ink);margin-top:.9rem;display:block;word-break:break-word}
-.card ul{margin:.4rem 0 0;padding:0;list-style:none}
-.card li{
-  font-size:.85rem;color:var(--ink-2);font-weight:300;line-height:1.6;padding-left:1rem;position:relative;margin-bottom:.5rem;
+/* .72 measured 3.91:1 at this size; inactive links need .86 */
+.rail a{
+  display:flex;gap:.7rem;align-items:baseline;padding:.7rem 1.5rem;color:rgba(255,255,255,.86);
+  white-space:nowrap;font-family:var(--mono);font-size:.8rem;
+  border-left:2px solid transparent;transition:color .22s,background .22s,border-color .22s;
 }
-.card li::before{content:"";position:absolute;left:0;top:.62em;width:4px;height:4px;background:var(--blue);opacity:.6}
-.card code,li code,p code{font-family:var(--mono);font-size:.8em;background:rgba(27,111,224,.08);color:var(--blue-ink);padding:.1em .3em;border-radius:3px}
-@media(max-width:880px){.g3{grid-template-columns:1fr}.g2{grid-template-columns:1fr}}
-
-/* address anatomy */
-.addr{border:1px solid var(--line);background:var(--white);padding:clamp(1.6rem,4vw,2.4rem)}
-.addr-str{font-family:var(--mono);font-size:clamp(1.05rem,3.2vw,1.7rem);letter-spacing:-.02em;margin-bottom:1.8rem}
-.addr-str .h{color:var(--blue);border-bottom:2px solid var(--blue);padding-bottom:2px}
-.addr-str .o{color:var(--amber);border-bottom:2px solid var(--amber);padding-bottom:2px}
-.addr-str .r{color:var(--ink-3)}
-.addr-defs{display:grid;grid-template-columns:repeat(3,1fr);gap:1.6rem}
-.addr-defs dt{font-family:var(--mono);font-size:.6rem;letter-spacing:.2em;color:var(--ink);margin-bottom:.5rem}
-.addr-defs dd{margin:0;font-size:.85rem;color:var(--ink-2);font-weight:300;line-height:1.65}
-@media(max-width:760px){.addr-defs{grid-template-columns:1fr}}
-
-/* callout */
-.callout{border-left:2px solid var(--amber);padding:1.1rem 1.3rem;background:rgba(154,107,20,.05);margin-top:1.6rem;max-width:74ch}
-.callout.blue{border-left-color:var(--blue);background:rgba(27,111,224,.05)}
-.callout h4{
-  margin:0 0 .5rem;font-family:var(--mono);font-size:.63rem;letter-spacing:.18em;text-transform:uppercase;color:var(--amber);font-weight:500;
+.rail a:hover{color:#fff;background:rgba(255,255,255,.045)}
+.rail a.on{color:#fff;border-left-color:var(--accent);background:rgba(143,192,255,.10)}
+.rail a i{font-style:normal;font-size:.72rem;color:rgba(255,255,255,.6)}
+
+.main{min-width:0}
+section{padding:clamp(4rem,7vw,6.5rem) var(--pad);border-bottom:1px solid rgba(255,255,255,.07)}
+h2{max-width:19ch;text-wrap:balance;text-shadow:0 2px 40px rgba(0,10,30,.45)}
+
+/* ---------- artifacts (glass over the sky, never paper) ---------- */
+.glass{
+  margin-top:2.6rem;background:var(--glass);border:1px solid var(--edge);border-radius:var(--r-lg);
+  backdrop-filter:blur(20px) saturate(1.3);overflow:hidden;
+  box-shadow:0 1px 0 0 rgba(255,255,255,.07) inset,0 40px 84px -42px rgba(0,6,25,.9);
 }
-.callout.blue h4{color:var(--blue-ink);font-family:var(--display);font-size:1rem;letter-spacing:-.01em;text-transform:none;font-weight:600}
-.callout p{font-size:.87rem;color:var(--ink-2);font-weight:300;line-height:1.7}
-
-/* wire sequence */
-.wire{border:1px solid var(--line);background:var(--white)}
-.wire-row{display:grid;grid-template-columns:44px 148px 1fr;gap:1rem;padding:1rem 1.3rem;border-bottom:1px solid var(--line-soft)}
-.wire-row:last-child{border-bottom:none}
-.wire-row .n{font-family:var(--mono);font-size:.68rem;color:var(--ink-3)}
-.wire-row .lane{font-family:var(--mono);font-size:.63rem;letter-spacing:.11em;color:var(--ink-3);line-height:1.5}
-.wire-row .f{font-family:var(--mono);font-size:.82rem;color:var(--ink);display:block;margin-bottom:.25rem}
-.wire-row .d{font-size:.85rem;color:var(--ink-2);font-weight:300;line-height:1.6}
-@media(max-width:760px){.wire-row{grid-template-columns:36px 1fr;gap:.6rem}.wire-row .lane{grid-column:2}}
-
-/* limits strip */
-.limits{display:grid;grid-template-columns:repeat(5,1fr);border:1px solid var(--line);background:var(--white);margin-top:1.6rem}
-.limit{padding:1.3rem 1.2rem;border-right:1px solid var(--line-soft)}
-.limit:last-child{border-right:none}
-.limit dt{font-family:var(--mono);font-size:.56rem;letter-spacing:.2em;color:var(--ink-3);text-transform:uppercase}
-.limit dd{margin:.45rem 0 0;font-family:var(--mono);font-size:.85rem}
-@media(max-width:900px){.limits{grid-template-columns:repeat(2,1fr)}.limit{border-bottom:1px solid var(--line-soft)}}
-@media(max-width:480px){.limits{grid-template-columns:1fr}}
-
-/* lens layout */
-.lens{display:grid;grid-template-columns:300px 1fr;gap:clamp(2rem,5vw,4rem);margin-bottom:clamp(3.5rem,8vw,6rem)}
-.lens:last-child{margin-bottom:0}
-.lens-tag{
-  display:inline-block;font-family:var(--mono);font-size:.6rem;letter-spacing:.18em;text-transform:uppercase;
-  border:1px solid var(--line);padding:.35rem .7rem;margin-bottom:1.1rem;color:var(--ink-2);
+.g-h{display:flex;align-items:center;gap:.55rem;padding:.68rem 1.1rem;border-bottom:1px solid var(--edge);background:rgba(255,255,255,.045)}
+.g-h .d{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,.2)}
+.g-h b{margin-left:.5rem;font-family:var(--mono);font-size:.6rem;letter-spacing:.18em;color:var(--tx-dim);font-weight:400}
+.g-h .r{margin-left:auto;font-family:var(--mono);font-size:.61rem;color:var(--accent);font-variant-numeric:tabular-nums}
+/* real 
, so the transcript is selectable text rather than a picture of it */
+.g-b{
+  margin:0;padding:1.3rem 1.45rem;font-family:var(--mono);font-size:.79rem;line-height:2;
+  white-space:pre-wrap;word-break:break-word;
 }
-.lens-aside h3{font-family:var(--display);font-size:clamp(1.5rem,2.6vw,2rem);line-height:1.12;margin-bottom:1rem}
-.lens-aside p{font-size:.9rem;color:var(--ink-2);font-weight:300;line-height:1.72;margin-bottom:.9rem}
-@media(max-width:900px){.lens{grid-template-columns:1fr}}
-
-/* layers */
-.layer{
-  display:grid;grid-template-columns:250px 1fr;gap:1.4rem;padding:1.4rem 1.5rem;
-  border:1px solid var(--line);border-left-width:3px;background:var(--white);margin-bottom:.55rem;
+.g-b+.g-b{padding-top:0}
+.g-b .ps{color:var(--accent)}
+.g-b .dim{color:var(--tx-dim)}
+.g-b .ok{color:var(--ok)}
+.g-b .bad{color:var(--bad)}
+.g-b .wa{color:var(--warn)}
+.say{
+  display:block;margin:0 1.45rem;padding:1rem 1.2rem;border-left:2px solid var(--accent);
+  background:rgba(143,192,255,.09);font-family:var(--sans);font-size:.94rem;line-height:1.74;
+  color:rgba(255,255,255,.93);
 }
-.layer:nth-child(1){border-left-color:var(--amber)}
-.layer:nth-child(2){border-left-color:var(--blue)}
-.layer:nth-child(3){border-left-color:var(--green)}
-.layer:nth-child(4){border-left-color:var(--red)}
-.layer h3{font-size:1.02rem;margin-bottom:.45rem}
-.layer .path{font-family:var(--mono);font-size:.7rem;color:var(--ink-3);word-break:break-all;line-height:1.5}
-.layer p{font-size:.87rem;color:var(--ink-2);font-weight:300;line-height:1.7}
-@media(max-width:760px){.layer{grid-template-columns:1fr;gap:.7rem}}
-
-/* security table */
-.tbl{width:100%;border-collapse:collapse;border:1px solid var(--line);background:var(--white);text-align:left}
-.tbl th{
-  font-family:var(--mono);font-size:.58rem;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-3);
-  font-weight:400;padding:.9rem 1.1rem;border-bottom:1px solid var(--line);vertical-align:bottom;
+.caption{margin-top:.9rem;font-size:.83rem;line-height:1.7;color:var(--tx-dim);max-width:70ch}
+
+/* the grant surface, shown as the file it actually is */
+.file-h{padding:.62rem 1.1rem;background:rgba(255,255,255,.045);border-bottom:1px solid var(--edge);font-family:var(--mono);font-size:.67rem;color:var(--tx-dim)}
+.file-b{display:grid;grid-template-columns:46px 1fr}
+/* absolute line-height on both: the gutter is .69rem and the code .78rem, so a
+   shared unitless 2.1 computes to 23.2px vs 26.2px and drifts out of register */
+.gutter{
+  background:rgba(0,0,0,.16);border-right:1px solid var(--edge);padding:1.1rem 0;
+  font-family:var(--mono);font-size:.69rem;color:rgba(255,255,255,.5);text-align:right;line-height:26px;
 }
-.tbl td{padding:1rem 1.1rem;border-bottom:1px solid var(--line-soft);vertical-align:top;font-size:.84rem;color:var(--ink-2);font-weight:300;line-height:1.6}
-.tbl tr:last-child td{border-bottom:none}
-.tbl .surf b{display:block;color:var(--ink);font-weight:500;font-size:.88rem;margin-bottom:.2rem}
-.tbl .surf span{font-family:var(--mono);font-size:.7rem;color:var(--ink-3)}
-.tag{
-  display:block;width:max-content;font-family:var(--mono);font-size:.58rem;letter-spacing:.11em;padding:.18rem .45rem;
-  border-radius:3px;margin-bottom:.5rem;
+.gutter span{display:block;padding-right:.8rem}
+.code{padding:1.1rem 1.3rem;font-family:var(--mono);font-size:.78rem;line-height:26px;overflow-x:auto}
+.code .k{color:#C7A6FF}
+.code .s{color:var(--ok)}
+.code .c{color:rgba(255,255,255,.6)}
+.code .p{color:var(--accent)}
+
+/* Three plain lines where a four-layer "permission model" used to be. The
+   layers read as an IT procurement doc and put a wall in front of the
+   individual dev who is the one actually installing this. Docs carry the model. */
+.facts{margin:1.8rem 0 0;padding:0;list-style:none;display:grid;gap:.9rem;max-width:62ch}
+.facts li{position:relative;padding-left:1.5rem;color:var(--tx);font-size:.97rem;line-height:1.7}
+.facts li::before{content:"";position:absolute;left:0;top:.66em;width:7px;height:1.5px;background:var(--accent)}
+.more{margin-top:1.8rem}
+.more a{font-size:.9rem;color:var(--accent);border-bottom:1px solid rgba(143,192,255,.35);padding-bottom:2px;transition:border-color .2s}
+.more a:hover{border-bottom-color:var(--accent)}
+.more a::after{content:" \2192"}
+
+.kv{display:grid;grid-template-columns:repeat(5,1fr)}
+.kv div{padding:1.3rem 1.2rem;border-right:1px solid var(--edge)}
+.kv div:last-child{border-right:0}
+.kv dt{font-family:var(--mono);font-size:.55rem;letter-spacing:.2em;text-transform:uppercase;color:var(--tx-dim)}
+.kv dd{margin:.45rem 0 0;font-family:var(--mono);font-size:.85rem;font-variant-numeric:tabular-nums}
+
+.honest{margin-top:2.6rem;display:grid;gap:.55rem}
+.hr-row{
+  display:grid;grid-template-columns:130px 1fr;gap:1.2rem;align-items:baseline;padding:1.15rem 1.3rem;
+  border-radius:var(--r-sm);background:var(--glass-2);border:1px solid var(--edge);
+}
+.bg{font-family:var(--mono);font-size:.56rem;letter-spacing:.14em;text-transform:uppercase;padding:.28rem .5rem;border-radius:4px;text-align:center}
+.b-ok{background:rgba(123,216,166,.16);color:var(--ok)}
+.b-mid{background:rgba(228,179,99,.16);color:var(--warn)}
+.b-no{background:rgba(255,156,138,.16);color:var(--bad)}
+.hr-row b{display:block;font-weight:500;font-size:.94rem;margin-bottom:.26rem}
+.hr-row p{color:var(--tx);font-size:.87rem;line-height:1.68}
+
+@media(max-width:900px){
+  .body{grid-template-columns:1fr}
+  .rail{
+    position:static;height:auto;border-right:0;border-bottom:1px solid var(--edge);
+    display:flex;gap:.25rem;overflow-x:auto;padding:.75rem .6rem;
+  }
+  .rail-h{display:none}
+  .rail a{border-left:0;border-radius:8px;white-space:nowrap;padding:.6rem .9rem;min-height:44px;align-items:center}
+  .rail a.on{border-left:0}
+  .kv{grid-template-columns:repeat(2,1fr)}
+  .file-b{grid-template-columns:1fr}
+  .gutter{display:none}
+  .hr-row{grid-template-columns:1fr;gap:.55rem}
+  .bg{justify-self:start}
 }
-.tag.ok{background:rgba(46,125,91,.12);color:var(--green)}
-.tag.warn{background:rgba(154,107,20,.13);color:var(--amber)}
-.tag.bad{background:rgba(168,58,50,.11);color:var(--red)}
-.tbl-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
-@media(max-width:820px){.tbl{min-width:720px}}
-
-/* status */
-.status{display:grid;grid-template-columns:repeat(3,1fr);gap:clamp(1.6rem,4vw,3rem)}
-.status h3{font-family:var(--display);font-size:1.15rem;padding-bottom:.6rem;border-bottom:2px solid;margin-bottom:1.1rem}
-.status .ok h3{color:var(--green);border-color:var(--green)}
-.status .mid h3{color:var(--amber);border-color:var(--amber)}
-.status .no h3{color:var(--red);border-color:var(--red)}
-.status ul{list-style:none;margin:0;padding:0}
-.status li{position:relative;padding-left:1rem;margin-bottom:.75rem;font-size:.86rem;color:var(--ink-2);font-weight:300;line-height:1.6}
-.status li::before{content:"";position:absolute;left:0;top:.58em;width:5px;height:5px}
-.status .ok li::before{background:var(--green)}
-.status .mid li::before{background:var(--amber)}
-.status .no li::before{background:var(--red)}
-.status li b{color:var(--ink);font-weight:500}
-.status li .iss{font-family:var(--mono);font-size:.7rem;color:var(--ink-3)}
-@media(max-width:880px){.status{grid-template-columns:1fr}}
-
-.note{margin-top:3rem;font-size:.82rem;color:var(--ink-3);font-weight:300;max-width:62ch;line-height:1.7}
+@media(max-width:520px){.kv{grid-template-columns:1fr}}
 
 /* ---------- closing CTA ---------- */
 .close{
@@ -433,8 +482,13 @@
   font-family:var(--display);font-size:clamp(7rem,21vw,17rem);color:rgba(255,255,255,.045);
   white-space:nowrap;letter-spacing:-.04em;pointer-events:none;user-select:none;
 }
-.close .eyebrow{color:rgba(255,255,255,.6)}
-.close .eyebrow::before{background:#8FC0FF}
+/* The dark half uses the glass variant of the same pill. Without this it
+   inherits the light pill and renders white text on a white background. */
+.hero .eyebrow,.close .eyebrow{
+  color:rgba(255,255,255,.82);border-color:rgba(255,255,255,.24);
+  background:rgba(255,255,255,.07);backdrop-filter:blur(6px);box-shadow:none;
+}
+.hero .eyebrow::before,.close .eyebrow::before{background:#8FC0FF;box-shadow:0 0 10px #8FC0FF}
 .close h2{max-width:16ch;margin:0 auto}
 .close p{margin-top:1.2rem;color:rgba(255,255,255,.72);font-weight:300}
 .close .hero-cta{margin-top:2.2rem}
@@ -448,7 +502,7 @@
   font:400 .92rem var(--sans);outline:none;
 }
 .waitlist-form input[type="email"]::placeholder{color:rgba(255,255,255,.5)}
-.waitlist-form button{border:0;cursor:pointer;white-space:nowrap}
+.waitlist-form button{border:0;cursor:pointer;white-space:nowrap;min-height:44px}
 .waitlist-form button:disabled{cursor:wait;opacity:.65;transform:none}
 .waitlist-status{min-height:1.5rem;margin:.75rem auto 0;color:rgba(255,255,255,.82);font-size:.84rem}
 .waitlist-status.success{color:#A9E8C5}
@@ -462,21 +516,33 @@
 }
 
 /* ---------- footer ---------- */
-footer{background:var(--paper);padding:clamp(3rem,6vw,4.5rem) 0 2rem;border-top:1px solid var(--line)}
+/* Dark, like everything else. A cream footer under a dark CTA was the one
+   place the page still flipped theme mid-scroll. */
+footer{background:rgba(4,9,24,.55);padding:clamp(3rem,6vw,4.5rem) 0 2rem;border-top:1px solid var(--edge)}
 .foot{display:grid;grid-template-columns:1.6fr repeat(3,1fr);gap:2rem}
-.foot p{font-size:.84rem;color:var(--ink-2);font-weight:300;max-width:34ch;margin-top:1rem;line-height:1.7}
-.foot h4{font-family:var(--mono);font-size:.58rem;letter-spacing:.2em;text-transform:uppercase;color:var(--ink-3);margin:0 0 1rem;font-weight:400}
+.foot p{font-size:.84rem;color:var(--tx);font-weight:300;max-width:34ch;margin-top:1rem;line-height:1.7}
+.foot h4{font-family:var(--mono);font-size:.58rem;letter-spacing:.2em;text-transform:uppercase;color:var(--tx-dim);margin:0 0 1rem;font-weight:400}
 .foot ul{list-style:none;margin:0;padding:0}
 .foot li{margin-bottom:.55rem}
-.foot li a{font-size:.86rem;color:var(--ink-2);transition:color .2s}
-.foot li a:hover{color:var(--ink)}
+.foot li a{font-size:.86rem;color:var(--tx);transition:color .2s}
+.foot li a:hover{color:#fff}
 .foot-base{
   display:flex;justify-content:space-between;gap:1rem;flex-wrap:wrap;margin-top:3rem;padding-top:1.4rem;
-  border-top:1px solid var(--line);font-family:var(--mono);font-size:.63rem;letter-spacing:.06em;color:var(--ink-3);
+  border-top:1px solid var(--edge);font-family:var(--mono);font-size:.63rem;letter-spacing:.06em;color:var(--tx-dim);
 }
 @media(max-width:880px){.foot{grid-template-columns:1fr 1fr}}
 @media(max-width:520px){.foot{grid-template-columns:1fr}}
 
+/* ---------- touch targets ---------- */
+@media(max-width:860px){
+  .brand{min-height:44px}
+  .term-replay{min-height:44px}
+  .foot li{margin-bottom:0}
+  .foot li a{display:flex;align-items:center;min-height:44px}
+  .foot-base a{display:inline-flex;align-items:center;min-height:44px}
+  .more a{display:inline-flex;align-items:center;min-height:44px}
+}
+
 /* ---------- motion ---------- */
 .rise{opacity:1;transform:none}
 .js .rise{opacity:0;transform:translateY(18px);transition:opacity .7s cubic-bezier(.2,.7,.3,1),transform .7s cubic-bezier(.2,.7,.3,1)}
@@ -489,6 +555,8 @@
 
 
 
+
+
+
@@ -561,336 +642,84 @@

Your agents should know who to call.

Enrollment
Invite only
- - - -
-
-
- Identity -

An address is a routing label, not a secret.

-

Handles are unique inside an organization, not globally. Acme and Beta can both have a ken. Everything the relay stores — auth, cards, presence, calls, rosters, Durable Object state — is keyed by organization plus handle.

-
- -
-
ken@acme.example.com
-
-
Handle
The person. Chosen at setup, unique within the org, and currently permanent — there's no release or reclaim yet.
-
Organization
The reachability boundary. Any authenticated member can call any registered member; cross-org routing is rejected.
-
Relay host
Which relay serves that org. A self-hosted relay uses its own hostname instead.
-
-
- -
-

Read this as a limit, not a feature

-

Because the org is the boundary, treat every colleague as able to reach your agent, and grant tasks accordingly. Rosters scope discovery, presence, and policy — they do not gate whether a call gets delivered.

-
-
-
- - -
-
-
- The call path -

Three machines. One socket each.

-

Nothing is polled. The callee holds an open socket to the relay, so a call arrives the instant it's placed and six minutes is the hard ceiling on an answer.

-
- -
-
- Caller's machine -

Your agent, or you

-
    -
  • agentcall call ken "…"
  • -
  • Resolves saved contacts
  • -
  • Prints the reply to stdout, status to stderr
  • -
  • Neutralizes terminal escapes from the remote agent
  • -
-
-
- Cloudflare -

The relay

-
    -
  • Hono Worker — auth, register, cards, rosters
  • -
  • HandleDO — one Durable Object per handle, holds both sockets
  • -
  • D1 — handles, invites, cards, rosters, audit events
  • -
  • Attests shared rosters; the caller can't claim them
  • -
-
-
- Callee's machine -

The listener

-
    -
  • agentcall listen via launchd or systemd
  • -
  • Resolves policy → task → capability envelope
  • -
  • Spawns a fresh one-shot agent in the task's workdir
  • -
  • Appends every call to ~/.agentcall/calls.log
  • -
-
-
- -

Wire sequence

-
-
01CALLER →
RELAY
call_requestRouting metadata plus a signed HPKE envelope. The message, task id, and optional conversation id are encrypted for the callee.
-
02RELAY →
CALLER
call_status ringingRate limit and 64 KB size check pass; a call record is written with a deadline.
-
03RELAY →
LISTENER
incoming_callCarries the ciphertext, verified routing identity, and roster ids the relay observed both sides sharing.
-
04LISTENER →
RELAY
call_acceptedAfter decryption, signature verification, replay protection, and policy admission, the job is owned. Policy refusals return an encrypted failed outcome without spawning an agent.
-
05LISTENER →
RELAY
call_startedThe agent process is up, scoped to the task's tools and working directory.
-
06LISTENER →
RELAY
call_outcomeAn authenticated encrypted reply or peer failure, plus a conversation id when the task permits a follow-up.
-
07RELAY →
CALLER
call_outcomeThe relay routes ciphertext. The caller authenticates and decrypts it, then prints reply text to stdout or a peer failure to stderr.
-
- -
-

What the relay can still see

-

End-to-end encryption hides prompts, replies, task content, and peer failure details from the relay. Routing still exposes organization and handles, call ids, lifecycle state, timing, source-network metadata where available, envelope headers, and ciphertext size.

-
- -
-
Agent kill timer
5 min
-
Relay hard deadline
6 min
-
Concurrency
1 running · 0 queued
-
Rate limit
30 / hour
-
Payload
64 KB in · 256 KB out
-
-
-
- - -
-
-
- Repository -

Three packages. One source of truth.

-

A pnpm workspace in TypeScript and ESM. Both sides of every call agree on shapes defined once in Zod, in packages/shared — the relay and the CLI import them rather than redeclaring them.

-
-
-
-

packages/shared

-

Every wire frame, every limit, every regex for a handle or task id. Changing a frame shape starts here or it doesn't happen. Also holds the A2A card mapping and the terminal-sanitizing helpers.

- @benree/agentcall-shared -
-
-

apps/relay

-

Cloudflare Worker (Hono) plus two Durable Object classes — HandleDO for call routing and presence, RateLimiterDO for IP-keyed limits — over a D1 database of handles, invites, cards, rosters, and audit events.

- wrangler deploy -
-
-

packages/cli

-

The whole product surface for a human: setup, call, listen, doctor, policy, card, roster, search, contacts, and task management. Also the tool guard that checks a spawned agent's tool attempts.

- npm install -g @benree/agentcall -
-
-
-
- - -
-
-
- Three lenses -

Useful to employees. Controllable by IT. Built to ship.

-
- -
-
- Lens 1 — the employee -

What a person actually does with it

-

One install, one address, then the vocabulary of a phone: call, check if they're in, look at what they offer, save a number, follow up.

-

Answering costs the callee's own model subscription, and their agent answers with their real context — that's the entire point, and also the reason the permission model below exists.

-
-
-

Ask someone's agent

Sends the message, waits, prints the reply. Add --json when your own agent is the one parsing it.

agentcall call ken "why did CI fail on main?"
-

Follow up without repeating yourself

Resumes the last open conversation with that address. Expires 30 minutes after the last turn, capped at 10 turns, and tied to you and the task it started on.

agentcall call ken "which commit?" --continue
-

Find who to ask

Search by what you need instead of who you know. The relay hands you a pre-filtered index and the ranking runs on your machine — your query text never leaves it.

agentcall search "why did we pick this auth migration"
-

Decide what you'll answer

Each task is one SKILL.md: a description, keywords, and the tools it may use. Offer it to everyone, grant it to one person, or refuse someone outright.

agentcall offer schedule-meeting · agentcall block spammer
-

See your own menu the way callers see it

Renders the effective policy after any IT ceiling, plus each runnable task's capabilities and the concrete directory it answers from.

agentcall policy · agentcall card
-

Fix it when calls stop arriving

Checks the binary, auth, agent spawn, tool-guard telemetry, the listener, and a round-trip call to yourself. ! means unproven this run, not broken.

agentcall doctor
-
-
- -
-
- Lens 2 — IT -

What the department controls

-

The design assumption is that an employee's agent is a company endpoint. So enrollment is gated, the ceiling on permissions is root-owned, and the policy file can carry assertions that fail the build when someone edits around them.

-

Some of what IT will eventually want — SSO, SCIM, and an admin console — is still open work. Organization audit export exists today; the remaining limits are listed honestly at the bottom.

-
-
-

Nobody enrolls themselves

The npm package is public, but organization enrollment is not. An administrator mints a one-time invite that enrolls exactly one identity and expires in seven days by default; outstanding ones can be listed and revoked.

agentcall invite create --description contractor
-

A ceiling users cannot raise

A root-owned file at /Library/Application Support/agentcall/policy.json caps which tasks may ever be offered and adds blocks that can't be undone locally. Unreadable or malformed means no agent spawns at all.

allowed_tasks · blocked_callers
-

Policy you can prove, not just write

Both the user's file and the managed file can carry assertions — this caller gets these tasks, that one gets nothing. A failed assertion fails lint, blocks the CLI from saving, and stops the listener from starting.

agentcall lint
-

Group grants the caller can't fake

Roster names are local labels. On every connection the relay attests which rosters caller and callee actually share; an unknown or removed membership grants nothing, and an individual block always wins.

agentcall roster expel acme <handle>
-

Two audit trails

Locally, every call — accepted or refused — is a JSONL line with caller, task, status, and duration. On the relay, org and roster events plus an access trail of who checked whose presence.

~/.agentcall/calls.log · ~/.agentcall/tools.log
-

A version you can pin

In-product self-update stays disabled so a managed deployment can't be bypassed. Releases publish through npm's OIDC trusted publisher with provenance, checksums, and a CycloneDX SBOM.

release.yml · environment: npm
-
-
- -
-
- Lens 3 — us -

How the work is run

-

Built test-first and kept that way: schema round-trips in shared, Durable Object relay logic against fake sockets, CLI tests with mocked ws and fs. No live agent spawns in CI.

-

Open work lives in GitHub Issues, never in a markdown TODO. The assignee is the claim. Everything under docs/superpowers/ is a dated historical record — right about why a decision was made, wrong about what the code does now.

-
-
-

The gate before anything ships

Build first, deliberately — the CLI typechecks against shared's built output, so running build last would check the previous run's types. CI runs exactly this order.

pnpm -r build && pnpm -r typecheck && pnpm -r test
-

Protocol changes start in shared

New field, new frame, new limit — the Zod schema moves first, then the relay and CLI are updated to match. No local redeclaration on either side.

packages/shared/src/protocol.ts
-

Typecheck covers tests too

Each package's typecheck includes test/. Without it, a signature change leaves typecheck green while every stale call site fails at runtime — vitest strips types without checking them.

tsconfig.test.json
-

Relay deploys are not reversible

Durable Object lifecycle changes are atomic and can't be rolled back, and a dry run doesn't compare state with Cloudflare. Compare the exports map against the intended live classes first.

wrangler deploy --dry-run
-
-
-
-
- - -
-
-
- The permission model -

Four layers. One direction.

-

They compose one way only: IT sets the ceiling, the owner grants under it, the task declares an envelope, and the guard checks each tool call at the moment it runs.

-
- -
-
-

1 · Administrator ceiling

/Library/Application Support/agentcall/policy.json
-

Filters every default, per-caller, and per-group grant down to allowed_tasks, and unions its blocked_callers into the owner's blocks. Absent means unmanaged; broken means fail closed.

-
-
-

2 · Owner policy

~/.agentcall/policy.json
-

A default offer, per-caller grants and blocks, and roster-group grants keyed by opaque roster id. Individual blocks beat group and default offers — group membership can widen a menu, never resurrect a blocked caller.

-
-
-

3 · Task envelope

~/AgentCall/<line>/tasks/<id>/SKILL.md
-

Frontmatter declares tools from read / write / fetch / exec, an optional absolute workdir, a timeout, and whether follow-ups are allowed. Claude maps this to --allowedTools; Codex maps it to a --sandbox level.

+
+ + +
+
+

The answer is on someone else's machine.

+

Half of what your team knows was never written down. Instead of interrupting a colleague, your agent asks theirs, and gets the answer from their real setup.

+ +
+
ONE CALL, START TO FINISH13.2s · 412 B
+
$ agentcall call ken "why did CI fail on main after the auth migration?"
+ringing   ken@acme.example.com
+answered  agent running on ken's machine
+

The migration renamed session_token but the fixture still seeds the old column, so every auth test 500s on a fresh database. It passes locally because your dev DB was never re-seeded.

+
ALLOW Grep  src/auth/**
+ALLOW Read  auth.fixture.ts
+DENY  Read  ~/.ssh/id_ed25519  
-
-

4 · Tool guard

runs in-process on every tool call
-

For a Claude answering agent, file-shaped tools are refused outside the resolved workdir and at credential paths — ~/.ssh, ~/.aws, .env, Keychains, ~/.agentcall, ~/.claude, ~/.codex, the guard's own code, the tasks directory, LaunchAgents, and shell startup files. Every attempt is logged.

+

Real commands, representative output. Timing is from one recorded call.

+
+ +
+

You decide what your agent answers.

+

One file per task: what it does, which tools it can touch, who can ask. That's the whole thing.

+ +
+
~/.agentcall/tasks/repo-context/SKILL.md
+
+
1234567
+
--- the whole grant surface, in one file
+name: repo-context
+description: "Answer questions about this repo's history"
+tools: [Read, Grep, Glob]
+workdir: ~/work/api
+allowed_callers: [alex, priya]
+blocked_callers: [spammer]
+
-
- -
-

The invariant worth understanding

-

Which task a caller may invoke is decided from the relay-verified caller handle and local files before their message is placed into any prompt. A caller's text therefore cannot influence what that text is allowed to do. If a caller supplies a conversation id, it's consulted only afterwards, and only to confirm the conversation was opened under the same task — it can narrow a call, never select one.

-
-
-
- - -
-
-
- Security model, stated plainly -

What is a boundary — and what only looks like one.

-

There is no OS-level sandbox — that was removed on purpose, because the answering agent is meant to be the owner's real agent with the owner's real context. Enforcement is capability scoping plus the guard. Here is where that holds and where it doesn't.

-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
SurfaceClaude answering agentCodex answering agent
File reads via toolsRead / Grep / Glob / LSBOUNDEDConfined to the resolved task workdir, canonicalized paths and symlinks included.NO FLOORCodex has no such tools; it reads through the shell, and --sandbox read-only still reads ~/.ssh.
Writes to executable config~/.claude, LaunchAgents, shell rcREFUSEDIncluding the guard's own package root, so a write can't neuter the next call's guard.NOT ENFORCEDThe guard runs in observe-only mode.
Shell executiona task granting execRECORDED ONLYCommands are logged, not blocked. Pattern-matching a command string is too weak to be a boundary.RECORDED ONLYSame, on the pinned release where the session hook is trusted.
Tool telemetry~/.agentcall/tools.logCOMPLETEEvery tool call reaching the guard is written.PARTIALOnly what emits PreToolUse. view_image and apply_patch read files and appear in no log at all.
Outbound databundled connectorsSCOPED BY TOOLSfetch is a declared capability a task either has or doesn't.REACHABLECodex's own codex_apps connector works in this spawn shape and can publish a site — that traffic isn't the shell's, so --sandbox doesn't touch it.
Message confidentialitycaller → calleeEND-TO-END ENCRYPTEDSigned HPKE envelopes protect prompts, replies, task selection, conversation ids, and peer failure details. The relay still sees routing and traffic metadata.
-
-
-

How to read the table

-

Capability scoping bounds what kind of action is possible, not where. The real control on a task that grants exec is which tasks you choose to write. Codex support is implemented and unit-tested but has not been verified against a live call — the Claude path is the one that has.

-
-
-
- - -
-
-
- Status -

Where the project actually stands.

-

AgentCall is pre-production software for trusted teams. Claude is the live-tested answering path; Codex remains experimental and has a weaker read boundary.

-
- -
-
-

Working today

-
    -
  • Calls end to end on the Claude path, live-tested
  • -
  • Signed end-to-end encrypted calls with identity pinning and replay protection
  • -
  • Invite-only enrollment with listing and revocation
  • -
  • Task, policy, and managed-ceiling model with assertions enforced at lint, save, and call time
  • -
  • Rosters, join keys, and local-ranked search
  • -
  • Multi-turn conversations with TTL, turn cap, and task binding
  • -
  • Tool guard and telemetry for Claude, observe-only for pinned Codex
  • -
  • macOS and Linux listeners, plus a customer-owned relay deployment path
  • -
  • Organization audit export with retention policy and legal holds
  • -
  • Signed npm releases with provenance and SBOM
  • -
-
-
-

Open, being worked

-
    -
  • Codex read floor via its own deny_read #2
  • -
  • Stable identity cutover separate from the reclaimable handle address #154
  • -
  • Automated audit expiry and end-to-end erasure
  • -
  • Hosted enterprise controls beyond today's CLI and audit APIs
  • -
-
-
-

Not there yet

-
    -
  • No native Windows listener. Native services support macOS and Linux today
  • -
  • Handles can't be released or reclaimed yet #16 · #154
  • -
  • No SSO, SCIM, or admin console #15 · #17 · #27
  • -
  • No OS-level sandbox, egress allowlist, or governed nested delegation chain
  • -
  • One caller can monopolize your agent — 30 long calls exceed an hour of listener time; the remedy is block
  • -
+
    +
  • Callers can narrow the job. They can't pick a different one.
  • +
  • Anything outside that folder is refused, and logged.
  • +
  • If your company runs a relay, IT can cap what's offerable.
  • +
+

The permission model, in full

+
+ +
+

It costs them nothing but compute.

+

Their agent answers on their machine, on their own subscription. They're never interrupted and never asked to approve anything.

+
+
Agent kill timer
5 min
+
Relay deadline
6 min
+
Concurrency
1 · 0 queued
+
Rate limit
30 / hour
+
Payload
64 KB / 256 KB
+
+
+ +
+

Pre-production, and specific about it.

+

Exactly what has and hasn't been run against a live call.

+
+
Live-tested
Claude

Run end to end against real calls, with the guard enforcing on every tool attempt.

+
Unverified
Codex

Built and unit-tested, never tried against a live call. Its read boundary is weaker.

+
Removed
The OS sandbox

Gone on purpose. Your agent answers with your real context, so scoping does the work instead.

- - -

Generated from the repository at v0.4.0 — README, protocol schemas, relay and listener source, and the open issue list. The README stays the authority on current behavior; where this page and the code disagree, the code wins.

- -
+

Limitations, in full

+ + + -
@@ -1007,15 +836,47 @@

Project

var nav=document.getElementById('nav'); var hero=document.getElementById('top'); + var term=document.getElementById('term'); + + /* Pin before the terminal card reaches the bar, not at the end of the hero — + otherwise the transparent nav scrolls across the dark demo and its links + land on top of the pane headers. */ + function pinAt(){ + var heroEnd = hero.offsetHeight - 90; + if(!term) return heroEnd; + var termTop = term.getBoundingClientRect().top + window.scrollY; + return Math.min(heroEnd, termTop - nav.offsetHeight - 8); + } function onScroll(){ - var over = window.scrollY < hero.offsetHeight - 90; + var over = window.scrollY < pinAt(); nav.classList.toggle('over-hero', over); nav.classList.toggle('pinned', !over); } onScroll(); window.addEventListener('scroll', onScroll, {passive:true}); + var navToggle=document.getElementById('nav-toggle'); + var navMenu=document.getElementById('nav-menu'); + function setMenu(open){ + navMenu.classList.toggle('open', open); + navToggle.setAttribute('aria-expanded', String(open)); + navToggle.setAttribute('aria-label', open ? 'Close menu' : 'Open menu'); + document.body.classList.toggle('menu-open', open); + } + navToggle.addEventListener('click', function(){ + setMenu(navMenu.classList.contains('open') === false); + }); + navMenu.addEventListener('click', function(e){ + if(e.target.closest('a')) setMenu(false); + }); + document.addEventListener('keydown', function(e){ + if(e.key === 'Escape' && navMenu.classList.contains('open')){ setMenu(false); navToggle.focus(); } + }); + window.addEventListener('resize', function(){ + if(window.innerWidth > 860) setMenu(false); + }); + // subtle parallax on the hero sky var sky=document.querySelector('.hero-sky'); var reduce=window.matchMedia('(prefers-reduced-motion: reduce)').matches; @@ -1032,17 +893,19 @@

Project

},{rootMargin:'0px 0px -8% 0px', threshold:0.06}); document.querySelectorAll('.rise').forEach(function(el){ io.observe(el); }); - // active section tab - var tabs=document.querySelectorAll('.tab'); - var ids=['identity','callpath','control','security']; - var so=new IntersectionObserver(function(entries){ - entries.forEach(function(e){ - if(e.isIntersecting){ - tabs.forEach(function(t){ t.classList.toggle('active', t.dataset.tab===e.target.id); }); - } - }); - },{rootMargin:'-45% 0px -45% 0px'}); - ids.forEach(function(id){ var el=document.getElementById(id); if(el) so.observe(el); }); + // active rail entry + var railLinks=[].slice.call(document.querySelectorAll('.rail a')); + var railSecs=railLinks.map(function(a){ return document.querySelector(a.getAttribute('href')); }); + railSecs.forEach(function(sec, i){ + if(!sec) return; + new IntersectionObserver(function(entries){ + entries.forEach(function(e){ + if(!e.isIntersecting) return; + railLinks.forEach(function(l){ l.classList.remove('on'); }); + railLinks[i].classList.add('on'); + }); + },{rootMargin:'-40% 0px -55% 0px'}).observe(sec); + }); })();