cloud: Freestyle devbox snapshot on the public platform (ubuntu user, base toolchain, Blaxel desktop), promote script, manifest as source of truth
Summary
Bakes the cmux Cloud devbox on the Freestyle public platform (api.freestyle.sh, freestyle@0.2.9) with the desktop layer from the retired Blaxel sandbox/cmux-devbox image, and makes the checked-in manifest the source of truth for the image users get.
Rebased onto main after #11602 / the E2B+Daytona removal and the two "record/refresh Freestyle snapshot" commits. Squashed to one commit.
How this lands next to what main already did
Main recorded cmux's own validated Freestyle snapshot (freestyle-cmux-devbox-20260902c, sh-940ec3bc…, mise-based image, baked on cmux's account) and wrote tests saying Freestyle resolves only through FREESTYLE_SANDBOX_SNAPSHOT. This PR goes the other way, on purpose, and reviewers should weigh in:
- The manifest is the only source of truth for images.
FREESTYLE_SANDBOX_SNAPSHOTis ignored. The resolver serves the manifest entry flaggeddefaultForKind(base when the client names no kind), in local dev and every deployed runtime alike; a client-requestedimagemust be in the manifest. Rollback is a manifest revert plus deploy. The env audit reports a lingeringFREESTYLE_SANDBOX_SNAPSHOTas stale configuration to remove; it is no longer a required key, and.env.exampleand the dev loader drop it. Production keeps serving the same image it does today because that image (…20260902c) is now the manifest base default. - The desktop devbox from this PR (
freestyle-cmux-devbox-20260902h,sh-1d66b9ca…) is listed as validated but NOT a default. Snapshots are account-scoped and this one was baked on Ben's Freestyle team, so it is a reference bake. Promote it under cmux's key with one command (below), commit the manifest diff, and it becomes the desktop+base default.
Design points (agreed with Freestyle)
- Use what the base ships.
freestyle/ubuntualready has Node LTS (nvm, symlinked into/usr/local/bin), Bun, Python 3.12, uv, Docker running from boot, and its own copies of Claude Code / Codex / OpenCode. The bake keeps that toolchain (no mise on Freestyle) and only replaces the agent copies with the exact Dockerfile pins via the base'snpm install -g, symlinking every agent bin into/usr/local/binso non-login daemon panes resolve them. Dropping the pins to track the base's versions is a one-step change if preferred. ubuntuis the one user. uid 1000, passwordless sudo, the API's default exec user and the SSH default. SSH, terminal panes, coding agents (claude --dangerously-skip-permissionsneeds non-root) and the desktop session all land in/home/ubuntu. The bake creates no users.- Login banner is
cmux welcome.cmux-motd(pam_motd) replaces the stock Ubuntu and Freestyle motd with thecmux cloudchevron logo the CLI's cloud SSH wrapper already uses, the Shortcuts block, and the Docs/Discord/GitHub/Email links. Ubuntu's legal notice is silenced, the devshell is sourced once from per-user rc files (after Ubuntu's ownPS1, so the cmux prompt wins), ble.sh's tput-cache seeds are readable by the work user (they were 0700 root, which made every first login print the tput notice), and the bake plus the verifier assert two real interactive logins asubuntuprint nothing from ble.sh or bash and show the cmux prompt.
What the desktop snapshot carries
The devbox plus the Blaxel desktop, ported to web/services/vms/images/devbox/desktop/:
- openbox + TigerVNC on 5901 (loopback only), noVNC via websockify on 6901 (the CLI's
cloudVMDesktopPort), tint2 dock (Chrome, Files, Ghostty), CC0 wallpaper, Ghostty 1.3.1 (pinned Ubuntu 24.04 community .deb), Chrome with first run pre-accepted, run by acmux-desktopsystemd unit asubuntu. - cmux-tui daemon under the
cmux-tui-daemonsystemd unit bound[::]:1337, unchanged contract (still runs as root; see follow-ups). - Includes main's
supports_websockets = falsecodex pin check.
Verified live with verify-devbox-image.ts: toolchain, exact agent pins from a clean login shell and a non-login shell as ubuntu (an earlier bake silently ran the base's Claude Code 2.1.234 in real logins; the verifier now catches that), ble.sh ghost text under a PTY, byte-identical baked files, agent-config generator, cmux-tui install + daemon on 1337, ubuntu sudo, docker as ubuntu, silent logins + cmux prompt, seeded tput caches, banner, desktop unit active, 5901 loopback-only + 6901 serving noVNC, openbox/tint2 up, 1440x900 display, Ghostty, Chrome first-run. Desktop and SSH were also exercised by hand on VMs published through a style.dev TLS rule.
The promote flow (the "script that outputs a snapshot id and commits it")
bun run devbox:promote -- freestyle # bake -> verify -> manifest write -> pointer slug; last line: IMAGE_ID sh-…
bun run devbox:promote -- freestyle --image sh-… # verify + record an existing snapshot
bun run devbox:promote -- freestyle --bake-result bake.json
promote-devbox-image.tsappends one manifest entry per kind flaggeddefaultForKind(anddefaultForLocalDev), demotes the previous defaults, never removes entries, and refuses anything the verifier did not pass.imageManifestProblemsguards the invariants (one default per kind, one local default, defaults validated, unique versions);tests/vm-image-manifest.test.tsruns them against the real file.- Bakes write a
--outJSON and printIMAGE_ID <id>as the last stdout line. Freestyle snapshot slugs are reassignable on the new API, so promote moves thecmux-devboxslug onto the new id as a dashboard convenience; production boots from the immutable id.
Other changes
build-devbox-freestyle.ts: files go in throughvm.fs.writeFile(atomic, sha256-verified, explicit modes);--no-desktop,--replace-slug,--keep-builder; a bake takes ~4 min.resolver.ts: an image listed under two kinds resolves to the entry of the requested kind (the desktop snapshot is a superset of a base one, so one snapshot serves both).defaultProviderAudit.mjs: manifest-only (see above); flags a staleFREESTYLE_SANDBOX_SNAPSHOT.- Manifest entries record
repoCommit. - READMEs for the devbox image and the VM service updated; package scripts
devbox:bake:freestyle,devbox:verify,devbox:promote.
Not in this PR (follow-ups)
- Promote the desktop devbox under cmux's Freestyle key (
bun run devbox:promote -- freestyle) so it becomes the default. - Freestyle driver still runs the daemon as root and pins
linuxUser: "root"; sessions should move toubuntu. - Ingress to 6901:
desktopWrapper.tsalready allowlists*.vm.cmux.sh; a Freestyle TLS rule (style.devneeds no verification) is the intended path, plus a per-VM token. With #11602's private VPCs the rule would target the VM's VPC address.
Verification
bun test tests/vm-*.test.ts tests/cloud-vm-*.test.tson the rebased branch: 349 pass / 0 fail.tsc --noEmitclean;eslintclean on the touched scripts.- Live: bake + verify (all checks passed) + promote on the public platform; desktop and SSH banner checked by hand.
🤖 Generated with Claude Code