cmux-next iPhone: identify a Mac by its machine id, never its name
#24117 hid a Mac from the iPhone app's Mac filter, Workspaces and Home in two cases:
- It was offline and another Mac of the same owner with the same name was online or seen later.
- It was unseen for 30 days.
A name is not an identity: "theoretically I could have several Macs with the same name." The duplicate it was meant to fix ("Abdulaziz's MacBook Pro (2) 39C6" offline beside "… 3EB7") came from two cmux builds on one MacBook: a deleted test build (dev.cmuxterm.app.next.iostest) and the nightly (com.cmuxterm.app.nightly.cmux-next). Each build keeps its own device id in its own Application Support folder, so each registered as its own device.
Design
Each Mac now says which computer it runs on (a machine id). The phone groups one owner's Macs by machine id and never by name.
Choice: builds stay separate devices, grouped on the phone (option b), not one shared device record (option a). One shared device id is not safe:
- Signaling. The user hub keeps one socket per device id. Two running builds with one id would close each other's socket (
4000 replaced) in a loop. - Trust. A phone pins the Mac identity key per Mac device id (PROTOCOL.md "Mac identity"). Each build keeps its key in a Keychain item named by its bundle id. Sharing one device id would either break every pin when the other build answers, or require sharing one key across builds, letting a Debug build hold the key phones trust for the release build.
- Keeping each build separate keeps pairing, approvals and removal per build, exactly as today.
What the phone does with it:
- It lists one Mac per (owner, machine id): the most recently active build (online first, then last seen, then the id).
- The other builds stay in Settings → Devices, marked "Another cmux build on this Mac", where Remove deletes them.
- A Mac with no machine id (an older host) is never folded into another.
- Macs that share a name stay separate, with the existing
MacNameDisambiguationtag. - The 30-day rule is gone.
Machine id. HMAC-SHA256 under the key cmux-mobile machine-id v1 of <owner sub> NUL <host UUID> (gethostuuid, the IOPlatformUUID), first 16 bytes in lowercase hex.
- The raw hardware id never leaves the Mac and is never logged;
HostUuid'sDebugis redacted. - Every build on one Mac computes the same value without sharing a file.
- Two accounts on one Mac get unrelated ids.
- A spoofed machine id can only group the spoofer's own Macs.
The user's stale record. "… 39C6" will never register again (its build is deleted), so nothing would ever give it a machine id. hostd therefore also sends siblingDeviceIds: the device-id files of other builds in ~/Library/Application Support (<bundle>/cmux-mobile-host, <bundle>/cnx-host, at most 16).
- The Worker gives the registering Mac's machine id to each sibling that is the caller's own Mac and has none yet. It never touches another owner's device, a phone, or a Mac that reported its own id.
- So when the nightly re-registers, 39C6 is listed as an older build of that Mac. No production rows are deleted by hand.
- hostd writes a
machine-idmarker beside its device id. A sibling folder whose marker names another machine (Migration Assistant, a restore) is skipped.
Changes
- hostd (Rust):
machineIdandsiblingDeviceIdsonPOST /v1/devices.CMUX_MOBILE_MACHINE_ID(validated) andCMUX_MOBILE_APP_SUPPORT_DIRfor tests. - Worker:
devices.machine_id, migration0003_device_machine_id(one nullable column).- Validation: 32 lowercase hex, Mac only.
siblingDeviceIdsonly with amachineId, at most 16 UUIDs. OmittingmachineIdkeeps the stored one. - The hub stores it. Every Mac list,
helloandpresencecarries it, and omits the key for a Mac without one, so older clients see the same shapes.
- iPhone:
MacDevice.machineID;MacRostergroups by machine id.- Links and focus follow the listed Macs. Another build of a listed Mac still links while something names it (a Feed answer or notification of that build,
touch), so its asks are answered on it. - Tags and Settings marks follow each team's list.
- Mac app: a Debug-only
CMUX_NEXT_MOBILE_DEVICE_NAME(passed withopen --env) so the rig can give two Macs one name. Release builds ignore it. - PROTOCOL.md: "Machine id" under Identity, plus the new fields.
- verify/multimac:
multimac_machine_id.py;rig.pytakesMULTIMAC_DEVICE_NAME.
Migration ordering. The open rich-push worker branch adds 0003_push_settings. Whichever lands second renumbers to 0004 and takes a later journal when (now in the backend README). This migration's when is its real creation time.
Tests
The failing tests were committed first (7cb05323a76d) and run on that commit:
- Swift:
cmux_mobile_kit-test-ios.sh CmuxMobileModelTests/MacRosterTests CmuxMobileModelTests/SettingsRowsTests CmuxMobileLiveTests/MacMachineIDLinkTests, iOS simulator on mini-cmux11s. The base fails to compile: nomachineID, noSettingsDeviceRows.otherBuilds. - Rust:
rbx -- cargo test --locked -p cmux-mobile-host --test machine_id. The base fails to compile: nomachine_id,sibling_device_idsorDeviceRegistration.machine_id. - Worker:
vitest run test/machine-id.test.tson a mini. 7 of 7 fail on the base: the machine id is not stored, listed or validated, and siblings are not claimed.
On the head (ca5861b4e295 and later):
- Swift (iOS 26.5 simulator, mini-cmux8s; mini-cmux11s was out of disk), all pass: CmuxMobileModelTests 173, CmuxMobileLiveTests 246, CmuxMobileChiefTests 67, CmuxMobileWorkspacesTests 101, CmuxMobileShellTests 3. New ones:
MacRosterTests: two builds on one Mac listed once; same-named Macs on two computers both listed; a Mac without a machine id never hidden; no 30-day rule; owner scoping; stable choice; per-team.SettingsRowsTests: other builds marked, per team.MacMachineIDLinkTests: one link for two builds; two links for two same-named computers; the other build links when touched; decoding.
- Rust (rbx):
machine_id7,cmux-mobile-link --lib75,cmux-mobile-host --lib82, all pass.clippy -D warningson both crates is clean. - Worker:
tscclean,vitest run75 of 75 pass, including the hub catch-up after a missed claim. prepush-gate.py: no new violations.
Observed / Not run
- Observed: everything above, plus the dev deploy and smoke and the live check (comments below).
- Not run:
- The user's own phone (no TapKit).
- The macOS CmuxNext packages' tests. The only Mac app change is the Debug-only name override in
IPhoneHostService+Wiring.swift; the fleet build compiles it.
No capture: this changes the iOS app's Mac lists, not Mac UI. The live-check stills are listed in the comment below.
Changelog
- Fixed: The iPhone app lists each Mac once even when several cmux builds run on it, and never hides a Mac because another has the same name.
🤖 Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
The iPhone app now identifies a Mac by its machine id rather than its name, so several cmux builds on one computer appear as one Mac, and same-named Macs on different computers stay separate. The previous behavior — hiding an offline Mac when another Mac had the same name and owner, and hiding any Mac unseen for 30 days — is gone.
- Every build on a Mac registers a machine id (HMAC-SHA256 of the owner sub and the host UUID, never the raw hardware id) and names the other builds it finds on the same machine.
- The worker stores the id (migration
0003_device_machine_id), validates it, carries it in every Mac list and presence frame, and gives a registering Mac's id to its siblings, so a deleted build's record is listed as this Mac's. - The phone groups one owner's Macs by machine id and lists the most recently active build; other builds stay in Settings → Devices, marked "Another cmux build on this Mac", and a Mac without a machine id is never folded in.
- Builds stay separate devices: one shared device id would let two running builds close each other's signaling socket and would let a debug build hold the identity key phones pin for the release build.
- Links, pickers, and online counts follow the listed Macs.
- The Mac app adds a Debug-only name override for the multimac rig, which now checks that the phone lists one Mac per computer.
Migration
- Deploy backend migration
0003_device_machine_id(one nullable column). - If the open push-settings migration lands first, renumber this one to
0004and take a later journalwhen.