Skip to content

cmux-tui: agent-transfer-v1 forwards chat moves to acpmux with the daemon's own guard (cx-rppd.1) - #19460

Merged
teamleaderleo merged 13 commits into
feat-cmux-nextfrom
continuity-agent-transfer
Oct 11, 2026
Merged

teamleaderleo merged 13 commits into
feat-cmux-nextfrom
continuity-agent-transfer

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Move to Cloud (cmuxterm-hq#1874 slice 1, bead cx-rppd.1) needs to move an agent chat from the Mac's acpmux to a Cloud VM's acpmux and back. acpmux's acpmux.transfer.v1 calls (#19359) only work on acpmux's local socket, and an unguarded receive would let the moved chat pick its own folder, preset and policy. This PR adds daemon verbs that forward those calls to the daemon's own acpmux, with a receive guard the daemon builds itself. The guarded receive in acpmux is the parallel PR "1a" (_acpmux/transfer_receive {manifest, guard} answering guarded:true); these tests use a fake acpmux, so they don't depend on 1a landing.

New capability agent-transfer-v1, advertised when the daemon has an acpmux socket path and knows its host (the binary sets the host next to the fs-v1 Cloud host test in link::start_link_entry). Spec: cmux-tui/spec/commands.md, "Agent chat transfer".

Target verbs (the chat arrives here):

  • agent-transfer-receive {manifest, cwd} sends _acpmux/transfer_receive {manifest, guard}. On a Cloud host the guard is {control:"peer", roots:[canonical $HOME/work]}. On any other host (a Mac taking a chat back) it's {control:"web", roots:[cwd]}, and cwd must canonicalize to an existing folder strictly under $HOME that isn't $HOME or inside a dot folder of $HOME. On every host cwd must canonicalize under the roots before anything is forwarded. If acpmux doesn't answer guarded:true, the receive is refused (agent_transfer.unguarded). The daemon keeps the canonical folder, and the move and chat ids from the manifest, per receive_id. It keeps at most 16 receives, and that cap is checked before acpmux makes a receive, so a refused receive leaves nothing behind in acpmux.
  • Machine kind (hq-84). Receive and finish read agent-session-start's host gate (AgentStartHostSource) again on every call. They refuse on a team VM with agent_session.team_vm_blocked, and on a Cloud VM of unknown kind (or a daemon never told its kind) with agent_session.host_unverified, the same codes start uses. This matters because a peer-control chat keeps its permission policy and mode and runs its queue at once. On a shared work user, other members could read its history. So peer-control receives are limited to Macs, SSH hosts and personal (owner) Cloud VMs.
  • agent-transfer-write {receive_id, path, offset, data} sends one page of a receive this daemon guarded.
  • agent-transfer-finish {receive_id, name?} sends _acpmux/transfer_finish with the receive's own folder as cwd. A finish that fails for good drops the receive: anything but a timeout, an unreachable or busy acpmux, or transfer.incomplete.
  • agent-transfer-receive-abort {receive_id} drops the receive and forwards acpmux's target-side _acpmux/transfer_forget {transferId, sessionId} (acpmux: a chat move whose client goes away ends on its own (cx-xtg0.9) #19411). The ids come from the receive's own manifest, never from the frame. acpmux then deletes the receive's files and refuses that move afterwards. The answer is {dropped, forgotten, landed}. acpmux's forget is not exposed as a daemon verb.

Source verbs (the chat leaves here):

  • agent-transfer-begin {surface, interrupt?}, agent-transfer-commit {surface, transfer_id, to} and agent-transfer-abort {surface, transfer_id} take the session id only from the store tab record of surface, the same way agent-session-attach does.
  • Begin serves scrubbed copies of two bundle files, and the manifest lists their scrubbed sizes:
    • manifest.json, without the absolute source paths of native files.
    • session.json, without the source machine's launch settings (sessionEnv, harnessArgv, harnessRoots, preset, tags). Its cwd stays, because the target's import needs a folder and finish replaces it.
    • These two names are compared case-insensitively.
    • If either file can't be scrubbed, the move is aborted, so the chat doesn't stay frozen.
  • The events logs still carry local paths from the source machine. That's accepted, because the guarded finish never runs them.
  • agent-transfer-read {transfer_id, path, offset, limit?} serves only paths the begin manifest of a move begun through this daemon lists, exactly as listed. So on a case-insensitive disk, Session.json can't reach the unscrubbed file.
  • Abort forgets the move even when acpmux refuses the abort.

A frame is refused before anything reaches acpmux if it carries guard, roots, control, env, mcpServers, sessionId/session_id, any of the relay gate's COMMAND_PARAMS, or any unknown param or verb. Pages are capped at 4 MiB of decoded data. Calls run on worker threads, at most 8 at once. A move entry is remembered for 24 h. agent-transfer-* client lines use the bulk mux lane in cmux-remote.

Receive ids are not tied to the connection that made them: another trusted local connection of the same user can write, finish or abort a receive, for example after the pump reconnects. That grants nothing new. The folder comes from the daemon's own record of the receive, never from the frame, and acpmux checks it again against the guard it stored with the receive.

Small supporting changes:

  • agent_session_link items are now pub(crate), so this module can reuse AcpmuxLink.
  • agent_session_start exposes its host gate as Mux::agent_host_gate.
  • The attach tests' fake acpmux now answers _acpmux/transfer_*.
  • cloud_fs splits its Cloud host test out of install_if_cloud_host.
  • server.rs stays within its god-file baseline after the base merge: the module is pub, and its route chains onto the attach route.

Relay and authorization analysis

Callers. Only a trusted same-uid Unix client that isn't a remote client can use these verbs. This is the same check as agent-session-attach: is_unix && !is_remote_client. On the legacy Cloud link, the app reaches the VM daemon exactly this way (cmux-remote mux-control opens a plain Unix connection to the daemon socket). So this PR adds no entry to the remote relay allowlist (remote_relay/gate.rs ALLOWED_COMMANDS). A cmux link client gets remote_denied. WebSocket clients, stamped clients and unregistered ids all get one generic agent_transfer.not_trusted answer before their frame is parsed.

Local command or content execution. Finish imports a chat whose queued prompts then run through an agent that's already configured on this machine. The guard narrows this:

  • No preset, env, argv, MCP servers or tags. acpmux clears them under the guard (1a), and this daemon refuses frames that try to pass them and scrubs them out of session.json.
  • The folder must be under roots the daemon chose from its own host.
  • Under web control (toward a Mac), the policy, rules and modes must ask, and acpmux refuses the import otherwise (1a).
  • Under peer control (toward a VM), the chat keeps its policy and mode. That's why receive is limited by the machine kind gate to personal Cloud VMs: team VMs and VMs of unknown kind are refused.

No verb takes a command. Command-bearing params are refused on every verb.

Access to unowned objects.

  • A receive creates a new chat. acpmux refuses an id that's already present, unless that copy moved away from this machine earlier.
  • Write, finish and receive-abort only work on receive ids this daemon created and guarded. The forget that receive-abort sends names only that receive's own move.
  • The source verbs are scoped by surface to agent tabs of this store that have a bound session. An unknown surface, a non-agent surface and an unbound tab all get the same agent_transfer.unknown_tab answer.
  • Commit and abort send the tab's session together with the transfer id, and acpmux checks the move.
  • Read only accepts moves begun through this daemon, and only paths their manifest lists.

Local state exposure. Begin and read hand out a whole chat (log, draft, native store copy), but only for a tab-bound chat and only to a trusted local client. That client can already attach to the chat and read its full history. The absolute native source paths and the session.json launch settings are removed. session.json keeps its cwd, and the events logs keep source paths; the guarded finish runs neither.

Policy tests (in agent_transfer_tests.rs):

  • the_remote_relay_denies_every_agent_transfer_verb and every_verb_is_refused_on_every_untrusted_connection
  • a_client_guard_roots_control_env_session_or_command_param_is_refused
  • receive_abort_has_acpmux_forget_that_move_only (acpmux's forget isn't a verb, and no frame can name a move)
  • a_folder_outside_the_roots_is_refused_before_acpmux
  • a_receive_is_refused_on_a_team_vm_and_on_a_cloud_vm_of_unknown_kind (and a personal Cloud VM is allowed), a_daemon_that_was_never_told_its_host_kind_refuses_a_receive and finish_reads_the_host_kind_again
  • an_unmapped_or_unbound_surface_gets_the_unknown_answer, read_takes_only_bundle_paths_of_a_move_this_daemon_began and read_serves_only_paths_the_begin_manifest_lists
  • write_and_finish_take_only_a_receive_this_daemon_guarded and an_acpmux_that_did_not_apply_the_guard_is_refused
  • Slot handling: abandoned_receives_free_their_slots_once_aborted, a_finish_that_cannot_succeed_drops_its_receive_and_a_retryable_one_keeps_it and abort_forgets_the_move_even_when_acpmux_refuses_it
  • Scrubbing: begin_strips_absolute_source_paths_from_the_bundle_manifest and begin_scrubs_launch_settings_from_session_json_and_keeps_its_folder
  • Allow cases: the Cloud and Mac receive tests, write_and_finish_forward_the_receive_and_finish_uses_its_folder, source_verbs_take_the_chat_from_the_surface_tab and pages_over_four_mebibytes_are_refused

Testing

All runs are on the cmux build fleet (scripts/ci/cmux-tui-rust-check.sh, macOS), using --arg test --arg agent_transfer unless noted.

First pass:

  • Red at 55b5789ea34c (tests plus a stub module), step 80524648138dbb4da1249383: 15 failed, 1 passed.
  • Green at dce31080889c, step 5b6ef14cee157bdb6ba98b11: 16 passed.

Review round (team VM gate, slots, scrubbing, listed paths):

  • Red at 99d77219e810, step afbd25c9987c4d707789fefd: 11 failed, 13 passed. Example: the team VM receive answered ok:true with receive_id rcv_1.
  • Green at 4b47dd1247bc, step 8eff23528e100fe738f51919: 24 passed. agent_session_attach (step 9028e5010696e416d6ce860d) passed 22, and clippy and fmt both passed.

After merging feat-cmux-next at 886962c91a79 (with #19411, transfer_forget):

  • Red at d9f6e113e27a (test that receive-abort forwards transfer_forget), step f9406c1f54cc3fadee85b612: 1 failed (receive_abort_has_acpmux_forget_that_move_only, the landed field), 24 passed.
  • Green at b2a910c0cb22, step fb2753ff3bd24068f9c69e7a: 25 passed, plus mux_lanes::tests::agent_transfer_pages_use_the_bulk_lane.
  • agent_session_attach at b2a910c0cb22, step 8242277c7593dd0fd112953e: 22 passed.
  • Clippy (step c3a7456aeb84f30fe96b4487) and fmt (step 94e2d8ad0bb59c022b629cf5) both passed.

Local checks:

  • cmux-tui/scripts/check-spec-inventory.py: ok.
  • scripts/cmux-next/check-no-godfiles.sh --only rust --base 886962c91a79: ok.
  • scripts/cmux-next/crash_ratchet.py: no Rust crate gained a hit. It reports Swift async_closure_default_arg gains in CmuxNextAgentPane and CmuxNextApp, but this branch changes no Swift files.

Not verified here:

  • No Testbox run of cargo check --target x86_64-pc-windows-gnu. The new code is cfg(unix), and I'm relying on this PR's Windows lane.
  • No live end-to-end move. That needs 1a (guarded acpmux) and 1c (the app pump). Until 1a lands, a real acpmux answers a receive without guarded:true, so this daemon refuses it (fail closed).

Changelog

none

Proof

None needed: daemon protocol only, with no UI change.

🤖 Generated with Claude Code


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


Summary by cubic

Adds agent-transfer-v1, daemon verbs that forward acpmux chat moves with a guard the daemon builds itself, letting a trusted local client move an agent chat between a Mac and a Cloud VM. The guard stops the moved chat from picking its own folder, preset, or policy, and unguarded acpmux answers are refused.

Guard

  • Cloud hosts receive with peer control rooted at $HOME/work; other hosts with web control at a cwd that must be an existing folder strictly under $HOME, outside dot folders.
  • Receives are refused at receive and again at finish on team VMs and on Cloud VMs of unknown kind, following agent-session-start's gate; a peer-control chat keeps its policy and mode.
  • Frames carrying a client guard, roots, control, env, MCP, session, or command params are refused before reaching acpmux, and only trusted local clients reach these verbs.

Verbs

  • agent-transfer-receive/write/finish forward _acpmux/transfer_*, and only the receive's own folder is usable; begin/commit/abort/read take the session from the surface's tab record, and begin strips absolute source paths and launch settings from the bundle manifest.
  • agent-transfer-receive-abort forwards acpmux's target-side _acpmux/transfer_forget with the move's ids from the receive's own manifest, so acpmux drops the receive's files at once; acpmux's forget is not a daemon verb.
  • Receive slots cannot leak: the cap is checked before acpmux makes a receive, a failed or abandoned receive releases its slot, and abort forgets the move even when acpmux refuses.
  • Pages are capped at 4 MiB and use the bulk mux lane so they never queue ahead of PTY input.

Written for commit 894a2f4. Summary will update on new commits.

View guided diff Turn on auto-fix

teamleaderleo and others added 3 commits October 11, 2026 08:12
…-v1)

Tests first for cmuxterm-hq#1874 slice 1 (cx-rppd.1): a trusted local
client moves a chat through the daemon, which forwards only the guarded
form of acpmux's transfer calls. The module is a stub that serves nothing,
so the tests fail on their assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…emon's own guard

The Move to Cloud pump (cmuxterm-hq#1874 slice 1, cx-rppd.1) moves a chat
between two daemons' acpmux through these verbs. A receive always carries a
guard the daemon builds: peer control rooted at $HOME/work on a Cloud host,
web control rooted at the named folder (strictly under $HOME, not in a dot
folder) elsewhere, and the folder must be under the roots. Finish uses the
receive's own folder. Source verbs take the chat only from the surface's
tab record, and begin serves the bundle manifest without absolute source
paths. Trusted local clients only; no relay allowlist entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 04fae729-ee0c-48d8-a898-96e5bf9e0896

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

teamleaderleo and others added 2 commits October 11, 2026 09:11
Review of #19460: a receive must be refused on a team VM and on a Cloud VM
of unknown kind (the hq-84 rule agent-session-start follows), at receive
and again at finish; receive slots must not leak; begin must scrub
session.json launch settings; read must serve only paths the begin
manifest lists. These fail until the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lots

Review of #19460:
- receive and finish read agent-session-start's machine kind gate at each
  call and refuse on a team VM or a Cloud VM of unknown kind (hq-84), with
  start's codes: a peer-control chat keeps its policy and mode.
- the receive cap is checked before acpmux makes a receive; a finish that
  fails for good drops its receive; agent-transfer-receive-abort forgets
  one (acpmux has no target-side abort); abort forgets the move even when
  acpmux refuses.
- begin scrubs session.json launch settings (env, argv, harness roots,
  preset, tags) next to manifest.json sources, names compared
  case-insensitively, and read serves only paths the manifest lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Oct 11, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

teamleaderleo and others added 6 commits October 11, 2026 09:35
…ty-agent-transfer

# Conflicts:
#	cmux-tui/spec/commands.md
acpmux now has a target-side transfer_forget (#19411). The daemon's
receive-abort should forward it for the move named in the receive's own
manifest, and acpmux's forget must stay unreachable as a daemon verb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Forwards acpmux's target-side _acpmux/transfer_forget (#19411) with the
transfer and session ids of the receive's own manifest, so acpmux drops
the receive's files at once and refuses that move afterwards; acpmux's
forget is not a daemon verb. A receive whose manifest lacks plain ids is a
bad request. server.rs keeps to its god-file baseline after the base
merge: the module is public and its route chains onto the attach route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cursor

cursor Bot commented Oct 11, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@teamleaderleo
teamleaderleo merged commit 77c1f18 into feat-cmux-next Oct 11, 2026
80 of 81 checks passed
@teamleaderleo
teamleaderleo deleted the continuity-agent-transfer branch October 11, 2026 18:00
teamleaderleo added a commit that referenced this pull request Oct 11, 2026
…br4q) (#19489)

* test(settings): empty states and full text for clipped help and paths (cx-9br4q)

Red on base: Accounts with no accounts, Harnesses while checking and a theme
search with no match draw nothing; clipped help and folder paths carry no title.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(settings): polish pass on every page (cx-9br4q)

- Buttons show a hover; MDM and token chips are small-radius rects, not capsules.
- Accounts with none, Harnesses while checking and a theme search with no match
  say so instead of drawing nothing.
- Card intros wrap; clipped help, subtitles and folder paths keep their full text
  as a tooltip; a refused chat root's reason is its own line.
- Action and theme rows reserve the Reset slot, so their controls line up with
  the setting rows; account details line up under the account name.
- Your Agents uses the page's buttons, pads its rows, drops the empty outline
  and draws the doctor's steps with the page's icons.
- Profile rows highlight under the pointer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(settings): review fixes for the polish pass (cx-9br4q)

- Action rows keep a long path and the Reset slot on one line.
- Terminal paths carry their full text as a tooltip.
- Only a collapsed profile row highlights, so an open form keeps its fields visible.
- Empty lines are output elements, announced as status.
- de and km use the page's existing word for theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-tui: agent-transfer-v1 forwards chat moves to acpmux with the daemon's own guard (cx-rppd.1) (#19460)

* cmux-tui: behavior tests for guarded agent chat moves (agent-transfer-v1)

Tests first for cmuxterm-hq#1874 slice 1 (cx-rppd.1): a trusted local
client moves a chat through the daemon, which forwards only the guarded
form of acpmux's transfer calls. The module is a stub that serves nothing,
so the tests fail on their assertions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-tui: agent-transfer-v1 forwards chat moves to acpmux with the daemon's own guard

The Move to Cloud pump (cmuxterm-hq#1874 slice 1, cx-rppd.1) moves a chat
between two daemons' acpmux through these verbs. A receive always carries a
guard the daemon builds: peer control rooted at $HOME/work on a Cloud host,
web control rooted at the named folder (strictly under $HOME, not in a dot
folder) elsewhere, and the folder must be under the roots. Finish uses the
receive's own folder. Source verbs take the chat only from the surface's
tab record, and begin serves the bundle manifest without absolute source
paths. Trusted local clients only; no relay allowlist entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-tui: agent-transfer page check in clippy's preferred form

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-tui: tests for agent-transfer host kind, slots and bundle scrubbing

Review of #19460: a receive must be refused on a team VM and on a Cloud VM
of unknown kind (the hq-84 rule agent-session-start follows), at receive
and again at finish; receive slots must not leak; begin must scrub
session.json launch settings; read must serve only paths the begin
manifest lists. These fail until the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-tui: agent-transfer follows the team VM rule and stops leaking slots

Review of #19460:
- receive and finish read agent-session-start's machine kind gate at each
  call and refuse on a team VM or a Cloud VM of unknown kind (hq-84), with
  start's codes: a peer-control chat keeps its policy and mode.
- the receive cap is checked before acpmux makes a receive; a finish that
  fails for good drops its receive; agent-transfer-receive-abort forgets
  one (acpmux has no target-side abort); abort forgets the move even when
  acpmux refuses.
- begin scrubs session.json launch settings (env, argv, harness roots,
  preset, tags) next to manifest.json sources, names compared
  case-insensitively, and read serves only paths the manifest lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-tui: test that receive-abort has acpmux forget the receive's move

acpmux now has a target-side transfer_forget (#19411). The daemon's
receive-abort should forward it for the move named in the receive's own
manifest, and acpmux's forget must stay unreachable as a daemon verb.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-tui: receive-abort has acpmux forget the receive's move

Forwards acpmux's target-side _acpmux/transfer_forget (#19411) with the
transfer and session ids of the receive's own manifest, so acpmux drops
the receive's files at once and refuses that move afterwards; acpmux's
forget is not a daemon verb. A receive whose manifest lacks plain ids is a
bad request. server.rs keeps to its god-file baseline after the base
merge: the module is public and its route chains onto the attach route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix: close the agent page's artifact frame CSS rule (chat pages render unstyled) (#19494)

* test: every webviews stylesheet closes the rules it opens (red)

feat-cmux-next's conversation.css leaves .acpmux-artifact-frame open since
#19473; every rule after it is swallowed and chat pages render unstyled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: close the agent page's artifact frame rule (green)

The unclosed .acpmux-artifact-frame block from #19473 swallowed the rest of
conversation.css: the composer, code blocks and user rows lost their styles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at 9ddddd6

- vp fmt webviews/test/stylesheets.test.ts

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38161397375

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant