Cloud VM image: cmux-rd desktop host on the agent display behind a root-only socket (role desktop)

Merged
TE
1d ago
+761-20

Why

Leo, 2026-11-05:

"Also go check in on the state of affairs of screen sharing remotely on these machines, like the cloud machines, because there was so much VNC work. But it's also like, I feel like it wasn't really linked up end to end here, and we have an opportunity quite soon to start doing that. [...] And then it became AI computer use!!"

Leo, on cmux#36080: "is that really necessary? can't you both do it at the same time?"

Leo: "don't stick it into dev. Put it into prod".

This is the image slice of the Cloud desktop: the person's view of a Cloud machine's agent display :99 is cmux.rd/1, carried over the owner's Native link as link service desktop. The machine's link receiver (the session daemon's __receive-vm, which runs as user cmux) admits the owner's link token for desktop and connects the host's socket; systemd then starts the cmux-rd host on :99 as its own user.

What

--agent-tools (the option the production image is baked with) now also bakes:

  • System user cmux-rd: useradd --system --user-group --no-create-home --home-dir /nonexistent --shell /usr/sbin/nologin; the bake asserts its only group is cmux-rd, it has no sudo, and cmux is not in its group.
  • cmux-agent-rd.socket: ListenStream=/run/cmux-rd/desktop.sock, SocketUser=root, SocketGroup=cmux, SocketMode=0660 (the link receiver runs as cmux), in /run/cmux-rd (0711 root:root, tmpfiles and DirectoryMode), Service=cmux-agent-rd.service, WantedBy=sockets.target, enabled and listening at bake (so in the snapshot too) with the host not running.
  • cmux-agent-rd.service (no [Install], only the socket starts it): User=cmux-rd, BindsTo=/After=cmux-agent-display@99.service, ExecStart=<store>/bin/cmux-rd host --listen-systemd --peer-uid 1000 --display :99 --xauthority /run/cmux-rd-host/Xauthority-99 --codec openh264. The binary path comes from the lock entry's store path, and the peer uid is DEVBOX_WORK_UID, the uid the link receiver runs as (the bake checks it against id -u cmux). RuntimeDirectory=cmux-rd-host (cmux-rd 0700) holds the cookie copy, apart from the socket directory. Sandbox: RestrictAddressFamilies=AF_UNIX (the activated socket and the X display are both Unix sockets), an empty CapabilityBoundingSet=, NoNewPrivileges, ProtectHome=yes, ProtectSystem=strict, PrivateTmp=yes with /tmp/.X11-unix bound read-only, PrivateDevices, RestrictNamespaces, RestrictSUIDSGID, and more.
  • The cookie copy (root ExecStartPre=+): root never opens the agent's cookie, because /home/cmux/.cache/cmux-display/Xauthority-99 is a path the agent controls. It is read as cmux (setpriv --reuid=cmux --regid=cmux --clear-groups --inh-caps=-all --bounding-set=-all --no-new-privs). Only a regular file that is not a symlink is accepted, read within 5 s and capped at 4096 bytes, into a temp file in /run/cmux-rd-host. The temp file is created exclusively (set -C, umask 077, after a check that the directory is cmux-rd's own 0700 directory and not a link), handed over with chown -h and renamed to Xauthority-99. The script has no systemd % specifier and no $ expansion. A test runs the script against a regular file, a symlink, a FIFO, an empty file and an oversized file.
  • cmux-agent-rd-openh264.service (oneshot, User=cmux-rd, StateDirectory=cmux-rd, ConditionPathExists=! its marker): cmux-rd openh264-install on the first start only (Cisco's binary over HTTPS, never bundled). It is the only cmux-rd unit allowed AF_INET/AF_INET6. The host Requires=/After= it.
  • Lock program cmux-rd (added to OPTIONAL_PROGRAMS, role desktop, baked): cmuxRdReleaseShape(V) is the release cmux-rd-vV on manaflow-ai/cmux, cmux-rd-V-x86_64-unknown-linux-gnu.tar.gz checked against its checksums.txt. The archive layout is the one #36433's scripts/release.py packs: bin/cmux-rd, plus LICENSE and THIRD_PARTY_LICENSES.md (notices, which the install checks) at the root. A test packs an archive the same way and runs the real install command on it. The validator refuses another URL, a missing checksums.txt, a missing desktop role or a first-use entry.
  • Pinned: cmux-rd 0.1.0 from release cmux-rd-v0.1.0 (#36433, merge 11edd026, release run 42028500138): sha256 8118a53a809e9204e68bb692e88ec1f429c897380c558b98604046dca2d29e73, 6868976 bytes. These agree with the release checksums.txt, the asset digest and a download. The archive holds LICENSE, THIRD_PARTY_LICENSES.md and bin/cmux-rd.
  • Start limit: StartLimitIntervalSec=0 on the host. With a start limit, a run of failed starts would leave cmux-agent-rd.socket failed (service-start-limit-hit) until a reset-failed. Without it, each start is one admitted connection, a failing host fails only that connection, and the next one tries again.
  • Bake record: result.desktop (role: desktop, program, version, socket) is written for each image that has the role, so the platform can derive desktop: true from the image record and never from the provider.
  • agent-tools-probe desktop checks: cmux-rd exists with nologin and only its own group; /run/cmux-rd is 711 root:root and the socket 660 root:cmux; socket enabled and active, host inactive; the host's --peer-uid equals id -u cmux, and its RestrictAddressFamilies is AF_UNIX; as nobody, connecting gives EACCES; a held connection as cmux (the receiver's uid) starts the host, which runs as cmux-rd with --listen-systemd --peer-uid 1000 and has no TCP or UDP socket; the cookie copy is 600 cmux-rd in a 700 cmux-rd directory; and, while the connection is still held (the host running, its copy present), cmux opening the copy fails with EACCES. ENOENT (a copy already removed with an idle host) fails the check.

Where the boundary is

The boundary is the link admission. Only the owner's app, through the owner's platform-minted, one-time link token for service desktop (verified by the receiver with its replay history and receiving policy), reaches this host from outside the machine.

On the machine the socket is not a boundary against the agent, and this PR does not claim it is. The link receiver runs as cmux, so the socket admits cmux (and the host's --peer-uid is cmux's uid). The agent is cmux too; it already reaches :99, its own display, and has NOPASSWD:ALL sudo (workUser.ts). The socket mode keeps every other local user out (nobody gets EACCES). The separate cmux-rd user and its private directories keep the host's files (cookie copy, OpenH264 state) out of the agent's reach, so the agent can't get at them without sudo. The host has no network family and no capabilities.

This differs from the shared brief's first table, which had a root receiver and a root-only socket. The receiver turned out to run as cmux.

The unit uses BindsTo= on the display, which includes Requires=, so that a restarted display (a new cookie) restarts the host.

Pending (not in this PR)

  • The bake: .github/workflows/cloud-vm-image-bake.yml with mode=bake, agent_tools=true, keep_snapshot=true; then agent-tools-probe.ts --snapshot <sh-id> with every rd-* check passing (the first host start downloads OpenH264); then mode=prod-copy with dev_snapshot=<sh-id> and a VM_BOOT_IMAGES entry.
  • The host README from #36433 still describes --peer-uid 0 (a root receiver). The host takes any uid from that flag, so nothing breaks with --peer-uid 1000; the README wording is a follow-up there.

Verification

Fleet, at head 985e8b3e:

  • every tests/vm-image-*.test.ts plus tests/vm-devbox-image.test.ts: 402 tests, 0 fail (run 42035021538). vm-image-restricted.test.ts runs on its own (7 pass), because its mock.module of the manifest does not take effect once another file in the same bun process has loaded the resolver; in one combined process 3 of its tests fail. This PR does not touch that test or the manifest it mocks; I did not run the combined process on the base.
  • tsgo --noEmit over web/: exit 0, with all six changed files in the program. tsgo --noEmit over web/ exits 0 with all six changed files in the program.

Agent session: agent session

šŸ¤– Generated with Claude Code