cmux-next: each phone is allowed on this Mac once, and can be removed
Stacked on https://github.com/manaflow-ai/cmux/pull/21897 (prompt suggestions), which stacks on https://github.com/manaflow-ai/cmux/pull/21896 (full-access switch). Until those land, this diff includes their commits.
Summary
This adds the Codex-style per-device step for phones, and Settings > Mobile becomes the one place that lists every phone allowed on this Mac.
- New Phones (
phone.newDevices, in Settings > Accounts > Phone Access next to the other Iroh phone settings) has two options: Ask on This Mac (default) or Any Phone on My Account (the old behavior).- With Ask, when a phone that isn't on the list connects for the first time, the Mac shows "Allow “Leo's iPhone” to use this Mac?". Escape means Deny, and Allow has no keyboard shortcut, so a stray Return can't let a phone in. Only the person can answer it (
debug.dialogrefuses it). - Allow adds the phone to the list. Deny holds for ten minutes. Several connections from the same phone share one question, at most two phones wait on a question at once, and an unanswered question closes as a Deny after two minutes.
- The gate fails closed:
PhoneAccessGate.admitsrefuses every phone until the app wiresphone.newDevicesinto it (before, an unwired gate admitted everyone). - Signed out, no phone gets in. An Allow that can't be saved lets nothing in. A Remove that lands while a connection is being admitted is checked again before the phone is served.
- With Ask, when a phone that isn't on the list connects for the first time, the Mac shows "Allow “Leo's iPhone” to use this Mac?". Escape means Deny, and Allow has no keyboard shortcut, so a stray Return can't let a phone in. Only the person can answer it (
- One list, two kinds of entry. There is one file: the iPhone host's
<state>/cmux-mobile-host/paired-devices.json(0600, per signed-in user). Both links read and write it throughMobileHostPairedStore.- A hostd entry is the cmux iOS app: the mobile service's device id plus its P-256 identity and Face ID approval keys.
- An iroh entry is cmux next: its id and identity key are the connection's TLS-proved endpoint key, and it has no approval key.
- Iroh entries never carry approval rights. hostd's
devicesline skips them, acpmux's approvers skip them (and any empty approval key), and a hostd request never matches one. - A file written before this change has no
transportfield, so every entry in it reads as hostd.
- Settings > Mobile is the one card. Each row names its app, "cmux (iOS)" or "cmux next", so one phone that uses both apps shows as two labeled rows, not a duplicate. Remove on a cmux next row (or
revokePhone) takes it off the list and closes its open Iroh sessions, and its next connection asks again. - Removed: the Accounts > Phone Access "Allowed Phones" card (Swift
Card.phonesand the layout'strail: [.phones], thecmux.settings.phones.removeop and its PageFactory wiring, the webviews card, op, store action, mock and category entry, and their three settings-window strings), the separatephone-access/paired-devices.jsonfile,AllowedPhonesandMobileHostService.removePhone. - Wiring:
CmuxNextPhoneAccessstill doesn't importCmuxNextMobileHost. The store lives inCmuxNextMobile;IPhoneHostServiceowns it (pairedStore) and the app hands it toMobileHostService.follow(_:launch:phones:)in the one existing AppDelegate line. CmuxNextApp shrinks by 8 code lines. - Strings: the two app labels are new in the IPhone table, and the Allow question now points to Settings > Mobile. Both are in all 21 locales.
- Long-term: (c) have the v2 directory carry the mobile-api device id, so one phone is one entry instead of one per app.
- Not in this PR: pairing from the phone with a code (QR or XXXX-XXXX) needs iOS work and comes later.
Testing
On head 26752c9ced4a67062d6cea4f89c446387f43ea7d:
cmux-ci compile-check --app --package CmuxNext(job a248390bb900bafdb8b8b96e): the app compiles. The CmuxNext package step fails only on the Linux lane's "no such module 'Testing'" (infra, 64 lines, no other error).package-test-lane.sh suite(mac lane, step beb396d6a286bf9c4dc367d4): all 8 passed: PhoneAdmissionTests, MobileHostPairingTests, PhoneAccessGateTests, PhoneAccessMigrationTests, PhoneAccessSettingTests, DebugDialogUserOnlyTests, SettingsSchemaExportTests, MobilePageProviderTests.- An earlier run at 5c905d83c9a (step c36f0fcacdaf2baa88364ca6) had MobilePageProviderTests exit with no error during
removeNeedsTheSettingsPageAndAGesture, and exports exported from a newer base. After a base merge and re-export, the suite passed alone (step 45642b764e49df772e86a5bf, 8/8) and in the full runs at e518389a8f6 (step 82fd4466e803b9dc16179225), 5d7b7c0cb45 (step 6e498438a02b48083a19381c) and above; base passed it too (step 6fc64c519e723e1df3950753). - New tests:
PhoneAccessGateTests.anUnwiredGateAdmitsNobody: an unwired gate refuses, a wired one admits exactly whom its decision admits, and unwiring refuses again.PhoneAdmissionTests, ported to the shared store. Remove closes the Iroh session and the next connection asks again; a hostd removal closes no Iroh session.MobileHostPairingTests.irohEntriesGetNoApprovalRights: the devices line and approvers skip Iroh entries and empty approval keys, and a hostd request never matches an Iroh entry.MobileHostPairingTests.anOldFileReadsAsHostdandtheStoreSavesThenReportsRemovals.MobilePageProviderTests.eachPhoneRowNamesItsAppAndRemoveForgetsAnIrohPhone.
- webviews:
bun testformobileCard,categoriesandaccountspassed 18/18 (one new test: a cmux next phone lists with Remove while iPhone connections are off).vp checkand every otherbun run checkstep passed.check:browser-lanesfails ontest/gallery-agent-pane-entries.test.ts, which this PR doesn't touch; that red is on base.check:fixed-waitsandcheck:text-focusfail at base 804cef3e5b76 on files this PR doesn't touch. - The settings exports come from the PR autofix run 39620032231. Its
ci-target-graph.jsonhunk was left out because only the base autofix writes that file (the generated-drift job refuses it).
Observed / Not run
- Observed: the fleet compile and suite results above, the webviews tests and checks,
prepush-gate.py, and the generated-drift check. - Not run: a real iPhone against a tagged build (the Allow dialog, a cmux next row's Remove closing a live session, and both apps on one phone), and the full webviews browser lane.
Capture
Capture steps-679dfcb80d at 320a417321ee (fleet build, capture mini, text checked for private data): Settings > Accounts, Phone Access, with the new New Phones row (Ask on This Mac / Any Phone on My Account). Both stills came out in the dark appearance; the mini did not switch to light.
| Light | Dark |
|---|---|
![]() | ![]() |
Changelog
Added: A new phone must be allowed on the Mac once; Settings > Mobile lists every allowed phone with its app and can remove it.
Agent session: cosmic-gray
🤖 Generated with Claude Code

