Skip to main content

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

RouteDoes
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-ticketAuthenticated. Mints a single-use, 60-second stream ticket.
GET /api/stream?ticket=…

Server-sent events: snapshot frames on every beat, alert frames on transitions. There is no token parameter.

GET /api/session-stream?session=…&ticket=…

The live terminal: server-sent screen, output, resize, and exit frames carrying one session's raw pty bytes. Ticket-authenticated like the fleet stream, and gated at approve — watching a terminal is over-the-shoulder reading.

GET /api/state

Authenticated snapshot of the session tree, redacted to the token's tier.

POST /api/controlOne verb, refused if it sits above the token's tier.
POST /api/session-input

The live terminal's keystroke channel — the input verb only, on its own rate-limit budget so typing isn't mistaken for a flood. Needs drive.

GET /api/push/keyThe 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

VerbTierEffect
focusapproveBring a session forward on the Mac.
approveapproveAnswer a waiting prompt with its own default. Bounded.
approveAllapproveThe same, for every session currently waiting.
answerapprove

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 — approve takes the first option, which both CLIs reserve for the conservative yes.

continueapproveNudge one stalled session onward.
continueIdleapproveNudge every idle agent at once.
senddriveType arbitrary text into a session. Unbounded, hence the tier.
inputdrive

The live terminal's raw keystrokes (base64) — arrows, Ctrl-C, a whole TUI. Unbounded like send, and the same tier.

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.