From e7cc424ae7c7b76d382273805b9f044d8eaab9c8 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 1 Oct 2026 14:08:35 +0000
Subject: [PATCH 1/3] feat(queen): MY RUNNERS cabinet on the leaderboard tab
The LEADERBOARD tab invited people to "lend the swarm a key" and had no
door. It now carries a cabinet where the person signed in on
app.t27.ai mints, lists and revokes runner tokens (trios-agent-server
/queen/me/runners). A runner runs on the lender's own machine with the
lender's own provider key; this page has no field for a key and never
sees one.
- lib/queenRunners.ts: pure decisions with an injected fetch; the
session token rides only as a bearer with credentials omitted and
exactly two headers; a runner token is accepted only in the shape the
Queen mints and shown once.
- On t27.ai (bridge path) the panel points to app.t27.ai/queen instead
of forwarding the read-only game token.
- Leaderboard rows are keyed by lane kind + first lane, since a runner's
Telegram name can equal an operator's lane name.
- qa/queen-runners-contract.mjs wired into website-checks; the new sheet
is registered with the contrast contract and uses opaque inner grounds.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01SJ8KjRoGNBoHBoDR92fAo2
---
.github/workflows/website-checks.yml | 5 +
apps/website/package.json | 1 +
apps/website/qa/queen-contrast-contract.mjs | 3 +
apps/website/qa/queen-runners-contract.mjs | 148 ++++++++++
.../src/components/QueenLeaderboard.tsx | 9 +-
apps/website/src/components/QueenRunners.css | 133 +++++++++
apps/website/src/components/QueenRunners.tsx | 264 ++++++++++++++++++
apps/website/src/lib/queenRunners.ts | 161 +++++++++++
8 files changed, 723 insertions(+), 1 deletion(-)
create mode 100644 apps/website/qa/queen-runners-contract.mjs
create mode 100644 apps/website/src/components/QueenRunners.css
create mode 100644 apps/website/src/components/QueenRunners.tsx
create mode 100644 apps/website/src/lib/queenRunners.ts
diff --git a/.github/workflows/website-checks.yml b/.github/workflows/website-checks.yml
index 1c82cb05da..2087216056 100644
--- a/.github/workflows/website-checks.yml
+++ b/.github/workflows/website-checks.yml
@@ -178,6 +178,11 @@ jobs:
# only our own /live/ is framed. Pure, no browser.
- name: The BROWSER view
run: npm run check:queen-browser
+ # The runner cabinet: the session token rides only in a header with
+ # credentials omitted, a runner token is shown once and kept nowhere, and
+ # the page has no field for a provider key. Pure, no browser.
+ - name: The runner cabinet
+ run: npm run check:queen-runners
# What that identity is FOR. The owner's rule, 2026-09-20: only registered
# people write to the Queen. The gate that holds it is the proxy's, not
# this bundle's -- a rule shipped in a public bundle is a suggestion -- and
diff --git a/apps/website/package.json b/apps/website/package.json
index 281c669437..d77dda67ab 100644
--- a/apps/website/package.json
+++ b/apps/website/package.json
@@ -83,6 +83,7 @@
"check:queen-redirect": "node --experimental-strip-types qa/queen-redirect-contract.mjs",
"check:app-session-identity": "node --experimental-strip-types qa/app-session-identity-contract.mjs",
"check:queen-browser": "node --experimental-strip-types qa/queen-browser-contract.mjs",
+ "check:queen-runners": "node --experimental-strip-types qa/queen-runners-contract.mjs",
"check:queen-chat-gate": "node --experimental-strip-types qa/queen-chat-gate-contract.mjs",
"check:queen-chat-tabs": "node --experimental-strip-types qa/queen-chat-tabs-contract.mjs",
"check:queen-contrast": "node --experimental-strip-types qa/queen-contrast-contract.mjs",
diff --git a/apps/website/qa/queen-contrast-contract.mjs b/apps/website/qa/queen-contrast-contract.mjs
index 14b93c4eda..62a958c522 100644
--- a/apps/website/qa/queen-contrast-contract.mjs
+++ b/apps/website/qa/queen-contrast-contract.mjs
@@ -222,6 +222,9 @@ const REACHED = {
/* PEOPLE, the contributors half of that tab: the fourth sheet the list has
caught on its first run, which is four for four. */
'src/components/QueenPeople.css',
+ /* MY RUNNERS, the cabinet inside that tab: a veil over the hive, blurred,
+ like the leaderboard rows it sits beside. */
+ 'src/components/QueenRunners.css',
/* LEVEL II, the comb on the ROADMAP view: five for five. It renders inside
.rm, whose opaque gradient already grounds it, and its own panel is
opaque on top of that. */
diff --git a/apps/website/qa/queen-runners-contract.mjs b/apps/website/qa/queen-runners-contract.mjs
new file mode 100644
index 0000000000..521baabc63
--- /dev/null
+++ b/apps/website/qa/queen-runners-contract.mjs
@@ -0,0 +1,148 @@
+// MY RUNNERS: the cabinet that mints runner tokens for the person signed in.
+//
+// Calls the decisions in src/lib/queenRunners.ts with real inputs and a fetch
+// that records what it was asked. Each rule below is one a wrong edit would
+// break quietly: a session token sent with cookies or an extra header, a
+// runner token kept past the one answer that carries it, a provider key field
+// slipping into a page that promises it has none.
+//
+// node --experimental-strip-types qa/queen-runners-contract.mjs
+
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import {
+ CABINET_HOME,
+ CABINET_PATH,
+ RUNNER_TOKEN,
+ cabinetOf,
+ callRunners,
+ runnerOf,
+ setupLines,
+} from '../src/lib/queenRunners.ts'
+
+const BASE = 'https://queen.invalid/'
+const TOKEN = `qr_${'A'.repeat(43)}`
+const RUNNER = {
+ id: 3,
+ label: 'laptop',
+ lane: 100000003,
+ tokenHint: 'AAAA',
+ createdAt: '2026-10-01T00:00:00Z',
+ lastSeenAt: null,
+ state: 'never-seen',
+}
+
+/** A fetch that answers from a script and records every request. */
+function recorder(script) {
+ const asked = []
+ const fetch = async (url, init) => {
+ asked.push({ url, ...init })
+ const next = script.shift()
+ assert.ok(next, `unexpected request ${init.method} ${url}`)
+ return { ok: next.status >= 200 && next.status < 300, status: next.status, json: async () => next.body ?? {} }
+ }
+ return { asked, fetch }
+}
+
+const env = (fetch, token = 'session-token') => ({ base: BASE, fetch, token: () => token })
+
+// 1. Nobody signed in: no request at all.
+{
+ const { asked, fetch } = recorder([])
+ assert.deepEqual(await callRunners(env(fetch, null), { kind: 'list' }), { state: 'signin' })
+ assert.equal(asked.length, 0)
+}
+
+// 2. Every request: credentials omitted, exactly two headers, bearer is the session.
+{
+ const { asked, fetch } = recorder([{ status: 200, body: { runners: [RUNNER], limit: 5 } }])
+ const view = await callRunners(env(fetch), { kind: 'list' })
+ assert.equal(view.state, 'ready')
+ assert.equal(view.cabinet.runners[0].label, 'laptop')
+ const [req] = asked
+ assert.equal(req.url, `https://queen.invalid${CABINET_PATH}`)
+ assert.equal(req.credentials, 'omit')
+ assert.deepEqual(Object.keys(req.headers).sort(), ['Authorization', 'Content-Type'])
+ assert.equal(req.headers.Authorization, 'Bearer session-token')
+}
+
+// 3. A refused session is "sign in", a server failure is "unavailable".
+{
+ const a = recorder([{ status: 401 }])
+ assert.deepEqual(await callRunners(env(a.fetch), { kind: 'list' }), { state: 'signin' })
+ const b = recorder([{ status: 503 }])
+ assert.deepEqual(await callRunners(env(b.fetch), { kind: 'list' }), { state: 'unavailable' })
+}
+
+// 4. Create: the token is shown once, only in `minted`, and only when it is a
+// runner token as minted; the list is re-read after.
+{
+ const { asked, fetch } = recorder([
+ { status: 201, body: { runner: RUNNER, token: TOKEN } },
+ { status: 200, body: { runners: [RUNNER], limit: 5 } },
+ ])
+ const view = await callRunners(env(fetch), { kind: 'create', label: ' my laptop ' })
+ assert.equal(view.state, 'minted')
+ assert.equal(view.token, TOKEN)
+ assert.equal(JSON.parse(asked[0].body).label, 'my laptop')
+ assert.equal(asked[1].method, 'GET')
+
+ const bad = recorder([{ status: 201, body: { runner: RUNNER, token: 'sk-live-something' } }])
+ assert.deepEqual(await callRunners(env(bad.fetch), { kind: 'create', label: 'x' }), { state: 'unavailable' })
+}
+
+// 5. Refusals keep the list on screen and send nothing when there is no name.
+{
+ const kept = { runners: [runnerOf(RUNNER)], limit: 5 }
+ const empty = recorder([])
+ const noName = await callRunners(env(empty.fetch), { kind: 'create', label: ' ' }, kept)
+ assert.deepEqual(noName, { state: 'refused', cabinet: kept, reason: 'label' })
+ assert.equal(empty.asked.length, 0)
+ const full = recorder([{ status: 409 }])
+ const limit = await callRunners(env(full.fetch), { kind: 'create', label: 'one more' }, kept)
+ assert.deepEqual(limit, { state: 'refused', cabinet: kept, reason: 'limit' })
+}
+
+// 6. Revoke: DELETE by id, then the list; a nonsense id never reaches the wire.
+{
+ const { asked, fetch } = recorder([{ status: 204 }, { status: 200, body: { runners: [], limit: 5 } }])
+ const view = await callRunners(env(fetch), { kind: 'revoke', id: 3 })
+ assert.equal(view.state, 'ready')
+ assert.equal(asked[0].method, 'DELETE')
+ assert.equal(asked[0].url, `https://queen.invalid${CABINET_PATH}/3`)
+ const n = recorder([{ status: 200, body: { runners: [], limit: 5 } }])
+ await callRunners(env(n.fetch), { kind: 'revoke', id: -1 })
+ assert.equal(n.asked.length, 1)
+ assert.equal(n.asked[0].method, 'GET')
+}
+
+// 7. The wire is data: malformed runners are dropped, labels are capped.
+{
+ assert.equal(runnerOf({ ...RUNNER, state: 'pwned' }), null)
+ assert.equal(runnerOf({ ...RUNNER, tokenHint: '' }), null)
+ assert.equal(runnerOf({ ...RUNNER, label: 'x'.repeat(200) }).label.length, 40)
+ assert.deepEqual(cabinetOf({ runners: [RUNNER, null, 7], limit: 'x' }).runners.length, 1)
+ assert.equal(cabinetOf(null).limit, 5)
+ assert.ok(RUNNER_TOKEN.test(TOKEN))
+}
+
+// 8. The setup lines name the provider key nowhere; they carry the runner
+// token and the Queen's address and nothing else.
+{
+ const lines = setupLines(TOKEN, 'https://queen.invalid').join('\n')
+ assert.ok(lines.includes(TOKEN))
+ assert.ok(!/API_KEY|sk-|provider/i.test(lines))
+ assert.ok(CABINET_HOME.startsWith('https://app.t27.ai/queen/'))
+}
+
+// 9. The page has no field for a provider key and never stores the token.
+{
+ const page = readFileSync(new URL('../src/components/QueenRunners.tsx', import.meta.url), 'utf8')
+ const lib = readFileSync(new URL('../src/lib/queenRunners.ts', import.meta.url), 'utf8')
+ for (const forbidden of ['localStorage', 'sessionStorage', 'document.cookie', 'type="password"', 'apiKey', 'api_key']) {
+ assert.ok(!page.includes(forbidden) && !lib.includes(forbidden), `runners cabinet must not use ${forbidden}`)
+ }
+ assert.ok(!lib.includes("credentials: 'include'"))
+}
+
+console.log('queen-runners contract: ok')
diff --git a/apps/website/src/components/QueenLeaderboard.tsx b/apps/website/src/components/QueenLeaderboard.tsx
index 05674d3d32..f8b5082d43 100644
--- a/apps/website/src/components/QueenLeaderboard.tsx
+++ b/apps/website/src/components/QueenLeaderboard.tsx
@@ -14,6 +14,7 @@
import { useEffect, useState } from 'react'
import { QUEEN_API } from '../lib/queenApi'
import QueenPeople from './QueenPeople'
+import QueenRunners from './QueenRunners'
import './QueenLeaderboard.css'
interface Contributor {
@@ -21,6 +22,8 @@ interface Contributor {
claimed: boolean
/** Their GitHub login, when the operator signed the lane as `@login`. */
github?: string
+ /** The lanes ran on the lender's own machine: a runner, named by Telegram. */
+ runner?: boolean
keys: number[]
accepted: number
/** Accepted issues whose boundary named a .t27 file: the game's own goal. */
@@ -163,6 +166,10 @@ export default function QueenLeaderboard({ lang }: { lang: 'en' | 'ru' }) {
open repositories. */}
+ {/* The door this tab used to only point at: lend a lane by running it
+ yourself, with your key on your own machine. */}
+
+
{i + 1}
{/* The avatar comes from github.com/.png, a public
redirect: no API call, no token, and a leaderboard that does
diff --git a/apps/website/src/components/QueenRunners.css b/apps/website/src/components/QueenRunners.css
new file mode 100644
index 0000000000..ee7ef24569
--- /dev/null
+++ b/apps/website/src/components/QueenRunners.css
@@ -0,0 +1,133 @@
+/* MY RUNNERS: same opaque/blurred grounds as the leaderboard rows, because the
+ panel carries text over the lit hive (qa/queen-contrast-contract.mjs). */
+.qr {
+ margin: 18px 0 8px;
+ padding: 14px 16px;
+ border: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22));
+ border-radius: 10px;
+ background: var(--hud-veil, rgba(2, 8, 6, 0.72));
+ -webkit-backdrop-filter: blur(6px);
+ backdrop-filter: blur(6px);
+}
+.qr-head h3 {
+ margin: 0 0 6px;
+ font-size: 15px;
+ letter-spacing: 0.08em;
+ color: var(--hud-green, #00ff88);
+}
+.qr-head p,
+.qr-note,
+.qr-small {
+ margin: 0 0 6px;
+ line-height: 1.5;
+ color: var(--hud-muted, rgba(255, 255, 255, 0.78));
+}
+.qr-key {
+ color: var(--hud-gold, #ffd700) !important;
+ font-size: 13px;
+}
+.qr-small {
+ font-size: 12px;
+}
+.qr-note a {
+ color: var(--hud-green, #00ff88);
+}
+.qr-list {
+ list-style: none;
+ margin: 10px 0;
+ padding: 0;
+ display: grid;
+ gap: 6px;
+}
+.qr-row {
+ display: grid;
+ grid-template-columns: 10px minmax(0, 1fr) auto;
+ grid-template-areas: 'dot label button' 'dot meta button';
+ align-items: center;
+ column-gap: 10px;
+ padding: 8px 10px;
+ border: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22));
+ border-radius: 8px;
+ background: #020806;
+}
+.qr-dot {
+ grid-area: dot;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.35);
+}
+.qr-row.is-online .qr-dot {
+ background: var(--hud-green, #00ff88);
+}
+.qr-label {
+ grid-area: label;
+ overflow-wrap: anywhere;
+ color: #e8f5ee;
+}
+.qr-meta {
+ grid-area: meta;
+ font-size: 12px;
+ color: var(--hud-muted, rgba(255, 255, 255, 0.78));
+ overflow-wrap: anywhere;
+}
+.qr-row button {
+ grid-area: button;
+}
+.qr button {
+ padding: 6px 10px;
+ border: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22));
+ border-radius: 6px;
+ background: #04170e;
+ color: var(--hud-green, #00ff88);
+ font: inherit;
+ font-size: 13px;
+ cursor: pointer;
+}
+.qr button:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+.qr-form {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin: 8px 0 4px;
+}
+.qr-form input {
+ flex: 1 1 180px;
+ min-width: 0;
+ padding: 6px 10px;
+ border: 1px solid var(--hud-line, rgba(0, 255, 136, 0.22));
+ border-radius: 6px;
+ background: #000;
+ color: #e8f5ee;
+ font: inherit;
+}
+.qr-minted {
+ margin: 10px 0;
+ padding: 10px 12px;
+ border: 1px solid var(--hud-gold, #ffd700);
+ border-radius: 8px;
+ background: #141000;
+}
+.qr-minted p {
+ margin: 4px 0 8px;
+ color: var(--hud-muted, rgba(255, 255, 255, 0.78));
+}
+.qr-setup {
+ margin: 0 0 8px;
+ padding: 8px;
+ max-width: 100%;
+ overflow-x: auto;
+ white-space: pre;
+ font-family: 'JetBrains Mono', ui-monospace, monospace;
+ font-size: 12px;
+ color: #e8f5ee;
+ background: #000;
+ border-radius: 6px;
+}
+.qr-actions {
+ display: flex;
+ gap: 8px;
+}
diff --git a/apps/website/src/components/QueenRunners.tsx b/apps/website/src/components/QueenRunners.tsx
new file mode 100644
index 0000000000..6d0f9f5cc9
--- /dev/null
+++ b/apps/website/src/components/QueenRunners.tsx
@@ -0,0 +1,264 @@
+// MY RUNNERS: mint, list and revoke the runner tokens of the person signed in
+// on app.t27.ai. Decisions live in lib/queenRunners.ts; this file only draws
+// them. There is no field for a provider key here and there never will be: the
+// key stays on the runner's machine, which is the whole reason a runner exists.
+import { useCallback, useEffect, useState } from 'react'
+import { appSessionFromWindow } from '../lib/appSessionIdentity'
+import { QUEEN_API } from '../lib/queenApi'
+import {
+ CABINET_HOME,
+ type CabinetView,
+ callRunners,
+ type RunnersCall,
+ type RunnersEnv,
+ setupLines,
+} from '../lib/queenRunners'
+import './QueenRunners.css'
+
+interface RunnersCopy {
+ title: string
+ lead: string
+ keyStays: string
+ signin: string
+ elsewhere: string
+ unavailable: string
+ loading: string
+ none: string
+ namePlaceholder: string
+ create: string
+ revoke: string
+ revokeConfirm: (label: string) => string
+ lane: string
+ state: Record<'never-seen' | 'online' | 'offline', string>
+ limit: (n: number) => string
+ refusedLabel: string
+ mintedTitle: string
+ mintedOnce: string
+ copy: string
+ copied: string
+ done: string
+ nextStage: string
+}
+
+const RUNNERS_COPY: Record<'en' | 'ru', RunnersCopy> = {
+ en: {
+ title: 'MY RUNNERS',
+ lead: 'A runner is a lane that runs on your own machine, under your own provider account. The Queen hands it a task; the work comes back; the XP lands here, on your name.',
+ keyStays: 'Your provider key never leaves your machine. This page has no field for it and the Queen never sees it — the token below only lets a process speak as your runner.',
+ signin: 'Sign in to app.t27.ai to manage your runners.',
+ elsewhere: 'Runners are managed on the app’s board, where your session lives:',
+ unavailable: 'The Queen did not answer. Try again in a minute.',
+ loading: 'Reading your runners…',
+ none: 'No runners yet.',
+ namePlaceholder: 'Name, e.g. my laptop',
+ create: 'Create runner',
+ revoke: 'Revoke',
+ revokeConfirm: (label) => `Revoke “${label}”? A process using its token stops at once.`,
+ lane: 'lane',
+ state: { 'never-seen': 'never connected', online: 'online', offline: 'offline' },
+ limit: (n) => `Up to ${n} runners.`,
+ refusedLabel: 'Give the runner a name.',
+ mintedTitle: 'Runner token',
+ mintedOnce: 'Shown once. Copy it now — afterwards only its last four characters are kept.',
+ copy: 'Copy',
+ copied: 'Copied',
+ done: 'I saved it',
+ nextStage: 'Today a runner can connect and show up online. Taking tasks and handing work back is the next stage of the Queen; until it ships there is nothing to take.',
+ },
+ ru: {
+ title: 'МОИ РАННЕРЫ',
+ lead: 'Раннер — это полоса, которая работает на вашей машине, под вашим аккаунтом провайдера. Королева даёт ему задачу, работа возвращается, а XP начисляется здесь, на ваше имя.',
+ keyStays: 'Ваш ключ провайдера не покидает вашу машину. На этой странице нет поля для него, и Королева его не видит — токен ниже лишь позволяет процессу говорить от имени вашего раннера.',
+ signin: 'Войдите в app.t27.ai, чтобы управлять раннерами.',
+ elsewhere: 'Раннеры управляются на доске приложения, где живёт ваша сессия:',
+ unavailable: 'Королева не ответила. Попробуйте через минуту.',
+ loading: 'Читаю ваших раннеров…',
+ none: 'Раннеров пока нет.',
+ namePlaceholder: 'Имя, например «мой ноутбук»',
+ create: 'Создать раннер',
+ revoke: 'Отозвать',
+ revokeConfirm: (label) => `Отозвать «${label}»? Процесс с его токеном сразу перестанет работать.`,
+ lane: 'полоса',
+ state: { 'never-seen': 'ещё не подключался', online: 'на связи', offline: 'не на связи' },
+ limit: (n) => `Не больше ${n} раннеров.`,
+ refusedLabel: 'Дайте раннеру имя.',
+ mintedTitle: 'Токен раннера',
+ mintedOnce: 'Показывается один раз. Скопируйте сейчас — потом хранятся только его последние четыре символа.',
+ copy: 'Скопировать',
+ copied: 'Скопировано',
+ done: 'Сохранено',
+ nextStage: 'Сейчас раннер может подключиться и отображаться «на связи». Выдача задач и приём работы — следующий этап Королевы; пока его нет, брать нечего.',
+ },
+}
+
+const env: RunnersEnv = {
+ base: QUEEN_API,
+ fetch: (url, init) => window.fetch(url, init),
+ token: () => {
+ const s = appSessionFromWindow()
+ return s.source === 'app-session' && s.state === 'signed-in' ? s.token : null
+ },
+}
+
+export default function QueenRunners({ lang }: { lang: 'en' | 'ru' }) {
+ const c = RUNNERS_COPY[lang]
+ const session = appSessionFromWindow()
+ const [view, setView] = useState(null)
+ const [busy, setBusy] = useState(false)
+ const [label, setLabel] = useState('')
+ const [copied, setCopied] = useState(false)
+
+ const cabinet = view && 'cabinet' in view ? view.cabinet : undefined
+
+ const act = useCallback(
+ async (call: RunnersCall) => {
+ setBusy(true)
+ try {
+ const next = await callRunners(env, call, cabinet)
+ setView(next)
+ if (next.state === 'minted') setLabel('')
+ } catch {
+ setView({ state: 'unavailable' })
+ } finally {
+ setBusy(false)
+ }
+ },
+ [cabinet],
+ )
+
+ useEffect(() => {
+ if (session.source === 'app-session') void act({ kind: 'list' })
+ // Once per mount: the list is re-read after every action anyway.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const head = (
+
+
{c.title}
+
{c.lead}
+
{c.keyStays}
+
+ )
+
+ // On t27.ai the session is not in reach, and the bridge's game token is not
+ // this panel's to forward. Say where the cabinet is instead.
+ if (session.source === 'bridge') {
+ return (
+
+ {head}
+
+
+ )
+}
diff --git a/apps/website/src/lib/queenRunners.ts b/apps/website/src/lib/queenRunners.ts
new file mode 100644
index 0000000000..505853bbc9
--- /dev/null
+++ b/apps/website/src/lib/queenRunners.ts
@@ -0,0 +1,161 @@
+/**
+ * MY RUNNERS: the cabinet half of "lend a lane without lending a key".
+ *
+ * The swarm runs every bee on one provider key, and the honest way to add a
+ * lane is the one where the key never moves: a runner on the lender's own
+ * machine, under the lender's own account, takes a task and brings the work
+ * back. This page never sees that key and has no field for one. What it hands
+ * out is a RUNNER TOKEN — it lets a process speak as one of your runners and
+ * can spend nobody's quota — minted by the Queen (trios-agent-server,
+ * /queen/me/runners) for the person the app's own session names.
+ *
+ * WHERE THE CREDENTIAL COMES FROM. Only the app's own copy of the board
+ * (https://app.t27.ai/queen/) holds the person's session, read through
+ * appSessionIdentity.ts on the same terms as everywhere else: memory only,
+ * `credentials: 'omit'`, exactly two headers. On t27.ai the panel says where to
+ * go instead of reaching for the bridge's game token, which is scoped to
+ * read-only tools and is not this page's to forward.
+ *
+ * Pure apart from `env`, so qa/queen-runners-contract.mjs drives the real code.
+ * The Queen's address arrives in `env.base` (the component passes QUEEN_API)
+ * rather than being imported here: queenApi.ts reads Vite's import.meta.env,
+ * which a contract running under node does not have.
+ */
+export const CABINET_PATH = '/queen/me/runners'
+/** Where a person on t27.ai is sent to manage runners: the app's board. */
+export const CABINET_HOME = 'https://app.t27.ai/queen/#/queen?tab=leaderboard'
+
+export interface RunnerView {
+ id: number
+ label: string
+ lane: number
+ tokenHint: string
+ createdAt: string
+ lastSeenAt: string | null
+ state: 'never-seen' | 'online' | 'offline'
+}
+
+export interface Cabinet {
+ runners: RunnerView[]
+ limit: number
+}
+
+/** What the panel can be showing. A token appears only in `minted`, once. */
+export type CabinetView =
+ | { state: 'signin' }
+ | { state: 'unavailable' }
+ | { state: 'ready'; cabinet: Cabinet }
+ | { state: 'minted'; cabinet: Cabinet; token: string; runner: RunnerView }
+ | { state: 'refused'; cabinet: Cabinet; reason: 'limit' | 'label' }
+
+export interface RunnersEnv {
+ fetch: (
+ url: string,
+ init: { method: string; credentials: 'omit'; headers: Record; body?: string },
+ ) => Promise<{ ok: boolean; status: number; json(): Promise }>
+ /** The app session's access token, or null when nobody is signed in here. */
+ token: () => string | null
+ /** The Queen's origin, e.g. QUEEN_API. */
+ base: string
+}
+
+const isRecord = (v: unknown): v is Record =>
+ !!v && typeof v === 'object' && !Array.isArray(v)
+
+const STATES = new Set(['never-seen', 'online', 'offline'])
+
+/** One runner from the wire, or null. Everything is DATA: rendered as text. */
+export function runnerOf(raw: unknown): RunnerView | null {
+ if (!isRecord(raw)) return null
+ const { id, label, lane, tokenHint, createdAt, lastSeenAt, state } = raw
+ if (!Number.isSafeInteger(id) || typeof label !== 'string' || !Number.isSafeInteger(lane)) return null
+ if (typeof tokenHint !== 'string' || !/^[A-Za-z0-9_-]{4}$/.test(tokenHint)) return null
+ if (typeof state !== 'string' || !STATES.has(state)) return null
+ return {
+ id: id as number,
+ label: label.slice(0, 40),
+ lane: lane as number,
+ tokenHint,
+ createdAt: typeof createdAt === 'string' ? createdAt : '',
+ lastSeenAt: typeof lastSeenAt === 'string' ? lastSeenAt : null,
+ state: state as RunnerView['state'],
+ }
+}
+
+export function cabinetOf(raw: unknown): Cabinet {
+ const body = isRecord(raw) ? raw : {}
+ const runners = Array.isArray(body.runners)
+ ? body.runners.map(runnerOf).filter((r): r is RunnerView => r !== null)
+ : []
+ const limit = Number.isSafeInteger(body.limit) && (body.limit as number) > 0 ? (body.limit as number) : 5
+ return { runners, limit }
+}
+
+/** A runner token as the Queen mints it, and nothing that merely resembles one. */
+export const RUNNER_TOKEN = /^qr_[A-Za-z0-9_-]{43}$/
+
+export type RunnersCall =
+ | { kind: 'list' }
+ | { kind: 'create'; label: string }
+ | { kind: 'revoke'; id: number }
+
+/**
+ * One call to the cabinet, and the view it leaves. `previous` is what the panel
+ * showed, so a refused create keeps the list on screen instead of blanking it.
+ */
+export async function callRunners(env: RunnersEnv, call: RunnersCall, previous?: Cabinet): Promise {
+ const token = env.token()
+ if (!token) return { state: 'signin' }
+ const base = `${env.base.replace(/\/+$/, '')}${CABINET_PATH}`
+ const headers = { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }
+ const list = async (): Promise => {
+ const res = await env.fetch(base, { method: 'GET', credentials: 'omit', headers })
+ if (res.status === 401) return { state: 'signin' }
+ if (!res.ok) return { state: 'unavailable' }
+ return { state: 'ready', cabinet: cabinetOf(await res.json().catch(() => ({}))) }
+ }
+
+ if (call.kind === 'list') return list()
+
+ if (call.kind === 'revoke') {
+ if (!Number.isSafeInteger(call.id) || call.id <= 0) return list()
+ const res = await env.fetch(`${base}/${call.id}`, { method: 'DELETE', credentials: 'omit', headers })
+ if (res.status === 401) return { state: 'signin' }
+ if (!res.ok && res.status !== 404) return { state: 'unavailable' }
+ return list()
+ }
+
+ const label = call.label.replace(/\s+/g, ' ').trim().slice(0, 40)
+ const kept = previous ?? { runners: [], limit: 5 }
+ if (!label) return { state: 'refused', cabinet: kept, reason: 'label' }
+ const res = await env.fetch(base, {
+ method: 'POST',
+ credentials: 'omit',
+ headers,
+ body: JSON.stringify({ label }),
+ })
+ if (res.status === 401) return { state: 'signin' }
+ if (res.status === 409) return { state: 'refused', cabinet: kept, reason: 'limit' }
+ if (res.status === 400) return { state: 'refused', cabinet: kept, reason: 'label' }
+ if (!res.ok) return { state: 'unavailable' }
+ const body = await res.json().catch(() => ({}))
+ const runner = isRecord(body) ? runnerOf(body.runner) : null
+ const minted = isRecord(body) && typeof body.token === 'string' && RUNNER_TOKEN.test(body.token) ? body.token : null
+ if (!runner || !minted) return { state: 'unavailable' }
+ const after = await list()
+ const cabinet = after.state === 'ready' ? after.cabinet : { ...kept, runners: [...kept.runners, runner] }
+ return { state: 'minted', cabinet, token: minted, runner }
+}
+
+/**
+ * The lines a person pastes on their own machine. The provider key is named as
+ * an environment variable THEY set there and is never part of anything this
+ * page writes, stores or sends.
+ */
+export function setupLines(token: string, base: string): string[] {
+ return [
+ `export TRIOS_QUEEN_URL=${base}`,
+ `export TRIOS_RUNNER_TOKEN=${token}`,
+ `curl -fsS -X POST -H "Authorization: Bearer $TRIOS_RUNNER_TOKEN" "$TRIOS_QUEEN_URL/queen/runner/heartbeat"`,
+ ]
+}
From d2ad4c35ce0a5a0c928c8894b0280be2fb59a1e0 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 1 Oct 2026 21:40:49 +0000
Subject: [PATCH 2/3] feat(queen): the runners cabinet says how to take tasks
The server now hands tasks to runners and takes their work back for
review, so the cabinet stops saying there is nothing to take:
- The setup lines name the person's public fork, download
queen-runner.mjs and run it, instead of a single heartbeat curl.
- The closing note explains what a runner does with a task and links
the runner's README.
- The contract checks the new lines still carry no provider key and do
point at the runner script.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01SJ8KjRoGNBoHBoDR92fAo2
---
apps/website/qa/queen-runners-contract.mjs | 6 ++++++
apps/website/src/components/QueenRunners.tsx | 17 +++++++++++++----
apps/website/src/lib/queenRunners.ts | 16 ++++++++++++----
3 files changed, 31 insertions(+), 8 deletions(-)
diff --git a/apps/website/qa/queen-runners-contract.mjs b/apps/website/qa/queen-runners-contract.mjs
index 521baabc63..92768fb504 100644
--- a/apps/website/qa/queen-runners-contract.mjs
+++ b/apps/website/qa/queen-runners-contract.mjs
@@ -17,6 +17,7 @@ import {
cabinetOf,
callRunners,
runnerOf,
+ RUNNER_SCRIPT_URL,
setupLines,
} from '../src/lib/queenRunners.ts'
@@ -132,6 +133,11 @@ const env = (fetch, token = 'session-token') => ({ base: BASE, fetch, token: ()
const lines = setupLines(TOKEN, 'https://queen.invalid').join('\n')
assert.ok(lines.includes(TOKEN))
assert.ok(!/API_KEY|sk-|provider/i.test(lines))
+ // ...and they run the runner, which pushes to the person's own public fork.
+ assert.ok(lines.includes('TRIOS_RUNNER_REMOTE=https://github.com/'))
+ assert.ok(lines.includes('node queen-runner.mjs'))
+ assert.ok(RUNNER_SCRIPT_URL.startsWith('https://raw.githubusercontent.com/gHashTag/BrowserOS/'))
+ assert.ok(RUNNER_SCRIPT_URL.endsWith('/queen-runner.mjs'))
assert.ok(CABINET_HOME.startsWith('https://app.t27.ai/queen/'))
}
diff --git a/apps/website/src/components/QueenRunners.tsx b/apps/website/src/components/QueenRunners.tsx
index 6d0f9f5cc9..9bc20c2c69 100644
--- a/apps/website/src/components/QueenRunners.tsx
+++ b/apps/website/src/components/QueenRunners.tsx
@@ -11,6 +11,7 @@ import {
callRunners,
type RunnersCall,
type RunnersEnv,
+ RUNNER_README_URL,
setupLines,
} from '../lib/queenRunners'
import './QueenRunners.css'
@@ -37,7 +38,8 @@ interface RunnersCopy {
copy: string
copied: string
done: string
- nextStage: string
+ howItWorks: string
+ readme: string
}
const RUNNERS_COPY: Record<'en' | 'ru', RunnersCopy> = {
@@ -63,7 +65,8 @@ const RUNNERS_COPY: Record<'en' | 'ru', RunnersCopy> = {
copy: 'Copy',
copied: 'Copied',
done: 'I saved it',
- nextStage: 'Today a runner can connect and show up online. Taking tasks and handing work back is the next stage of the Queen; until it ships there is nothing to take.',
+ howItWorks: 'The runner takes one task at a time, runs your own coding agent on it (Claude Code by default) and pushes the result to your public fork. The Queen fetches that branch and her review judges it like any other bee’s work. Ctrl-C hands the task back.',
+ readme: 'Setup and settings',
},
ru: {
title: 'МОИ РАННЕРЫ',
@@ -87,7 +90,8 @@ const RUNNERS_COPY: Record<'en' | 'ru', RunnersCopy> = {
copy: 'Скопировать',
copied: 'Скопировано',
done: 'Сохранено',
- nextStage: 'Сейчас раннер может подключиться и отображаться «на связи». Выдача задач и приём работы — следующий этап Королевы; пока его нет, брать нечего.',
+ howItWorks: 'Раннер берёт по одной задаче, запускает на ней ваш собственный агент (по умолчанию Claude Code) и пушит результат в ваш публичный форк. Королева забирает эту ветку, и её ревью оценивает работу так же, как работу любой другой пчелы. Ctrl-C возвращает задачу.',
+ readme: 'Установка и настройки',
},
}
@@ -258,7 +262,12 @@ export default function QueenRunners({ lang }: { lang: 'en' | 'ru' }) {
)
}
diff --git a/apps/website/src/lib/queenRunners.ts b/apps/website/src/lib/queenRunners.ts
index 505853bbc9..4494a9ed5e 100644
--- a/apps/website/src/lib/queenRunners.ts
+++ b/apps/website/src/lib/queenRunners.ts
@@ -147,15 +147,23 @@ export async function callRunners(env: RunnersEnv, call: RunnersCall, previous?:
return { state: 'minted', cabinet, token: minted, runner }
}
+/** The runner script and its instructions, in the Queen's own repository. */
+export const RUNNER_SCRIPT_URL =
+ 'https://raw.githubusercontent.com/gHashTag/BrowserOS/feat/queen-supervisor/trios/agent-server/tools/queen-runner/queen-runner.mjs'
+export const RUNNER_README_URL =
+ 'https://github.com/gHashTag/BrowserOS/blob/feat/queen-supervisor/trios/agent-server/tools/queen-runner/README.md'
+
/**
- * The lines a person pastes on their own machine. The provider key is named as
- * an environment variable THEY set there and is never part of anything this
- * page writes, stores or sends.
+ * The lines a person pastes on their own machine. The provider key is never
+ * part of anything this page writes, stores or sends: the runner's agent uses
+ * whatever key the person already has set up there.
*/
export function setupLines(token: string, base: string): string[] {
return [
`export TRIOS_QUEEN_URL=${base}`,
`export TRIOS_RUNNER_TOKEN=${token}`,
- `curl -fsS -X POST -H "Authorization: Bearer $TRIOS_RUNNER_TOKEN" "$TRIOS_QUEEN_URL/queen/runner/heartbeat"`,
+ 'export TRIOS_RUNNER_REMOTE=https://github.com//.git',
+ `curl -fsSLO ${RUNNER_SCRIPT_URL}`,
+ 'node queen-runner.mjs',
]
}
From 26586e20e8f273768b03b5ceb8ce6c619fc7cf45 Mon Sep 17 00:00:00 2001
From: Claude
Date: Sat, 3 Oct 2026 20:48:32 +0000
Subject: [PATCH 3/3] fix(queen): download the runner from the branch
production builds from
The setup lines fetched queen-runner.mjs from feat/queen-supervisor,
where it does not exist (a 404, as the review found). The runner PRs
(gHashTag/BrowserOS#518, #521) now target
fix/queen-worker-provider-and-prompt-size, the branch the deployed
Queen is built from, so the script and its README are linked there,
through one RUNNER_BRANCH constant.
The contract's "no provider key" check matched the bare word
"provider", which that branch name contains. It now matches a key in
any spelling (API_KEY, sk-, provider key) and proves it still catches
each of them.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01SJ8KjRoGNBoHBoDR92fAo2
---
apps/website/qa/queen-runners-contract.mjs | 16 ++++++++++++++--
apps/website/src/lib/queenRunners.ts | 12 ++++++++----
2 files changed, 22 insertions(+), 6 deletions(-)
diff --git a/apps/website/qa/queen-runners-contract.mjs b/apps/website/qa/queen-runners-contract.mjs
index 92768fb504..7e860bd715 100644
--- a/apps/website/qa/queen-runners-contract.mjs
+++ b/apps/website/qa/queen-runners-contract.mjs
@@ -132,11 +132,23 @@ const env = (fetch, token = 'session-token') => ({ base: BASE, fetch, token: ()
{
const lines = setupLines(TOKEN, 'https://queen.invalid').join('\n')
assert.ok(lines.includes(TOKEN))
- assert.ok(!/API_KEY|sk-|provider/i.test(lines))
+ // A provider KEY, in any spelling. Not the bare word: the production branch
+ // the script is downloaded from is named fix/queen-worker-provider-and-...
+ const providerKey = /API_KEY|\bsk-|provider[_ -]?key/i
+ assert.ok(!providerKey.test(lines))
+ // ...and the narrower pattern still catches what it is for.
+ for (const leak of ['export ANTHROPIC_API_KEY=x', 'sk-abc', 'PROVIDER_KEY=x', 'your provider key']) {
+ assert.ok(providerKey.test(leak), leak)
+ }
// ...and they run the runner, which pushes to the person's own public fork.
assert.ok(lines.includes('TRIOS_RUNNER_REMOTE=https://github.com/'))
assert.ok(lines.includes('node queen-runner.mjs'))
- assert.ok(RUNNER_SCRIPT_URL.startsWith('https://raw.githubusercontent.com/gHashTag/BrowserOS/'))
+ // The production branch, the one the deployed Queen is built from.
+ assert.ok(
+ RUNNER_SCRIPT_URL.startsWith(
+ 'https://raw.githubusercontent.com/gHashTag/BrowserOS/fix/queen-worker-provider-and-prompt-size/',
+ ),
+ )
assert.ok(RUNNER_SCRIPT_URL.endsWith('/queen-runner.mjs'))
assert.ok(CABINET_HOME.startsWith('https://app.t27.ai/queen/'))
}
diff --git a/apps/website/src/lib/queenRunners.ts b/apps/website/src/lib/queenRunners.ts
index 4494a9ed5e..0b28487d2c 100644
--- a/apps/website/src/lib/queenRunners.ts
+++ b/apps/website/src/lib/queenRunners.ts
@@ -147,11 +147,15 @@ export async function callRunners(env: RunnersEnv, call: RunnersCall, previous?:
return { state: 'minted', cabinet, token: minted, runner }
}
+/**
+ * The branch the Queen's production service is built from, so the script a
+ * person downloads is the one the server they connect to speaks.
+ */
+export const RUNNER_BRANCH = 'fix/queen-worker-provider-and-prompt-size'
+
/** The runner script and its instructions, in the Queen's own repository. */
-export const RUNNER_SCRIPT_URL =
- 'https://raw.githubusercontent.com/gHashTag/BrowserOS/feat/queen-supervisor/trios/agent-server/tools/queen-runner/queen-runner.mjs'
-export const RUNNER_README_URL =
- 'https://github.com/gHashTag/BrowserOS/blob/feat/queen-supervisor/trios/agent-server/tools/queen-runner/README.md'
+export const RUNNER_SCRIPT_URL = `https://raw.githubusercontent.com/gHashTag/BrowserOS/${RUNNER_BRANCH}/trios/agent-server/tools/queen-runner/queen-runner.mjs`
+export const RUNNER_README_URL = `https://github.com/gHashTag/BrowserOS/blob/${RUNNER_BRANCH}/trios/agent-server/tools/queen-runner/README.md`
/**
* The lines a person pastes on their own machine. The provider key is never