Skip to content

Settings > Devices: a per-device Full access switch for acpmux-paired client machines - #25069

Merged
teamleaderleo merged 8 commits into
feat-cmux-nextfrom
hb-acpmux/remote-full-access-device-ui
Oct 25, 2026
Merged

teamleaderleo merged 8 commits into
feat-cmux-nextfrom
hb-acpmux/remote-full-access-device-ui

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

Follows #25038 (merged): acpmux's per-device Full-access setting (pairing.json fullAccess, _acpmux/pair_set, full_access in pair_list).

The ask, word for word:

Build it: a per-device setting (off by default) that lets a paired phone drive a Full-access chat. Turning it on is an explicit choice on the Mac, per device, and shows what it allows. Revoking it takes effect immediately. Without the setting, remote_guard stays as it is. Update tests/remote_guard_codex_bridge.rs so the 2 base-red tests cover both states (setting off refuses, setting on allows), and add a test that a device without the setting is still refused.

Leo, relayed: "#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 brief also said: "Find the Mac UI that lists paired devices ... and add the per-device toggle with that explanation there. If the UI is a separate surface, make it a second small PR after the acpmux one." This is that second PR.

Which devices get the switch

Only acpmux-paired client machines (rows with id paired:<acpmux id>) get the switch. Phones don't reach acpmux with a per-device credential yet: a phone paired with this Mac talks to the Mac app, not to acpmux as its own pairing. acpmux has no per-device record for a phone that could carry the setting, so a phone row has no switch. Peers, logins, read-only and daemon pairings don't get one either. The switch is off by default, because a pairing without fullAccess reads as off.

What changed

  • cmux-tui device list --json: each acpmux client pairing carries full_access (from pair_list; a missing value reads as false). Other rows omit the field.

  • Swift (CmuxNextServer):

    • DeviceListing.Device.fullAccess decodes it.
    • DeviceListRow and the page JSON carry full_access plus full_access_help: "Its chats edit files and run commands here without asking".
    • The brief asked for "Lets NAME run chats in Full access: the agent edits files and runs commands on this Mac without asking." Settings draws no description line over 60 characters in any language (Leo, 2026-10-11, enforced by rowSummary.test.tsx), and that sentence is about 104. The help line is the device's own row under its name, so the shortened line drops the name and says what Full access allows. Every locale fits.
    • DevicesHostState.full_access_title is "Full access".
    • DeviceDirectory.setFullAccess(id:on:) strips paired:, calls the injected pair_set, records an error, and reads the list again. A row without the setting never reaches acpmux.
  • Swift (CmuxNextApp):

    • The injected pair_set opens an AcpmuxPersonLine to this app's acpmux, the same proven-as-the-person connection the approval banner uses.
    • It refuses to turn the setting on when the line is not the person, and calls _acpmux/pair_set {id, fullAccess}.
    • cmux.settings.devices.full_access {id, on} on the Settings provider reaches it.
    • On needs the person's own click or key in Settings (context.userGesture, from the host's record, never the page's word), and that gesture is spent once. So page script in the Settings WebView alone never turns a machine's Full access on (Codex review). Off needs no gesture: it only narrows what the machine may do, so a revoke always works, assistive technology included.
    • acpmux itself also refuses an on without the person proof (permission.person_required).
  • Errors: the person sees one of two messages:

    • "Still off: this Mac could not confirm it was you"
    • "Full access could not be turned off. Try again."

    AcpmuxPersonLine.call drops the error object, so full_access_role and not_found show these same messages.

  • Revoking: turning the switch off takes effect at once. acpmux's pair_set off holds that machine's running chats' policies again (held_again, acpmux: a paired machine's Full-access setting (off by default) lets it drive a remote chat in Full access #25038), and the list is read again.

  • Webview (Settings page):

    • The device row shows a "Full access" line with the help sentence and a Switch. The switch is disabled while its call runs.
    • Toggling it calls the op, then refreshes the host lists.
    • The mock provider gains a paired client row.
  • Strings: four new AddDevice.xcstrings keys, translated for all 21 locales. The localization ratchet's counts for the catalog are unchanged. This is most of the line count: 524 of the 834 added lines are catalog JSON (21 locales), and the code and tests are about 310 lines.

