Skip to content

Settings > Devices: each paired phone gets its own acpmux credential and the Full access switch (off by default) - #25088

Merged
teamleaderleo merged 9 commits into
feat-cmux-nextfrom
hb-acpmux/phone-full-access-mac
Oct 25, 2026
Merged

teamleaderleo merged 9 commits into
feat-cmux-nextfrom
hb-acpmux/phone-full-access-mac

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Follows #25078 (merged, squash 5251e6b), which gave a paired phone its own acpmux credential (role phone) for the per-device Full-access setting of #25038. #25069 added the Settings > Devices switch for paired machines. This PR gives every paired phone a credential and puts the same switch on its row.

The ask

Leo, relayed by claude-sr2: "#2 seems reasonable i think... it's a gate that you can enable for your phone, which is an attack vector you opt in to".

claude-sr2's build order: a per-device setting, off by default, set from the Mac; revoking it takes effect at once; remote_guard is unchanged for a device without it; a test proves a device without the setting is refused.

Before / after (Settings > Devices, the page's mock data)

Captured headless (Playwright Chromium, on the fleet) from the Settings page's own dev server and mock provider, at 2x. Before is the mock as base sends it, where a phone row has no switch. After is the row the app now sends for a phone with its own record. The native Settings window was not captured, because Swift is not run on agent hosts. The page is the same React page the window shows.

No capture: the native files here carry no UI of their own. They are the phone credential plumbing: approver sync, credentials, hostd's line, and the directory join. The one visible change is the Settings > Devices row, which is a webview page, captured below from its own dev server. A fleet cmux-ci capture tour cannot show that row, because the row only exists for a phone paired with this Mac that has its own acpmux record.

Before After
devices phone before devices phone after

What changed

  • The iPhone host issues each paired phone its own credential (MobileHostApproverSync, on the connection it already proves as the person to set approvers):
    • Only the paired phones the account still lists get one (MobileHostPairedDevices.listed(in:)). Those are the phones that have a row, and so a switch. Until the first directory read succeeds, no phone gets one.
    • On each new connection it calls _acpmux/pair_issue {name: "phone-<link id>", role: "phone"} for each of those phones. acpmux replaces that phone's old record and keeps its setting.
    • The state lives in a value type, MobileHostPhoneCredentials, which is unit-tested.
    • The tokens go to hostd by phone id. The path is the supervisor's setPhoneCredentials, then hostd's phone_credentials stdin line. They are kept in memory only and sent again to each new hostd.
  • Revoking takes effect at once:
    • Its record is revoked once a phone is no longer paired or listed. That covers removal in Settings, sign-out on the phone or on this Mac, turning iPhone connections off (which ends every phone's setting), and removal from the account. pair_revoke puts its chats back in their policy's mode before it answers (acpmux: a paired machine's Full-access setting (off by default) lets it drive a remote chat in Full access #25038).
    • pair_list is the source of truth. A phone that leaves, and an unlisted record, are "ending" by record name. Each live record of that name is revoked, at once when its id is known. No credential goes under that name until a list, taken after its last issue and revoke, shows no live record of it. So a new record never inherits a setting meant to end, and an issue whose reply was lost is found by the next list.
    • Each new connection lists first, and issues wait for that list. Unlisted records are revoked only once the account's list has been read, or connections are off, so a launch never drops a saved setting.
    • A refusal drops the connection. The sync's existing backoff retries it, and the backoff is not reset while the refusal stands.
  • cmux device list: a phone pairing is listed as kind: phone with its full_access.
  • Settings > Devices (DeviceDirectory):
    • The phone's own acpmux record is not a row of its own.
    • The phone's row (phone:<link id>, the id it paired with, even when its presence id sorts first) carries that record's switch, off by default, with the same help line. The record is matched by kind phone and its exact name, never by name alone.
    • A phone with no record yet has no switch, and its row never reaches acpmux.
    • The provider's rule is unchanged: on needs the person's own click, spent once, and off needs nothing.
  • Webview: the mock phone row carries the switch, and the page test covers it.

Tests (red first)

Red commit fcce648 adds the tests only. On base the Swift suites do not compile: MobileHostLaunch.phoneCredentialsLine and MobileHostPairedDevices.acpmuxRecordName do not exist (fleet step c090acdc0056).

  • AddDeviceFlowTests.aPairedPhoneHasTheFullAccessSwitchOfItsOwnRecord checks:

    • the app and Settings agree on the record name;
    • the phone's record is not a row;
    • the phone row's switch is off by default;
    • a computer named like the phone's record is never matched;
    • on and off send the phone record's id;
    • a phone without a record never reaches acpmux.
      Each review round's fixes came with a red commit of their own: tests only, with the new API missing, so the Swift suites do not compile there. Round 1's tests are in a1856a6 (fleet step adb1fd8c03e0), round 2's in efebfce (step 9ad74ca20e9e), and round 3's in 9a0dd8e.
  • MobileHostPairingTests.aPairedPhoneIsItsLinkIDAndOnlyAListedOneGetsACredential: a phone's row and credential follow its link id even when its presence id sorts first, and a paired phone the account no longer lists gets no credential.

  • MobileHostPhoneCredentialsTests (7 tests) cover:

    • Launch, before the account's list is read: nothing is issued, and nothing is revoked for being unlisted, so a saved setting survives. Once the list is read, an unlisted record ends and the listed phone is issued after the list.
    • A phone paired again before acpmux heard its revocation is issued only after a list shows its old record gone, so the new credential starts off.
    • An issue whose reply was lost with its connection is found by name and revoked by the next connection's list.
    • Turning iPhone connections off ends the record. A refused revocation is retried on a new connection, and not_found counts as done.
    • A removal with nothing under way is found by a new list.
    • A phone listed again waits for its unlisted record to end.
    • An issue reply for a phone removed in the meantime is revoked.
  • MobileHostTests.phoneCredentialsAreOneControlLine checks the stdin line, and that the record name is valid for acpmux (at most 80 characters, [a-z0-9-]).

  • cmux-tui devices::tests::a_phone_pairing_is_a_phone_row_with_the_setting.

  • Webview hostSections.test.tsx checks the phone row's switch and its {id: "phone:p1", on: true} call.

Fleet:

  • Swift (cmux-ci run, package-test-lane): 4 suites passed at b079a57 (AddDeviceFlowTests, SettingsDeviceFullAccessGestureTests, MobileHostTests, MobileHostPairingTests). At the final head b094515, all 5 suites passed (fleet step 6852c06ed7b6), with CmuxNextMobileHostTests included (21 tests in 6 suites).
  • cmux-tui devices 17/17 and fmt clean.
  • Webview hostSections.test.tsx 11/11 and vp fmt --check clean.

Review

Codex gpt-6-astra xhigh, an independent reviewer. It reviewed the acpmux and hostd half (#25078) to SHIP. On this half, rounds 1 to 3 were BLOCK:

  • Round 1: a row could carry the presence id; a phone removed while the app was stopped kept a credential; revocations were fire-and-forget.
  • Round 2: a re-issue could overtake a revocation; a lost issue reply left nothing to clean up; a refusal never retried; startup could drop saved settings.
  • Round 3: a removal with nothing under way was never listed; unlisted revocations were not tracked by name; connections off left records live.

Each round's findings were fixed with tests, above. Round 4 was SHIP at b094515.

Risk: this decides which phones hold an acpmux credential, and so which phones a Full-access setting can cover. It only narrows. A phone without its own record falls back to the dashboard token, which the phone filter already restricts to modes that ask.

Not in this PR

The iPhone's mode menu still offers only modes that ask (ACPMode.phoneChoices). With the setting on, the phone already drives a chat in Full access: it can prompt a Mac-started chat in Full access, and switch modes through acpmux. Offering Full access in the phone's own menu (acpmux's initialize now says fullAccess) is an iOS follow-up.

Stop conditions

No new timer, observer or loop. The issue and revoke calls ride the approver sync's existing connection, its reconnect and its backoff. The credential set is replaced whole on every change.

Agent session: big-red
🤖 Generated with Claude Code

teamleaderleo and others added 3 commits October 25, 2026 00:17
…and the Full access switch

Leo: "#2 seems reasonable i think... it's a gate that you can enable for
your phone, which is an attack vector you opt in to".

- The iPhone host's approver sync (its proven person line) issues each
  paired phone its own acpmux credential (pair_issue role phone) and hands
  hostd the set (phone_credentials); a phone no longer paired has its
  record revoked at once, and each new connection revokes phone records no
  paired phone owns.
- cmux device list shows a phone pairing as kind phone with its setting;
  Settings joins it to the phone's row (by kind and name), so the phone row
  gets the same Full access switch, off by default, and its record never
  shows as a row of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review b094515: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr25088 -f ref=hb-acpmux/phone-full-access-mac -f platform=macos -f backend_mode=local

Unclear: web / react-apps-check failed: no known signature; failed step: Check webview test shards. This PR merged before its CI finished: fix forward on main.

CI failed on b094515111 (run 40233296710 attempt 1): 1 code, 1 unknown.
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped.

Job Verdict Why
web / react-apps-base-check (3) code a web test failed
web / react-apps-check unknown no known signature; failed step: Check webview test shards

Failures:

  • seen on other PRs agent-pane-queue-collapse.test.ts a long queue > an opened long queue keeps Show less and the focused row in sight, never a half row (...: failed (job)
  • seen on other PRs agent-pane-queue-collapse.test.ts a long queue > an opened queue with a two-line row shows whole rows only, no row under another (capt...: failed (job)
  • seen on other PRs agent-pane-queue-collapse.test.ts a long queue > a folded queue with a long row opened shows only whole rows above the fold (capture, ...: failed (job)
  • seen on other PRs composer-glass.test.ts light, an opaque window: the input box is translucent glass with a light rim: failed (job)
  • seen on other PRs composer-glass.test.ts light, art under Plain glass: the input box is translucent glass with a light rim: failed (job)
  • seen on other PRs composer-glass.test.ts light, art under Lens glass: the input box is translucent glass with a light rim: failed (job)
  • seen on other PRs composer-glass.test.ts dark, an opaque window: the input box is translucent glass with a light rim: failed (job)
  • seen on other PRs composer-glass.test.ts dark, art under Plain glass: the input box is translucent glass with a light rim: failed (job)
Matched log lines
web / react-apps-base-check (3): (fail) a long queue > an opened long queue keeps Show less and the focused row in sight, never a half row (capture, 2026-10-15) [1205.19ms]

Not re-run automatically: web / react-apps-base-check (3), web / react-apps-check are not machine failures.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

red: web / Web status; NEW: green on the base 5251e6b; do not merge until green; see lane-rules
red: web / react-apps-base-check (3); no finished base run on 5251e6b to compare; do not merge until green; see lane-rules
red: web / react-apps-check; no finished base run on 5251e6b to compare; do not merge until green; see lane-rules

Gallery before/after for b094515: 16 states rendered, 7 images differ, 1850 more touched states not rendered. Images are in the run's artifact, not on this public PR.

gh run download 40233296679 -R manaflow-ai/cmux -n gallery-advisory-25088-b09451511149ea2e65549e966b2f9d3a49cffbc2 (then read gallery-review.json)

States
State Theme Status Diff Picked by
agent-pane.add-project-panel/environment Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.add-project-panel/environment Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.add-project-panel/sources Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.add-project-panel/sources Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.add-project-panel/directory Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.add-project-panel/directory Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.add-project-panel/keyboard Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.add-project-panel/keyboard Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.addressed-message/relayed Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.addressed-message/relayed Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/sent Apple System Colors Light changed 0.0037% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/sent Apple System Colors changed 0.0028% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/sent-subagents Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/sent-subagents Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/sent-hover Apple System Colors Light changed 0.0043% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/sent-hover Apple System Colors changed 0.0033% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/sent-opened Apple System Colors Light changed 0.0037% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/sent-opened Apple System Colors changed 0.0031% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/sent-opened-long Apple System Colors changed 0% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/sent-opened-long Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/outcomes Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/outcomes Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/both-directions Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/both-directions Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/both-directions-opened Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-message-row/both-directions-opened Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-pair-note/paused Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-pair-note/paused Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-pair-note/held-list Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-pair-note/held-list Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-pair-note/repeating Apple System Colors Light unchanged 0% imports mockProvider.ts (2 hops)
agent-pane.agent-pair-note/repeating Apple System Colors unchanged 0% imports mockProvider.ts (2 hops)

Pick other states with a Gallery: entry.id, entry.id/variant line in the PR body.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by hb-acpmux on big-red · session

@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 25, 2026
@blacksmith-sh

blacksmith-sh Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Found 13 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpjpkclcd_/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpxe_37xnz/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
bench OTHER_SWIFT_FLAGS is 'OTHER_SWIFT_FLAGS=$(inherited) -DCMUX_BENCH_HOOKS -DCMUX_BL
OCKING_CHECKS'/
bench OTHER_SWIFT_FLAGS is 'OTHER_SWIFT_FLAGS=$(inherited) -DCMUX_BENCH_HOOKS -DCMUX_BL
OCKING_CHECKS'
View Logs
bench OTHER_SWIFT_FLAGS is 'OTHER_SWIFT_FLAGS=$(inherited) -DCMUX_BENCH_HOOKS -DCMUX_BL
OCKING_CHECKS'/
bench OTHER_SWIFT_FLAGS is 'OTHER_SWIFT_FLAGS=$(inherited) -DCMUX_BENCH_HOOKS -DCMUX_BL
OCKING_CHECKS'
View Logs
convention violations found (ERROR lines above)./
convention violations found (ERROR lines above).
View Logs
nightly.yml prepare-nightly-variant: 'Package prepared app for signing' is not in list/
nightly.yml prepare-nightly-variant: 'Package prepared app for signing' is not in list
View Logs

...and 3 more test failures. View all on Blacksmith

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-5ahgd created for this PR by the cmux coordinator.

teamleaderleo and others added 2 commits October 25, 2026 13:33
…n unlisted phone gets no credential, and revocations hold until confirmed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red
…ets a credential, and revocations retry until acpmux confirms them

Review findings on #25088: the row used the phone's first-sorting id (possibly its presence id),
so it found no record; a phone removed while the app was stopped stayed paired locally and was
issued a credential with no row to switch it off; and a revocation lost with its connection was
never sent again. MobileHostPhoneCredentials keeps the credentials as a value: phones come from
the paired phones the account lists, the first connection lists acpmux's phone records even with
nothing else to sync, and a revocation stays pending (and asks for a connection) until acpmux
answers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red
teamleaderleo and others added 2 commits October 25, 2026 13:43
…d record to end, a lost issue is found by the next list, a refusal retries, and an unread account list revokes nothing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red
… a re-pair, a lost reply or a refusal never leaves a record live

Round-2 review findings on #25088:
- A phone that leaves is removed until a list, taken after its last issue and revoke,
  shows no live record of its name; only then can it get a new credential, so
  pairing again never inherits the old setting, and an issue whose reply was lost
  is found by the next list.
- Issues wait for the connection's list.
- A refusal drops the connection, and the backoff retries it.
- Until the account's list is read, no record is revoked for being unlisted, so a
  saved setting survives the launch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red
@cursor

cursor Bot commented Oct 25, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next guards / nightly-guards / workflow-guard-tests / release-notary is red on 10 of 10 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo and others added 2 commits October 25, 2026 13:58
…s listed, a phone listed again waits for its unlisted record, and connections off end the records

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red
…nections off ends them

Round-3 review findings on #25088:
- An unlisted record being revoked blocks a new credential under its name, the
  same as a phone that left, until a list shows it gone.
- A removal with nothing under way owes a list at once.
- With the setting read off, no phone may hold a credential, so every phone record
  ends. Before the setting or the account's list is read, which phones may is
  still unknown, and nothing is revoked for being unlisted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a16c8c-54ec-71b2-a0fa-125d1404dd72&host=big-red
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next / cmux-next checks (god files, concurrency, crash safety, l10n) › Script tests (pin-cmux-tui, embed-cef, ensure-cef store) is red on 9 of 14 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo
teamleaderleo merged commit 77727e2 into feat-cmux-next Oct 25, 2026
736 of 760 checks passed
@teamleaderleo
teamleaderleo deleted the hb-acpmux/phone-full-access-mac branch October 25, 2026 21:28
@github-actions

Copy link
Copy Markdown
Contributor

Shipped in:

This branch was previously deployed

4 inactive (2 outdated) deployments
ci-routing-agent — b0945151 Deployed Oct 25, 2026 by github-actions[bot] via cmux-next macOS placement #34654
cmux-tui-merge-tree — b0945151 Deployed Oct 25, 2026 by teamleaderleo via publish the merge tree #5704
artifacts — 4ea7e497 Deployed Oct 25, 2026 by teamleaderleo via add the Windows daemon to the tree #52027
sentry-dsyms — 4ea7e497 Deployed Oct 25, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #52027
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant