ARS — Auth Relay Service
A local page where the owner types a code or password an agent asked for — npm OTP, authenticator-app codes, logins — so the value never passes through a chat or an LLM context.
Vision
The fleet runs 100 % on its own — and the few moments that genuinely need the owner's hand (a one-time code from his phone, a password only he knows) take him five seconds on one clean page, never a terminal, never a chat, never a copy-paste.
Mission
Give every agent in the fleet one simple, safe way to ask the owner for a secret value, and give the owner one simple, safe place to answer it: the agent asks, he gets a notification, he types it once, only the asking agent receives it — once — and the value is never stored, logged or seen by an LLM.
What it is
Today, when an agent hits an npm publish that needs a one-time code, a site with two-factor login, or a password, the work stops. The owner has to notice it in a terminal, read the request, and paste the value into chat — where it lands in a transcript and an AI's memory.
ARS removes that. It is a small web service that runs on the owner's Mac, reachable only from the Mac itself:
- An agent asks.
POST http://127.0.0.1:<port>/v1/askswith who is asking, the question, and the kind of answer (code,password,textorapprove). - The owner is told. A macOS notification names who asks and why, and the ask page opens in his browser.
- He answers. One field (masked for passwords, numeric for codes), Send or Afvis.
- The agent gets it, once. The agent long-polls
GET /v1/asks/:id?wait=55; the first read hands out the answer and wipes it. A later read only says it was answered.
Promises
- Never stored, never logged. Answers live only in memory until read or expired. An audit list shows who asked for what and when — never the value.
- Expires. An ask unanswered after 10 minutes can no longer be answered.
- Only the asker can read it. Each ask carries its own read token, and only the same BID principal that asked may read it.
- Agents prove who they are. Every agent call carries a BrobergID ticket; the owner sees who asks from the ticket (
edge m1 · session lens), never from text the agent wrote. - Only from this Mac — and, for agents, the fleet's private network. Bound to
127.0.0.1, with guards against web pages trying to reach it. Agents on the other Macs reach the agent API (and nothing else) on this Mac's Tailscale address; the window, history and answering stay on loopback. - Not a shortcut past the owner's own word. An
approveask is for routine confirmations; deploys with destructive migrations, DNS, env flips and the like still need his direct order.
Who uses it
- Lens — a test step
fill [field] from owner "<question>"asks ARS and types the answer, so a flow that hits 2FA keeps running. - Any cc session — ask for an npm OTP before
npm publish, with onecurl.
Asking from a cc session
Get a ticket from buddy (valid 300 s — fetch a new one for each call), then ask and wait:
T=$(curl -s -m 5 "http://127.0.0.1:4123/api/edge-ticket?audience=https://ars.broberg.ai&scope=ars:request&session=$BUDDY_SESSION_NAME" | jq -r .ticket)
curl -s -X POST http://127.0.0.1:7480/v1/asks -H "authorization: Bearer $T" -H 'content-type: application/json' \
-d '{"requester":"my-session: npm publish","question":"npm OTP for @broberg/x 1.2.3","kind":"code"}'
# → { id, read_token, … }; then GET /v1/asks/<id>?wait=55 with the same kind of header + x-ars-read-token
From another Mac, use this Mac's Tailscale address instead of 127.0.0.1 (printed by scripts/install.sh, e.g. http://100.78.170.62:7480).
Pass the answer to the next command through an environment variable (npm_config_otp=…), never as text in chat.
- Later: buzzalert — the mobile version (push to the iPhone) on the same
/v1/askscontract, so consumers change only the address.
Status
Planned. See docs/PLAN.md for the contract, stories and rollout.
Pairing — only your window can answer
The first time the ARS window opens on a Mac it asks for a 6-digit code, which ARS shows only as a macOS notification. The right code pairs that window; from then on only it can see waiting asks and answer them. ARS keeps only a hash of the window's key (~/Library/Application Support/ARS/owner.json).
Honest limit: pairing stops any program from simply calling ARS and answering its own ask. It does not stop a program that runs as you and drives your browser. When BID gives Mac sessions an identity, this moves to BID.
For the move into cardmem (cardmem-F406)
Start with docs/MIGRATION.md — what cardmem takes over, the mount, history import, sound/speech, the client and the switch-off order.
src/core/is the ask core (store, four states, single read, expiry, read_token). It imports onlynode:crypto; a test keeps it that way.src/contract.test.tsis the/v1/askscontract. Run it against any host:ARS_CONTRACT_URL=http://host:port bun test src/contract.test.ts. The owner-answer cases need an adapter (ARS:ARS_CONTRACT_PAIR_CODE); without one they are reported as skipped.
Using ARS in a host (the package)
Bun only. @broberg/auth-relay ships TypeScript source (engines.bun); a Node host cannot import it. Pin it exactly — it guards secrets.
import { createAsksRoute, webRoot } from '@broberg/auth-relay';
import { serveStatic } from 'hono/bun';
app.route('/ars', createAsksRoute({
auth: (req) => /* your agent auth */ ({ principal: 'agent-123', label: 'components' }), // null → 401
delivery: {
notify: ({ id, label, question, pageUrl }) => { /* tell the owner; never gets an answer */ },
isOwner: (c) => /* your owner session check */ true,
},
pageUrl: (id) => `https://host/ars/?ask=${id}`,
}));
app.get('/ars', (c) => c.redirect('/ars/')); // the window's URLs are relative
app.use('/ars/*', serveStatic({ root: webRoot, rewriteRequestPath: (p) => p.replace(/^\/ars/, '') }));
auth.labelis what the owner sees as the requester; an answer is only readable by theprincipalthat asked.- Put your own Host/JSON guard in front (ARS standalone uses
localRequestGuardfrom@broberg/local-guard/hono). - If
isOwnersays no, the window shows ARS' pairing screen — a host with its own login should send the owner to it instead. - Prove your host:
defineContractSuite({ baseUrl, adapter, agentHeaders, guards })anddefineNoPersistSuite(start)from@broberg/auth-relay/testing.