Tests

  • Rust: devices::tests::client_and_read_only_pairings_are_devices_that_use_or_watch_this_one checks:
    • a client pairing carries its setting;
    • a read-only pairing and a peer have none;
    • a pairing without the field reads as off.
  • Swift: AddDeviceFlowTests.fullAccessIsAPairedClientsSwitchSetThroughAcpmux checks:
    • only the paired client row has the setting (off) and the help sentence;
    • the page JSON has full_access and full_access_title;
    • turning it on sends the pairing id without paired:;
    • the list is read again;
    • a peer row never calls acpmux;
    • a refused on or off shows its error.
  • Swift: SettingsDeviceFullAccessGestureTests checks:
    • on is refused, and never reaches acpmux, from page script with no gesture, from a page that claims origin: "user", and from another page with a gesture;
    • one click turns on one setting, and the same gesture again is refused;
    • off needs no gesture.
  • Webview: hostSections.test.tsx "only a paired client machine has a Full access switch..." checks:
    • one switch, on the paired row only;
    • unchecked;
    • the help text shows;
    • on sends {id, on: true}, and off sends {id, on: false}, updating the switch each time.

Fleet runs (cmux-ci test) at the head:

  • Rust: cargo fmt -p cmux-tui --check passes, and cargo test -p cmux-tui --lib devices passes 16 of 16.
  • Webview: bun test src/pages/settings passes 310 and fails 7. The 7 failures are in serverCard, claimPaint and categories, and the same 7 fail with this PR's webview diff reversed, so they come from base. vp fmt --check, vp lint and typecheck all pass.
  • Swift: compiles and tests run on this PR's CI only.

Stop conditions

This adds no timers, observers or loops. Each AcpmuxPersonLine lives for one toggle: it is closed by defer, and its own deadline timer (5 s) ends it in any case.

No capture: this runs on CI only. Swift is not built on agent hosts, so no screenshot of the real Settings window was taken. The capture plan, once a CI build exists: Settings > Devices with a paired client machine, showing the switch off, then on with its help line.

Agent session: big-red
🤖 Generated with Claude Code

teamleaderleo and others added 5 commits October 24, 2026 21:04
`device list --json` gives each acpmux client pairing its pair_list Full-access setting
(absent reads as off), so Settings > Devices can show it. Other rows have no such setting
and omit the field.

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
Each acpmux-paired client machine's row carries its Full-access setting and a sentence
saying what it allows. `cmux.settings.devices.full_access {id, on}` sets it through
acpmux's `_acpmux/pair_set` on a connection proven as the person (turning it on is refused
without the proof), then reads the list again. Other rows have no setting.

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
… rows

A row whose host data carries `full_access` gets a switch titled "Full access" with the
host's sentence on what it allows; toggling it calls `cmux.settings.devices.full_access`
and reads the host lists again. The mock provider gains a paired client row for 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
Settings draws no description line over 60 characters in any language (rowSummary.test.tsx).
The device's name is already the row's title, so the help line says only what Full access
allows; the refusal line is shorter too. Every locale fits. The mock provider's handler
reads `on` as a boolean.

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
…as FeedControl does; the switch shows only for a boolean

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 d4d91be: 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=pr25069 -f ref=hb-acpmux/remote-full-access-device-ui -f platform=macos -f backend_mode=local

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

gh run download 40161997785 -R manaflow-ai/cmux -n gallery-advisory-25069-d4d91be3bc4cb8fe46f927b3671d3e85089e9e21 (then read gallery-review.json)

