Cloud VM: a chat move in and folder trust need the caller's vmKind owner, never the VM's record

Merged
TE
9d ago
+532-111

Built on #26323 (PR 3, merged as a6c49dbd358), which adds the app's per-link vmKind answer (CloudAttachEndpoint.callerOwnsVM, CloudMachineLink.callerOwnsVM) and vm_kind on agent-session-start. This PR needs that field. It is now rebased onto feat-cmux-next after #26323, with patches identical to the reviewed stack (range-diff =).

The rule (claude-sr2, relayed by the area owner): "the kind comes from outside the guest. The client side (the app or the daemon that starts the remote session) asks the Cloud API which kind the VM is, for the signed-in account, and passes that to the remote start; the guest never decides." And: "Agents keep sudo in Cloud VMs". #26323 applied that rule to agent-session-start only. A chat move in and folder trust on a Cloud VM still trusted the guest's own /etc/cmux/vm-kind owner record, which any agent there can write.

What changes

  • Daemon (cmux-tui-core): agent-transfer-receive, agent-transfer-finish and agent-session-trust take a vm_kind param and use the same gate as agent-session-start:
    • On a Cloud VM they go on only on the exact vm_kind: "owner".
    • The VM's own record never counts.
    • Team evidence refuses even with the caller's word.
    • With no word they fail closed (agent_session.host_unverified).
    • Macs and SSH hosts need no word.
    • The old None path that took the record is gone: host_gate now takes a plain bool.
  • Page path: a page's routed agent_session.trust.get/set cannot vouch for the Cloud API's answer. It takes no vm_kind (bad_request), so it reaches no Cloud VM's trust.
  • Capabilities: new agent-transfer-vm-kind-v1 and agent-session-trust-vm-kind-v1.
  • Pump (cmux link agent move --vm-kind KIND):
    • Sends the kind on the receive and the finish to a target that serves the capability.
    • The flag is also the caller's word that the target is a Cloud machine. An older target would still trust its own record, so the pump refuses any kind but owner there (host_unverified, side: target) before the chat freezes. What the target advertises (fs-v1 or not) never changes that, because a guest can hide any sign of being a Cloud VM.
    • Without the flag the caller says the target is not a Cloud machine (Bring Back Local's Mac). A current Cloud daemon still refuses a chat with no word.
  • App:
    • Move to Cloud passes --vm-kind owner|none from the running link's answer, read at the move and never kept. The owner word names the link process it came from: --to-pid is CloudMachineLink.ownerPID. The pump sends the receive and the finish only over a connection whose peer is that process (SO_PEERCRED / LOCAL_PEERPID, via cmux_link::caller::peer_pid, now pub). So a link that restarts while the source's begin is pending never receives the old link's answer. Because the socket path stays the same, a rerun still resolves an unresolved move.
    • Remote folder trust takes the start's rule from #26323's review fix (8fa90e61b79). The app reads the link's owner answer (CloudMachineLink.ownerLink, a per-link id) before the connection and again as it sends. A Cloud machine's trust is refused without that answer, through any daemon. vm_kind: "owner" goes only to a daemon whose own identify on that connection lists agent-session-trust-vm-kind-v1.
  • Daemon gate: a vm_kind other than owner is refused (host_unverified) on any host for start, move-in and trust, because a guest's root can remove every Cloud sign the daemon reads.
  • Not changed: MDM's refusals. Nothing here loosens any check; on a Cloud VM every path only gets stricter.

Risk

  • Medium. Move to Cloud and remote folder trust on an updated Cloud VM now need the app's link to carry the Cloud API's owner answer.
  • The legacy /api/vm link carries it (#26323). The app-server link carries none yet, so a move or a trust read through it is refused, and Move to Cloud already refuses that link (linkUnsupported).
  • Bring Back Local is not affected, because its target is the Mac.

Tests (cmux-ci, Linux)

  • Head b45560da004 (on #26323's 8ad048a1e9c), run https://github.com/manaflow-ai/cmux/actions/runs/40590890295: green. An explicit "vm_kind": null refuses on trust, receive and finish, as on start (present_nullable). Tested on a host with no Cloud sign: a_word_that_is_not_owner_refuses_on_any_host and the trust test. Clippy ran without the collapsible_if allowance, because the tip has the fix. Earlier heads, all green: 73a5f87134a (run 40589090968), fb6883a2fd1 (run 40586211848), d5506bb1052 (run 40583784546), 40d8fa0661c (run 40570069438), 7af51f7ed48 (run 40563544239), c0b87266ae7 (run 40558028188).
    • fmt check and clippy -D warnings. (The earlier runs allowed clippy::collapsible_if because #26323's old base had that red.)
    • cmux-tui-core server::: 1069 passed.
    • cli agent_session: 36 passed.
    • link_agent_move, cloud_move_journey, machine_update_journey, bring_back_journey, access_request_journey: all passed.
    • link:: bin tests: passed.
  • Base (#26323's head plus only the three new tests), run https://github.com/manaflow-ai/cmux/actions/runs/40558019316: each new test fails.
    • a_cloud_vm_takes_a_chat_only_on_the_callers_owner_word: a receive with no word succeeded on the owner record.
    • agent_session_trust_on_a_cloud_vm_needs_the_callers_owner_word: a trust read with no word succeeded.
    • the_cloud_apis_kind_reaches_the_target_or_an_older_one_is_refused: the pump has no --vm-kind. At the head this test also covers an older target that advertises nothing Cloud-like, given --vm-kind team or none: it is refused before agent-transfer-begin.
  • New at the head:
    • link_agent_move::the_owner_word_reaches_only_the_link_process_it_came_from: with another process at the socket, the pump sends no receive and no vm_kind, and the chat is given back. With the same process, the chat moves with owner.
    • CloudLinkReconnectingTests.theOwnerWordNamesTheLinkProcessItCameFrom (Swift): ownerPID is the link's pid, nil once the link ends, and the replacement's own pid after that.
  • Updated: cloud_move_journey and machine_update_journey now pass --vm-kind owner / vm_kind. The journey also checks that the bare and team forms are refused.
  • Swift: AgentSessionStartOwnerLinkTests.aCloudTrustCallGoesOutOnlyOnTheOwnerAnswerOfTheSameLink (real line socket); CloudMoveTests now expects --vm-kind none with no owner link. Compiled by PR CI only.

No capture: nothing visible changes. The Swift edits only decide whether --vm-kind owner / vm_kind is sent for Move to Cloud and remote folder trust. A refusal shows through the existing agent_session.host_unverified strings, and a fleet capture mini has no live owned Cloud link to show either path.

Agent session: agent session

šŸ¤– Generated with Claude Code