cloud new-machine: --persistent, --size, --name and a bootstrap script in one call

Merged
TE
11d ago
+938-20

Leo (2026-10-25, via claude-sr2): "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 new-machine --persistent --size medium --name lane-vm-1 \
  --bootstrap lane --bootstrap-script tools/lane-vm-bootstrap

This makes the machine in one call and answers JSON.

  • Size and name go with the create.
    • --size small|medium|large|xlarge sends memoryMb (4/8/16/24 GiB), and the server's size ladder picks the vCPUs (2/4/8/12); the answer reports both. medium is the lane VM's 4 vCPU / 8 GB.
    • --name must be a plain word: 1-64 letters, digits, ., _ or -. That makes it a valid host name and shell word in the commands the answer prints.
  • --persistent turns the idle pause off at create (idlePauseMinutes: "off"; the server half is #25216).
    • If the server didn't store it (an older deploy), the app PUTs /idle-pause right after.
    • If that fails too, persistent reads false and enrollment.pending names the cloud set-idle-pause command.
    • persistent reports what the server stored; persistent_requested reports what was asked.
  • --bootstrap-script FILE (--bootstrap lane also sets LANE_VM_NAME):
    • The script must be a regular file of at most 1 MB. It is read on the app's files lane before anything is created, so an unreadable script costs no machine.
    • It is uploaded with the Cloud file API to /home/cmux/.cmux/bootstrap/ and started by one short exec as a transient systemd unit (cmux-machine-bootstrap). If systemd or systemd-run fails, it falls back to setsid. A long bootstrap therefore never hits exec's 25 s limit.
    • Every command names the image's work user cmux, so the script always runs as cmux with its home, whether the exec runs as root or as that user.
    • It logs to bootstrap.log and writes status.json (running|done|failed, exit code, script sha256).
    • A rerun of the same script while it runs (flock) or after it finished starts nothing.
    • The CLI resolves a relative path against its working directory, and a quoted ~/ is expanded.
  • The answer: machine_id, target, name, size, cpu, memory_mb, persistent, idle_pause_minutes, address, bootstrap {status, log, status_file, check}, enrollment {done, pending, enroll_command, registry_command}. After 30 s it answers pending, and the create and bootstrap go on.
  • A slow create still finishes. Each request has one idempotency key.
    • A 503 vm_operation_in_progress or 409 vm_create_in_progress is sent again with that key, up to 70 times 15 s apart. That covers the server's 16-minute provisioning budget.
    • The first send's team is pinned for the retries, the bootstrap and the idle-pause write, so a team switch in the app can't turn the key into a second machine in another team.
    • The bootstrap starts inside the creation's own create step, so the Retry button starts it too. A Retry skips the local plan gate to reach the server's lookup of its key.
  • A text flag with no value (e.g. --bootstrap-script last on the line) is refused, not ignored.
  • The app-server Cloud backend (CMUX_NEXT_CLOUD_LINK=app) refuses every option for now: its create takes none, and its machines have no file or exec route for a bootstrap.
  • A bare cloud new-machine, the palette and the menu take the old path unchanged.

Why upload over the file API instead of baking the bootstrap into the image (cloud-init, a first-boot unit): the lane bootstrap lives in leo-workspace and changes weekly, and the image bake is a separate, manual pipeline. The upload rides the signed-in person's authenticated link, holds no secret and needs no image change.

Enrollment (ssh route and acpmux peer) is #25263 (cmux cloud enroll-machine). The leo-workspace registry entries come from tools/lane-vm-enroll.

Supersedes nothing, and shares no files with #21028, #7560 or #18670.

Changelog

  • Added: cmux cloud new-machine --persistent --size --name --bootstrap-script makes a sized, never-idle-paused Cloud machine and starts its setup script in one command.

Observed / Not run

  • Ran here:
    • prepush-gate.py: no new violations.
    • The localization check: no new findings in the touched catalogs.
    • A local run of the start command and runner (the setsid path, as user cmux): it printed started, running and skipped, and wrote done and failed status as expected.
  • Review: Codex gpt-6-astra at xhigh through SR2, 10 rounds, SHIP at 36f2dc81ef03d6d7b5938c63fb3aaf0b1d13f017. Two read-only compile passes found three compile errors, all fixed: Kind was missing Equatable, a String + Substring concatenation, and a missing public. Round 8 noted that a session added after an active-team switch attaches with the new team's header. Round 9 confirmed that is base behavior, since every Cloud session uses the app's one API client, and not caused by this change.
  • Not run here (no builds on this host): the Swift suite CloudNewMachineTests and the Rust test new_machine_bootstrap_script_is_made_absolute. This PR's CI runs them. No live create either, since there is no signed-in app here; p0-machines runs the lane VM commands on a Mac.

No capture: CLI-only (no UI changes; the palette row and menu item are unchanged).

Timers/loops: the same-key create retry is bounded at 70 tries, 15 s apart, and ends with the create. There is nothing else in the app. On the machine, the bootstrap runner ends when the script does, and its unit is transient (--collect).

Risk: normal.

Agent session: acpmux 01a1756f-4a01-79f0-a405-561d20fd162c on gcp-agent-lane-1 (pm-newmachine, helper of p0-machines)

šŸ¤– Generated with Claude Code