States
State Theme Status Diff Picked by
pages.settings/general Apple System Colors Light unchanged 0% own HostSections.tsx
pages.settings/general Apple System Colors unchanged 0% own HostSections.tsx
pages.settings/general-customized Apple System Colors Light unchanged 0% own HostSections.tsx
pages.settings/general-customized Apple System Colors unchanged 0% own HostSections.tsx
pages.settings/general-group-1 Apple System Colors Light unchanged 0% own HostSections.tsx
pages.settings/general-group-1 Apple System Colors unchanged 0% own HostSections.tsx
pages.settings/general-group-2 Apple System Colors Light unchanged 0% own HostSections.tsx
pages.settings/general-group-2 Apple System Colors unchanged 0% own HostSections.tsx
pages.settings/general-group-3 Apple System Colors Light unchanged 0% own HostSections.tsx
pages.settings/general-group-3 Apple System Colors unchanged 0% own HostSections.tsx
pages.settings/general-group-4 Apple System Colors Light unchanged 0% own HostSections.tsx
pages.settings/general-group-4 Apple System Colors unchanged 0% own HostSections.tsx
pages.settings/general-group-5 Apple System Colors Light unchanged 0% own HostSections.tsx
pages.settings/general-group-5 Apple System Colors unchanged 0% own HostSections.tsx
pages.settings/general-group-6 Apple System Colors Light unchanged 0% own HostSections.tsx
pages.settings/general-group-6 Apple System Colors unchanged 0% own HostSections.tsx
pages.settings-motion/play-search Apple System Colors Light unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-search Apple System Colors unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-changed-only Apple System Colors Light unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-changed-only Apple System Colors unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-override-reset Apple System Colors Light unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-override-reset Apple System Colors unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-reset-newtabposition Apple System Colors Light unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-reset-newtabposition Apple System Colors unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-reset-fontfamily Apple System Colors Light unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-reset-fontfamily Apple System Colors unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-reset-hibernation Apple System Colors Light unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-reset-hibernation Apple System Colors unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-reset-hibernationexclusions Apple System Colors Light unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-reset-hibernationexclusions Apple System Colors unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-reset-pinned Apple System Colors Light unchanged 0% imports store.ts (1 hops)
pages.settings-motion/play-reset-pinned Apple System Colors unchanged 0% imports store.ts (1 hops)

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

Not verified: macOS jobs did not run: compile and app tests were skipped on d4d91be3bc; ci-status is green without them.

