clone

an rts arena for scripts: submit tribe strategies over the web or mcp, run deterministic matches, watch them

fix: Connect is two semantic columns, not a card bag

Dami committed b3474cd
Left: your account (account, identities, danger zone). Right: your agent
(connection card with docs folded in, examples). The paste-a-pubkey 'add
device key' flow is gone entirely — UI and endpoint — the pairing code IS
how devices join. Identities show human labels ('shoo account',
'device 1a2b3c4d') with the raw identifier in a tooltip, and the link-code
output tells you exactly what to say to your agent. Sign-in copy no longer
mentions token minting.
commit
b3474cd58e3fe314743c482183810cf839a40cdc
parent
1ac43a1714b671b73fcdfd33a90a5406ed854661
author
Dami <dami.etoile@gmail.com>
7 files changed, +61 -105
Mdocs/auth.md8+-
Mpackages/protocol/src/api.ts1-
Mweb/src/api.ts1-
Mweb/src/pages/auth-card.ts2+-
Mweb/src/pages/connect.ts120++++++++++++------------------
Mweb/src/style.css9++
Mworker/src/router.ts25------
M docs/auth.md +4 -4
M packages/protocol/src/api.ts +0 -1
M web/src/api.ts +0 -1
M web/src/pages/auth-card.ts +1 -1
M web/src/pages/connect.ts +47 -73
@@ -14,21 +14,27 @@ export function renderConnect(main: HTMLElement, ctx: AppCtx): void {
1414 // Signed in on the canonical auth origin on behalf of another host? Bounce back.1515 if (honorReturnRedirect()) return;1616 17+ // Two SEMANTIC columns: everything account-shaped on the left, everything18+ // your agent needs on the right. Never mix the themes.1719 const accountCard = h('div', { class: 'card' });18- const tokensCard = h('div', { class: 'card' });19- const docsCard = h('div', { class: 'card' });20- const examplesCard = h('div', { class: 'card' });20+ const identitiesCard = h('div', { class: 'card' });2121 const dangerCard = h('div', { class: 'card danger-zone' });22+ const agentCard = h('div', { class: 'card' });23+ const examplesCard = h('div', { class: 'card' });2224 main.append(2325 h('div', { class: 'home-grid' },24- h('div', { class: 'home-col' }, accountCard, tokensCard, docsCard),25- h('div', { class: 'home-col' }, examplesCard, dangerCard),26+ h('div', { class: 'home-col' },27+ h('h2', { class: 'col-title' }, '- your account -'),28+ accountCard, identitiesCard, dangerCard),29+ h('div', { class: 'home-col' },30+ h('h2', { class: 'col-title' }, '- your agent -'),31+ agentCard, examplesCard),2632 ),2733 );2834 2935 paintAccount(accountCard, ctx);30- paintIdentities(tokensCard);31- paintDocs(docsCard);36+ paintIdentities(identitiesCard);37+ paintAgent(agentCard);3238 void paintExamples(examplesCard).catch((e) => showErr(examplesCard, e));3339 paintDanger(dangerCard, ctx);3440 }
@@ -96,17 +102,32 @@ function paintAccount(card: HTMLElement, ctx: AppCtx) {
96102 // Docs97103 // --------------------------------------------------------------------------98104 99-function paintDocs(card: HTMLElement) {105+function paintAgent(card: HTMLElement) {100106 card.append(101- h('h2', null, 'Docs'),107+ h('h2', null, 'Connect your agent'),108+ h(109+ 'p',110+ { class: 'muted' },111+ 'No signup: the agent mints an ed25519 keypair, signs in with it, and starts submitting strategies.',112+ ),113+ h(114+ 'pre',115+ { class: 'snippet' },116+ [117+ `# how to ${location.origin}/auth.md (identity file + mint snippet)`,118+ `# docs ${location.origin}/llms.txt (full scripting reference)`,119+ `# MCP ${location.origin}/mcp (streamable HTTP, bearer token)`,120+ `# REST ${location.origin}/api/*`,121+ ].join('\n'),122+ ),102123 h(103124 'p',104125 { class: 'muted' },105- 'Everything an agent (or a human) needs to write tribe scripts: ',126+ 'Read the reference: ',106127 h('a', { href: '/llms.txt', target: '_blank' }, '/llms.txt'),107- ' — full scripting reference. MCP endpoint: ',108- h('a', { href: '/mcp' }, `${location.origin}/mcp`),109- ' (streamable HTTP, bearer token).',128+ ' - identity convention: ',129+ h('a', { href: '/auth.md', target: '_blank' }, '/auth.md'),130+ '.',110131 ),111132 );112133 }
@@ -219,19 +240,9 @@ function paintIdentities(card: HTMLElement) {
219240 clear(card);220241 card.append(221242 h('h2', null, 'Identities'),222- h(223- 'p',224- { class: 'muted' },225- 'Your account is its keys. Agents mint an ed25519 keypair and sign in with it (see /auth.md); add another device by pasting its public key here.',226- ),243+ h('p', { class: 'muted' }, 'The ways you can sign in to this account.'),227244 );228245 const listWrap = h('div');229- const status = h('div');230- const pubInput = h('input', {231- type: 'text',232- placeholder: 'public key (base64url, 43 chars) - from identity.json .publicKey.x',233- maxLength: 64,234- });235246 236247 async function refresh() {237248 try {
@@ -243,8 +254,12 @@ function paintIdentities(card: HTMLElement) {
243254 }244255 }245256 246- function fingerprint(externalId: string): string {247- return externalId.startsWith('key:') ? externalId.slice(4, 16) + '...' : externalId;257+ // Humans never need the full identifier - a short label tells keys apart;258+ // the full value lives in the title tooltip.259+ function labelFor(k: KeyIdentity): string {260+ if (k.kind === 'key') return `device ${k.externalId.slice(4, 12)}`;261+ if (k.kind === 'dev') return k.externalId;262+ return 'shoo account';248263 }249264 250265 function paintList(keys: KeyIdentity[]) {
@@ -253,7 +268,6 @@ function paintIdentities(card: HTMLElement) {
253268 h(254269 'table',255270 { class: 'list' },256- h('thead', null, h('tr', null, h('th', null, 'Kind'), h('th', null, 'Identity'), h('th', null, 'Added'), h('th'))),257271 h(258272 'tbody',259273 null,
@@ -261,12 +275,11 @@ function paintIdentities(card: HTMLElement) {
261275 h(262276 'tr',263277 null,264- h('td', null, k.kind),265- h('td', { class: 'mono' }, fingerprint(k.externalId)),266- h('td', { class: 'muted' }, fmtDate(k.createdAt)),278+ h('td', { title: k.externalId }, labelFor(k)),279+ h('td', { class: 'muted' }, `added ${fmtDate(k.createdAt)}`),267280 h(268281 'td',269- null,282+ { style: 'text-align:right' },270283 k.kind === 'key' && keys.length > 1271284 ? h(272285 'button',
@@ -291,36 +304,11 @@ function paintIdentities(card: HTMLElement) {
291304 292305 card.append(293306 listWrap,294- status,295- h(296- 'div',297- { class: 'row', style: 'margin-top:0.6rem' },298- pubInput,299- h(300- 'button',301- {302- onclick: async () => {303- clear(status);304- const pub = pubInput.value.trim();305- if (!pub) return;306- try {307- await api.addKey(pub);308- pubInput.value = '';309- status.append(h('p', { class: 'ok-text' }, 'Key added - that device can now sign in as you.'));310- await refresh();311- } catch (err) {312- status.append(h('p', { class: 'error-text' }, err instanceof Error ? err.message : String(err)));313- }314- },315- },316- 'Add device key',317- ),318- ),319307 h('h3', { style: 'margin-top:1.2rem' }, 'Link a device'),320308 h(321309 'p',322310 { class: 'muted' },323- 'Already minted a key on another machine (or as an agent)? Generate a pairing code and redeem it there - the device account merges into this one.',311+ 'Working from another machine or an agent? Generate a pairing code and redeem it there - that device becomes this account.',324312 ),325313 (() => {326314 const codeOut = h('div');
@@ -333,17 +321,16 @@ function paintIdentities(card: HTMLElement) {
333321 onclick: async () => {334322 clear(codeOut);335323 try {336- const { code, expiresAt } = await api.mintLinkCode();324+ const { code } = await api.mintLinkCode();337325 codeOut.append(338326 h(339327 'div',340328 { class: 'token-once' },341- h('p', null, 'On the device, redeem within 10 minutes:'),342329 h('p', { class: 'row' }, h('code', { class: 'mono' }, code)),343330 h(344331 'p',345332 { class: 'muted' },346- `MCP: link_account {"code": "${code}"} - or POST /api/keys/link with it. Expires ${fmtDate(expiresAt)}.`,333+ 'Single use, valid 10 minutes. On the device, tell your agent: "link this device to my tribes account with code ' + code + '".',347334 ),348335 ),349336 );
@@ -357,19 +344,6 @@ function paintIdentities(card: HTMLElement) {
357344 codeOut,358345 );359346 })(),360- h('h3', { style: 'margin-top:1.2rem' }, 'Connect your agent'),361- h(362- 'pre',363- { class: 'snippet' },364- [365- '# No signup: mint an ed25519 keypair and sign in with it.',366- `# convention ${location.origin}/auth.md (identity file + snippet)`,367- `# docs ${location.origin}/llms.txt`,368- `# MCP ${location.origin}/mcp (streamable HTTP)`,369- `# REST ${location.origin}/api/*`,370- '# POST /api/keys/auth -> a bearer token for the wire; re-mint anytime.',371- ].join('\n'),372- ),373347 );374348 375349 void refresh();
M web/src/style.css +9 -0
M worker/src/router.ts +0 -25