Skip to content

feat(desktop): Build machines tab and build request approval - #1839

Merged
janicduplessis merged 7 commits into
mainfrom
feat/1806-desktop-build-machines
Sep 28, 2026
Merged

janicduplessis merged 7 commits into
mainfrom
feat/1806-desktop-build-machines

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Phase 3 of build offload (#1802). Phase 1 (#1809) added the build capability and request-and-approve pairing, but the only surfaces were stim doctor --fix on the client and stim-server devices grant <id> --build on the worker. Phase 1 also left build clients in the Phones list with an Allow control checkbox that the server refuses for them (grant --read|--control on a build client fails with a build mismatch).

Solution

Client Mac: Settings > Build Machines. A new tab lists the offload.machines entries with each one's state, and the other online Macs on the tailnet whose tailscale serve route on 7443 answers as stim-server, each with Use for Builds. The tab only runs the CLI: stim settings set|unset offload.machines ... --scope machine to add or remove, and stim doctor --json --platform ios [--fix] for the state and to send the request. --platform ios keeps --fix from running the Android .cxx cleanup in the checkout doctor runs in (the first workspace stim status lists). --fix is not per machine, so asking one Mac also re-asks any other listed Mac that has not approved; the tab says so. Removing a Different Mac also runs --fix, otherwise its stale pin survives and adding it back still shows it as a different node.

Two small additions make that possible:

  • stim doctor --json gains buildMachines: [{ machine, state, dnsName?, deviceId?, requestedAt? }]. Findings could not carry it: an approved machine produces no finding, and the titles are prose. States: approved, pending, not-asked, revoked, node-changed, not-on-tailnet, tailscale-off, unreachable, invalid. Documented in guide facts and website/docs/commands.md.
  • stim-server answers GET /health from a tailnet peer (through tailscale serve or on a tailnet address) with only { server, version, protocol }, unless the request carries Origin or Sec-Fetch-Site, which a DNS-rebound web page's fetch would. Before, that request got 426, so there was no way to find stim-server on a peer without a WebSocket hello, and an unauthenticated hello counts against the failed-attempt limiter. stimHome, the Tailscale state and the route stay loopback-only.

Worker Mac: the approval. Nothing on the loopback protocol lists pending requests, so the app polls stim-server devices --json every 10 s while a server runs (about 80 ms of CPU per run). A new request adds an inbox entry " wants to build on this Mac" (new category A Mac asks to build here) and shows a card or a macOS notification; the card and notification are withdrawn once the request is answered or lapses. Review opens an NSAlert with the name, tailnet node and user, request id, lapse time, and what building here allows. Allow runs devices grant <id> --build and is not the default button (Return does nothing, Escape is Later); Deny runs devices revoke <id>. Names reach AppKit and SwiftUI only as plain strings or Text(verbatim:), never as a Markdown-interpreting LocalizedStringKey. The Phones tab now lists build clients in their own Macs that build here section, with Review... / Deny while pending and Revoke once approved, and no read/control checkbox.

Things a reviewer should weigh:

  • Build requests default to Alert. fix(notify): default every notification category to Silent #1825 made every category start Silent. A build request is the exception: it waits on an answer from this Mac and lapses after 15 minutes. If it started Silent, a fresh worker would only list the request in the inbox. The level picker still covers it.
  • A hidden stimServerPort default (defaults write, no UI) moves the port Desktop runs or looks for stim-server on. Test copies need it so they never adopt the Mac's own server on 7787; unset, nothing changes.
  • doctor --fix also applies the sandbox allowance when Desktop's environment names a Claude Code harness (Desktop launched from a Claude session). Outside that it only sends build requests with --platform ios.
  • While a machine is pending, the tab re-runs the full stim doctor --json --platform ios (about 1 s) every 15 s, for at most the 15 minutes a request lives, and only while the tab is open.
  • Discovery only probes port 7443. A worker served on another port still works by name in offload.machines, but the list does not show it.

Fixes #1806

Test plan

Two Macs, both on the tailnet. The worker was a Desktop test copy on the Mac mini with a scratch STIM_HOME, its own stim-server on 7797 and a temporary tailscale serve --https=7443 route. The client was this MacBook with a scratch STIM_HOME. Then the roles were swapped so the client UI could be driven on the mini, against a stim-server on this MacBook (port 7797, temporary route on 7445; the maintainer's 7443 route and server were not touched). Each state was exercised in the UI and confirmed on the other side:

  • Request: stim doctor --fix from the client created the pending request. The worker added the inbox entry, a macOS notification (window behind) or a card (window in front), and a Waiting for you row.
  • Allow, from the dialog: stim-server devices lists it as build, and the client shows Approved (the tab re-checks on its own every 15 s while pending).
  • Revoke, from the Phones list, with confirmation: the client shows Revoked. Ask Again made a new pending request.
  • Deny, both from the list and from the dialog: the request is gone and the client shows Revoked.
  • Not asked, Waiting for approval, Approved, Revoked and Different Mac in the client tab. Different Mac was produced by editing the scratch build-machines.json pin. Remove then emptied the pins and unset the setting.
  • Peer probe: curl https://janics-mac-mini.<tailnet>.ts.net:7443/health from this Mac returned {"server":"stim-server","version":"1.14.0","protocol":1}. The client tab on the mini correctly listed no peer: this Mac's 7443 serves a main-built server, which still answers 426.

Not covered: clicking Use for Builds on a discovered peer in the UI. No Mac on 7443 ran this branch's server while the other Mac ran the client UI, and computer-use access to the client copy on this MacBook was denied. The same two CLI calls it makes were run by hand.

pnpm test, swift test, format, lint, typecheck and knip pass. New tests: the buildMachines states and their pin handling (build-machines.test.ts), the peer health payload (server.test.ts), and on the Swift side the tailnet peer filter, doctor state decoding (including an unknown future state), setting edits, splitting build clients from phones, and the inbox target round trip (BuildMachinesTests).

Light Dark
Worker: request card (inbox)
Worker: review dialog
Worker: pending in Phones
Worker: approved, revoke confirm
Client: not asked
Client: waiting
Client: approved
Client: revoked
Client: different Mac

The light Different Mac shot predates a copy change; the dark one shows the current text.

- Refuse the peer health probe from a web page (Origin or Sec-Fetch-Site).
- Withdraw a pending macOS notification too, mark an answered request read, and open the review dialog after the notification response returns.
- Offer Ask for a machine that never answered, keep polls from overlapping a doctor run, and say when Remove runs doctor --fix.
- Document that build requests alert by default and the stimServerPort default.

Refs #1806
@janicduplessis
janicduplessis marked this pull request as ready for review September 28, 2026 22:13
@janicduplessis
janicduplessis merged commit f0fbc24 into main Sep 28, 2026
9 checks passed
@janicduplessis
janicduplessis deleted the feat/1806-desktop-build-machines branch September 28, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build offload phase 3: Desktop Build machines and worker approval

1 participant