CI passes on d4d91be3bc (run 40161997767 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

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.

web and size checks: green

Tour of d4d91be3bc (run)

The path-selected recorder failed for core-after, core-before; see the run artifact and failed manifests.

Path-selected interactions

Interaction Changed paths
core Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/PageFactory.swift
Packages/macOS/CmuxNext/Sources/CmuxNextApp/Pages/SettingsPageProvider.swift
Packages/macOS/CmuxNext/Sources/CmuxNextApp/Remote/AddDeviceServices\+App.swift
Packages/macOS/CmuxNext/Sources/CmuxNextServer/AddDevice/AddDeviceStrings.swift
Packages/macOS/CmuxNext/Sources/CmuxNextServer/AddDevice/DeviceDirectory.swift
Packages/macOS/CmuxNext/Sources/CmuxNextServer/AddDevice/DeviceListRow.swift
Packages/macOS/CmuxNext/Sources/CmuxNextServer/AddDevice/DeviceListing.swift
Packages/macOS/CmuxNext/Sources/CmuxNextServer/Resources/AddDevice.xcstrings
Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/AddDeviceFlowTests.swift
Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/SettingsDeviceFullAccessGestureTests.swift

core-before: the media could not be published (refused 1 file(s) that show private names: 25069/d4d91be3/next-core-before/shots/07-new-terminal-tab.png (fleet shell prompt). Nothing was uploaded. Show fixture names in the scene instead (the sample)

Core UI (after): FAILED — 16 ok, 3 failed, 1 unavailable

tour

Video (mp4)

# Step Result Command
1 Launch ok
2 New workspace ok cmux workspace new --name Tour
3 Type in the terminal ok cmux workspace name:Tour screen current pane current tab current terminal current write --text "echo hello from the cmux-next tour\r"
4 A command reports progress: tab loading spinner ok cmux workspace name:Tour screen current pane current tab current terminal current write --text "clear; printf '\\033]9;4;3\\007'; sleep 900\r"
5 Split right failed: exited 1 cmux action run splitRight
6 Split down failed: exited 1 cmux action run splitDown
7 New terminal tab failed: exited 1 cmux action run newSurface
8 Browser tab ok cmux tab new-browser --url https://example.com
9 Hide the sidebar ok cmux action run toggleSidebar
10 Show the sidebar ok cmux action run toggleSidebar
11 Notification ok cmux notify --title Tour --body "Hello from CI"
12 Notifications panel ok cmux action run showNotifications
13 Agent chat ok cmux agent new-chat
14 Agent workspace ok cmux workspace new --name Agent
15 Agent starts ok cmux workspace name:Agent screen current pane current tab current terminal current write --text "clear; printf '\\033]7501;state=working\\033\\\\'; sleep 8; printf '\\033]7501;state=done\\033\\\\'; sleep 900\r"
16 An agent works: row working spinner ok cmux workspace new --name Notes
17 The agent finishes: done mark in the spinner's place ok
18 Session tree: keycaps on the first rows ok cmux settings set sidebar.sessionTree true
19 Session tree back off for later tours ok cmux settings set sidebar.sessionTree false
20 Profile menu unavailable: exited 1 (the menu shot needs a screen capture route) cmux action run sidebar.profileMenu

Failed steps:

5. Split right
Split right

6. Split down
Split down

7. New terminal tab
New terminal tab

Screenshots (12)

1. Launch
Launch

2. New workspace
New workspace

3. Type in the terminal
Type in the terminal

4. A command reports progress: tab loading spinner
A command reports progress: tab loading spinner

8. Browser tab
Browser tab

9. Hide the sidebar
Hide the sidebar

11. Notification
Notification

12. Notifications panel
Notifications panel

13. Agent chat
Agent chat

16. An agent works: row working spinner
An agent works: row working spinner

17. The agent finishes: done mark in the spinner's place
The agent finishes: done mark in the spinner's place

18. Session tree: keycaps on the first rows
Session tree: keycaps on the first rows

Tours are data in scripts/cmux-next/media/tours: add a step or a tour file to show your feature.

@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 12 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpccgqgofe/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpkb889fpd/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
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
test_secret_free_prepare_fallback_does_not_requeue_a_retry_on_the_side_pool (main.N
ightlyOffBlacksmithTests.test_secret_free_prepare_fallback_does_not_requeue_a_retry_on_
the_side_pool)/
test_secret_free_prepare_fallback_does_not_requeue_a_retry_on_the_side_pool (main.N
ightlyOffBlacksmithTests.test_secret_free_prepare_fallback_does_not_requeue_a_retry_on_
the_side_pool)
View Logs
test_targets_outside_the_app_go_to_the_mac_canary (main.SwiftCanaryFleetCoverage.te
st_targets_outside_the_app_go_to_the_mac_canary)/
test_targets_outside_the_app_go_to_the_mac_canary (main.SwiftCanaryFleetCoverage.te
st_targets_outside_the_app_go_to_the_mac_canary)
View Logs

...and 2 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-dc6z9 created for this PR by the cmux coordinator.

teamleaderleo and others added 3 commits October 24, 2026 21:17
…sure, not an async default literal (crash ratchet)

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
…person's own click in Settings, spent once

Codex review: page script in the Settings WebView could call
cmux.settings.devices.full_access with no gesture, and the native closure proves
the app as the acpmux person. On now needs context.userGesture in the Settings
page with a gesture event not already spent; off needs none.

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
teamleaderleo temporarily deployed to cmux-tui-merge-tree October 25, 2026 04:28 — with GitHub Actions Inactive
@teamleaderleo
teamleaderleo merged commit 8ff691f into feat-cmux-next Oct 25, 2026
255 of 266 checks passed
@teamleaderleo
teamleaderleo deleted the hb-acpmux/remote-full-access-device-ui branch October 25, 2026 05:22
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

This branch was previously deployed

4 inactive deployments
artifacts — d4d91be3 Deployed Oct 25, 2026 by teamleaderleo via add the Windows daemon to the tree #51857
ci-routing-agent — d4d91be3 Deployed Oct 25, 2026 by github-actions[bot] via cmux-next macOS placement #34439
sentry-dsyms — d4d91be3 Deployed Oct 25, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #51857
cmux-tui-merge-tree — d4d91be3 Deployed Oct 25, 2026 by teamleaderleo via publish the merge tree #5688
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