feat(desktop): Build machines tab and build request approval - #1839
Merged
Merged
Conversation
…Deny for a pending request Refs #1806
- 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
…machine state Refs #1806
janicduplessis
marked this pull request as ready for review
September 28, 2026 22:13
This was referenced Sep 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Phase 3 of build offload (#1802). Phase 1 (#1809) added the
buildcapability and request-and-approve pairing, but the only surfaces werestim doctor --fixon the client andstim-server devices grant <id> --buildon 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|--controlon a build client fails with a build mismatch).Solution
Client Mac: Settings > Build Machines. A new tab lists the
offload.machinesentries with each one's state, and the other online Macs on the tailnet whosetailscale serveroute on 7443 answers as stim-server, each with Use for Builds. The tab only runs the CLI:stim settings set|unset offload.machines ... --scope machineto add or remove, andstim doctor --json --platform ios [--fix]for the state and to send the request.--platform ioskeeps--fixfrom running the Android.cxxcleanup in the checkout doctor runs in (the first workspacestim statuslists).--fixis 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 --jsongainsbuildMachines: [{ 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 inguidefacts andwebsite/docs/commands.md.GET /healthfrom a tailnet peer (throughtailscale serveor on a tailnet address) with only{ server, version, protocol }, unless the request carriesOriginorSec-Fetch-Site, which a DNS-rebound web page'sfetchwould. Before, that request got 426, so there was no way to find stim-server on a peer without a WebSockethello, 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 --jsonevery 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 anNSAlertwith the name, tailnet node and user, request id, lapse time, and what building here allows. Allow runsdevices grant <id> --buildand is not the default button (Return does nothing, Escape is Later); Deny runsdevices revoke <id>. Names reach AppKit and SwiftUI only as plain strings orText(verbatim:), never as a Markdown-interpretingLocalizedStringKey. 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:
stimServerPortdefault (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 --fixalso 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.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.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 temporarytailscale serve --https=7443route. The client was this MacBook with a scratchSTIM_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:stim doctor --fixfrom 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.stim-server deviceslists it asbuild, and the client shows Approved (the tab re-checks on its own every 15 s while pending).build-machines.jsonpin. Remove then emptied the pins and unset the setting.curl https://janics-mac-mini.<tailnet>.ts.net:7443/healthfrom 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: thebuildMachinesstates 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).The light Different Mac shot predates a copy change; the dark one shows the current text.