Repository navigation
cmux-tui: agent-transfer-v1 forwards chat moves to acpmux with the daemon's own guard (cx-rppd.1) - #19460
Conversation
…-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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
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>
Bugbot is paused — on-demand spend limit reachedBugbot 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. |
…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>
Bugbot is paused — on-demand spend limit reachedBugbot 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. |
…ty-agent-transfer # Conflicts: # cmux-tui/spec/commands.md
…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>
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.v1calls (#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}answeringguarded: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 thefs-v1Cloud host test inlink::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]}, andcwdmust canonicalize to an existing folder strictly under$HOMEthat isn't$HOMEor inside a dot folder of$HOME. On every hostcwdmust canonicalize under the roots before anything is forwarded. If acpmux doesn't answerguarded:true, the receive is refused (agent_transfer.unguarded). The daemon keeps the canonical folder, and the move and chat ids from the manifest, perreceive_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.agent-session-start's host gate (AgentStartHostSource) again on every call. They refuse on a team VM withagent_session.team_vm_blocked, and on a Cloud VM of unknown kind (or a daemon never told its kind) withagent_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_finishwith the receive's own folder ascwd. A finish that fails for good drops the receive: anything but a timeout, an unreachable or busy acpmux, ortransfer.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}andagent-transfer-abort {surface, transfer_id}take the session id only from the store tab record ofsurface, the same wayagent-session-attachdoes.manifest.json, without the absolutesourcepaths of native files.session.json, without the source machine's launch settings (sessionEnv,harnessArgv,harnessRoots,preset,tags). Itscwdstays, because the target's import needs a folder and finish replaces it.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.jsoncan't reach the unscrubbed file.A frame is refused before anything reaches acpmux if it carries
guard,roots,control,env,mcpServers,sessionId/session_id, any of the relay gate'sCOMMAND_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 incmux-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_linkitems are nowpub(crate), so this module can reuseAcpmuxLink.agent_session_startexposes its host gate asMux::agent_host_gate._acpmux/transfer_*.cloud_fssplits its Cloud host test out ofinstall_if_cloud_host.server.rsstays within its god-file baseline after the base merge: the module ispub, 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.rsALLOWED_COMMANDS). Acmux linkclient getsremote_denied. WebSocket clients, stamped clients and unregistered ids all get one genericagent_transfer.not_trustedanswer 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:
session.json.No verb takes a command. Command-bearing params are refused on every verb.
Access to unowned objects.
surfaceto agent tabs of this store that have a bound session. An unknown surface, a non-agent surface and an unbound tab all get the sameagent_transfer.unknown_tabanswer.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
sourcepaths and thesession.jsonlaunch settings are removed.session.jsonkeeps itscwd, 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_verbandevery_verb_is_refused_on_every_untrusted_connectiona_client_guard_roots_control_env_session_or_command_param_is_refusedreceive_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_acpmuxa_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_receiveandfinish_reads_the_host_kind_againan_unmapped_or_unbound_surface_gets_the_unknown_answer,read_takes_only_bundle_paths_of_a_move_this_daemon_beganandread_serves_only_paths_the_begin_manifest_listswrite_and_finish_take_only_a_receive_this_daemon_guardedandan_acpmux_that_did_not_apply_the_guard_is_refusedabandoned_receives_free_their_slots_once_aborted,a_finish_that_cannot_succeed_drops_its_receive_and_a_retryable_one_keeps_itandabort_forgets_the_move_even_when_acpmux_refuses_itbegin_strips_absolute_source_paths_from_the_bundle_manifestandbegin_scrubs_launch_settings_from_session_json_and_keeps_its_folderwrite_and_finish_forward_the_receive_and_finish_uses_its_folder,source_verbs_take_the_chat_from_the_surface_tabandpages_over_four_mebibytes_are_refusedTesting
All runs are on the cmux build fleet (
scripts/ci/cmux-tui-rust-check.sh, macOS), using--arg test --arg agent_transferunless noted.First pass:
55b5789ea34c(tests plus a stub module), step80524648138dbb4da1249383: 15 failed, 1 passed.dce31080889c, step5b6ef14cee157bdb6ba98b11: 16 passed.Review round (team VM gate, slots, scrubbing, listed paths):
99d77219e810, stepafbd25c9987c4d707789fefd: 11 failed, 13 passed. Example: the team VM receive answeredok:truewithreceive_id rcv_1.4b47dd1247bc, step8eff23528e100fe738f51919: 24 passed.agent_session_attach(step9028e5010696e416d6ce860d) passed 22, and clippy and fmt both passed.After merging feat-cmux-next at
886962c91a79(with #19411,transfer_forget):d9f6e113e27a(test that receive-abort forwardstransfer_forget), stepf9406c1f54cc3fadee85b612: 1 failed (receive_abort_has_acpmux_forget_that_move_only, thelandedfield), 24 passed.b2a910c0cb22, stepfb2753ff3bd24068f9c69e7a: 25 passed, plusmux_lanes::tests::agent_transfer_pages_use_the_bulk_lane.agent_session_attachatb2a910c0cb22, step8242277c7593dd0fd112953e: 22 passed.c3a7456aeb84f30fe96b4487) and fmt (step94e2d8ad0bb59c022b629cf5) 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 Swiftasync_closure_default_arggains in CmuxNextAgentPane and CmuxNextApp, but this branch changes no Swift files.Not verified here:
cargo check --target x86_64-pc-windows-gnu. The new code iscfg(unix), and I'm relying on this PR's Windows lane.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
Need help on this PR? Tag
@codesmith-botwith 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
peercontrol rooted at$HOME/work; other hosts withwebcontrol at acwdthat must be an existing folder strictly under$HOME, outside dot folders.agent-session-start's gate; a peer-control chat keeps its policy and mode.Verbs
agent-transfer-receive/write/finishforward_acpmux/transfer_*, and only the receive's own folder is usable;begin/commit/abort/readtake the session from the surface's tab record, and begin strips absolute source paths and launch settings from the bundle manifest.agent-transfer-receive-abortforwards acpmux's target-side_acpmux/transfer_forgetwith 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.Written for commit 894a2f4. Summary will update on new commits.