cloud: Freestyle devbox snapshot on the public platform (ubuntu user, base toolchain, Blaxel desktop), promote script, manifest as source of truth

Merged
BE
2mo ago
+1960-669

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_SNAPSHOT is ignored. The resolver serves the manifest entry flagged defaultForKind (base when the client names no kind), in local dev and every deployed runtime alike; a client-requested image must be in the manifest. Rollback is a manifest revert plus deploy. The env audit reports a lingering FREESTYLE_SANDBOX_SNAPSHOT as stale configuration to remove; it is no longer a required key, and .env.example and 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/ubuntu already 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's npm install -g, symlinking every agent bin into /usr/local/bin so non-login daemon panes resolve them. Dropping the pins to track the base's versions is a one-step change if preferred.
  • ubuntu is 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-permissions needs 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 the cmux cloud chevron 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 own PS1, 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 as ubuntu print 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 a cmux-desktop systemd unit as ubuntu.
  • cmux-tui daemon under the cmux-tui-daemon systemd unit bound [::]:1337, unchanged contract (still runs as root; see follow-ups).
  • Includes main's supports_websockets = false codex 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.ts appends one manifest entry per kind flagged defaultForKind (and defaultForLocalDev), demotes the previous defaults, never removes entries, and refuses anything the verifier did not pass. imageManifestProblems guards the invariants (one default per kind, one local default, defaults validated, unique versions); tests/vm-image-manifest.test.ts runs them against the real file.
  • Bakes write a --out JSON and print IMAGE_ID <id> as the last stdout line. Freestyle snapshot slugs are reassignable on the new API, so promote moves the cmux-devbox slug onto the new id as a dashboard convenience; production boots from the immutable id.

Other changes

  • build-devbox-freestyle.ts: files go in through vm.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 stale FREESTYLE_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 to ubuntu.
  • Ingress to 6901: desktopWrapper.ts already allowlists *.vm.cmux.sh; a Freestyle TLS rule (style.dev needs 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.ts on the rebased branch: 349 pass / 0 fail.
  • tsc --noEmit clean; eslint clean 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

https://claude.ai/code/session_013M6fLnbEMj9WSDsCQS9z3C