cloud enroll-machine: ssh from this Mac through the app's tunnel, and an acpmux peer
Leo (2026-10-25): "One command for a persistent machine: cmux cloud new-machine --persistent --size ..., with built-in bootstrap, no idle pause, and automatic acpmux and fleet-registry enrollment. Then use it to create the lane VMs in machines/lane-vms.md, and make lane-place able to create them on demand."
cmux cloud enroll-machine --target machine:<id> --name lane-vm-1 [--ssh-key FILE.pub]
Run it on the Mac with the signed-in cmux app. It makes a Cloud machine an ssh host and an acpmux peer of that Mac. These are the enrollment steps of #25217's cloud new-machine, which prints this command as enrollment.enroll_command.
- On the machine, over the exec API (the signed-in person's authenticated link):
- It adds this Mac's ed25519 key to the
cmuxuser'sauthorized_keys, whichever account the exec runs as. It adds the key once (a whole-key match that ignores comment lines), adds a newline first if the file doesn't end in one, and runs underset -eu. - It starts sshd and prints the machine's host key. That key is pinned from this authenticated answer, so there is no trust on first use.
- It adds this Mac's ed25519 key to the
- On this Mac: a marked
Host lane-vm-1block in~/.ssh/cmux-cloud/config.HostName <private address>,User cmux,Port 22.ProxyCommand '<app cmux-tui>' wg dial --hub '<stable hub link>' %h %p.HostKeyAlias cmux-<machine id>, with the pinned key in~/.ssh/cmux-cloud/known_hosts,StrictHostKeyChecking yes,GlobalKnownHostsFile noneandKnownHostsCommand none. Only the pinned key vouches for the machine.ControlMaster noandControlPath none, so a shared master from an earlier enrollment of the same name can't answer.IdentityFile(the private half of--ssh-key, which must be*.pub) andIdentitiesOnly yes.- The folder is opened
O_NOFOLLOW|O_DIRECTORYand locked through that descriptor. One enrollment at a time edits it, and its files are 0600, written through a temporary file and a rename. - The markers must pair up. Damaged markers are refused, not guessed.
- The person's
~/.ssh/configis never written. When there is none, one is created holding only theIncludeline (a temporary file, thenlink(2)). Otherwise OpenSSH decides:ssh -G NAMEmust show exactly this route (address, user, port, alias, proxy, known_hosts, no global known hosts, no master).- Only then does the acpmux step run.
- If it doesn't match,
steps_pendingnames the oneIncludeline to add first, and a rerun picks it up.
cmux wg dial --hub PATH HOST PORT(new, Rust): one TCP stream through the app's WireGuard hub (SOCKS5 CONNECT), on stdio, for ssh'sProxyCommand. When the input ends it half-closes, and the peer's answer still drains.- The hub keeps a stable symlink (
CloudPaths.hubLink) to its per-launch socket.- The link moves before the start counts as done; a failure fails the start, and a moved link is re-pointed.
- A marker file records that an ssh route depends on the hub. Then the hub starts after each successful machine list (sign-in, Cloud re-enabled, periodic refresh) whenever Cloud is available.
- If its process ends, it starts again, at most 3 times in 10 min.
- acpmux:
acp host enroll NAME --harness claude --harness codex. That command installs only this Mac's platform, so for a Linux machine (whose acpmux comes from the image or the lane bootstrap) it falls back toacp host add NAME. The peer counts as done only when it is connected; the harness profiles then stay pending. - The answer:
machine_id,name,ssh_host,address,steps_done,steps_pending,rerun_command, andregistry_command(tools/lane-vm-enroll ... --applyin leo-workspace). After 30 s the same fields come back withpending: trueand the work keeps going. A rerun is idempotent.
So ssh lane-vm-N and reach lane-vm-N work from that Mac while its cmux app runs. Other hosts reach the VM through that Mac.
Threat model: a process that can write the person's ~/.ssh already owns their ssh. Races against such a process inside ~/.ssh/cmux-cloud are out of scope. Within that: no symlink is ever followed, and enrollments are serialized.
Changelog
Split to stay under 800 changed lines: cmux wg dial is #25493 and the 21-language strings are #25494. Both have landed, and this branch now includes them from feat-cmux-next. It is 720 changed lines.
- Added:
cmux cloud enroll-machinemakes a Cloud machine reachable asssh NAMEfrom this Mac, through the app's tunnel with its host key pinned, and adds it as an agent host.
Observed / Not run
- Ran here:
prepush-gate.py: no new violations.- rustfmt on the Rust file.
ssh -G(OpenSSH 9.6) against the generated block shapes, which confirmed how each checked key prints.- The
authorized_keysedit, run twice on a file with no final newline: one newline and the key were added once. A commented-out key reads as absent; anoptions ssh-ed25519 KEYline reads as present.
- Review: Codex
gpt-6-astraat xhigh through SR2, 10 rounds, SHIP at ce1aabe916a2767060da9b4a64cf02d58351aa05. A read-only compile pass found one compile error, a missingpublic, which is fixed. - Review (codex gpt-6-astra xhigh, independent): SHIP at a784ecbcd68b31b1fef9b4f9b250f483f06983a7. An automated next-review then blocked there on two P2s, both fixed in 454d14c0:
- the hub's exit watcher no longer schedules a second start when a caller already began one;
- a public key whose comment has several words (or tabs) is accepted.
A full Codex pass at 454d14c0 confirmed both fixes. A confirming pass after feat-cmux-next (with #25493) came in is SHIP at a33d4329e97392da707308d4597447b50d6c5ebe.
- Fleet (Mac):
CloudEnrollMachineTestsandActionCatalogTestspassed.ActionSurfaceParityTestsfailed onlyeveryMenuKeepsAShortTopLevelon.sidebarBackground, a menu this PR doesn't touch. - Not run: a live enrollment (it needs a signed-in app and a Cloud machine).
No capture: CLI-only (one new palette row, Enroll Machine as Agent Host, and no new UI).
Timers/loops: the hub's exit watcher ends when the hub process does. It restarts the hub only under the same generation, at most 3 times in 10 minutes, and stop()/revoke() end it by bumping the generation. There is no polling.
Risk: high by rule ("adds a delete of files"). The deletes remove only files this code creates: its temporary files and the staged hub-link symlink. It lands on an independent review receipt on the exact head plus a green compile (docs/ci/risk-tiers.md).
Agent session: agent session
š¤ Generated with Claude Code