Wire protocol
The companion speaks one small JSON protocol and nothing else. It is
declared once in companion/lib/protocol.ts
and implemented on the Mac by the Swift control server. Contract tests
on both sides decode the same fixture file, so a field renamed on one
side fails the build on the other.
Endpoints
| Route | Does |
|---|---|
| POST /api/pair | Offer a code and a requested tier. Answers 202 with a handle while the Mac asks a human; 401 for a wrong code, 429 when throttled or already busy. |
| POST /api/pair/claim | Poll the handle. 202 while the account check (and, on a Mac set to ask, the prompt) is undecided; 200 with a token and the granted tier once authorized; otherwise a refusal carrying its reason — an account mismatch, a denial, or a lapse. |
| POST /api/stream-ticket | Authenticated. Mints a single-use, 60-second stream ticket. |
| GET /api/stream?ticket=… | Server-sent events: |
| GET /api/session-stream?session=…&ticket=… | The live terminal: server-sent
|
| GET /api/state | Authenticated snapshot of the session tree, redacted to the token's tier. |
| POST /api/control | One verb, refused if it sits above the token's tier. |
| POST /api/session-input | The live terminal's keystroke channel — the
|
| GET /api/push/key | The Mac's VAPID public key, for a push subscription. |
| POST /api/push/(un)subscribe | Register or drop a browser push subscription, owned by the calling grant. |
Verbs
| Verb | Tier | Effect |
|---|---|---|
| focus | approve | Bring a session forward on the Mac. |
| approve | approve | Answer a waiting prompt with its own default. Bounded. |
| approveAll | approve | The same, for every session currently waiting. |
| answer | approve | Answer a waiting prompt with a chosen option, by its menu
number. Bounded the same way: the Mac refuses a choice its live
menu doesn't offer, off the same read that produced the
buttons. This is how a device says no —
|
| continue | approve | Nudge one stalled session onward. |
| continueIdle | approve | Nudge every idle agent at once. |
| send | drive | Type arbitrary text into a session. Unbounded, hence the tier. |
| input | drive | The live terminal's raw keystrokes (base64) — arrows, Ctrl-C,
a whole TUI. Unbounded like |
Snapshots
A snapshot carries the Mac's name, the time it was produced (which doubles as the liveness signal), the Mac's Marginal Utility account if it has one, and the sessions. Each session carries its status band, the agent detected in it, whether an answerable prompt is waiting, git state, its last activity, and — above the view tier — the last few transcript lines, the question a waiting prompt is asking, and the choices its menu offers.
Those last two are read off the transcript, so the view tier loses them with it. A grant that couldn't read the tail but could read the question in it — and the filenames and commands a question carries — would be a redaction in name only.
Optionality is part of the contract, not an accident: a client must render correctly when the account is absent, the transcript tail is missing, and the project path is empty, because that is exactly what a view-tier grant receives.