Cloud legacy machine update: a DEV build takes its pin from `cmux link machine pin`

Merged
TE
4d ago
+312-84

Leo's ask: "whenever i start a chat it should be able to send me to cloud autommatically keep launching/downloading new dev builds and such".

Problem

A NEXT DEV build signed in to staging creates its Cloud machine through classic /api/vm, which deploys from main. main's web/services/vms/images/manifest.json bakes cmux-tui a6d89d44 (2026-10-28), which serves neither fs-v1 nor detached-terminals-v1. So cmux link machine update answers machine_update_unsupported (too old to update in place), and the app falls back to the legacy exec update (CloudLegacyMachineUpdate). That fallback only took its pin from the bundled Resources/bin/cmux-tui-ssh/manifest.json. A DEV build doesn't ship that file, so pin(besideCLI:) returned nil, and pin(for:besideCLI:) also needed the pin's commit to prefix-match the app's build. The first Cloud send then failed with "cloud-chats runs an older cmux that can't take this chat yet".

#30604 (merged) taught cmux_remote::pinned_linux_payload to resolve a DEV build's pin: from its own commit's published manifest or, for a tree-mode bundle, from tree/<key>/source.json's publishing commit. This PR hands that pin to the app's legacy path.

Change

  • cmux-tui: new read-only cmux link machine pin (link/machine_pin.rs, dispatched next to machine update in link/mod.rs). It prints one JSON line, {"ok":true,"commit":"<40hex>","sha256":"<64hex>","target":"x86_64-unknown-linux-musl"}, or {"ok":false,"error_code":"machine_update_unavailable",...} (exit 1; bad usage exits 64). It resolves the pin the same way as machine update: pinned_linux_payload is split into resolve_linux_pin plus a fetch, and the new cmux_remote::pinned_linux_pin stops before fetch_published, so nothing is downloaded.
  • App (thin): if no manifest is bundled, the legacy update runs <cli> link machine pin and uses the pin it returns. It takes that pin even when its commit is the tree's publishing commit, because the Rust side only returns this client's own commit or tree. The guest script and the exec flow are unchanged; the machine still checks the download against the SHA-256.
  • CloudMachineUpdateTarget.update: a legacy update that installed the pin's commit counts as .updated, even when that commit is the tree's publishing commit rather than the app's own. Before this, build.sameCommit(reported) turned it into a failure.
  • Line ratchet: the pin logic (manifest parse, CLI-answer parse, build match, isHex) moves out of CmuxNextApp/Cloud/CloudLegacyMachineUpdate.swift into CmuxNextCloud/Move/CloudLegacyPin.swift, the same pattern as #30610. CloudLegacyMachineUpdate.Pin stays as a typealias.

scripts/cmux-next/arch-ratchet.py --base origin/feat-cmux-next --head HEAD: app_lines -34 (CmuxNextApp -34), no architecture count rises.

Does staging /api/vm give the app exec?

Yes. On main (what cmux-staging deploys), /api/vm list/get/create return capabilities: vmCapabilitiesFor(provider) (web/app/api/vm/route.ts:227, :472; web/app/api/vm/[id]/route.ts:62). vmCapabilitiesOf defaults exec: declared.exec ?? true (web/services/vms/drivers/index.ts:74), and the only provider, Freestyle (index.ts:42-48), declares just { stats, sizing, desktop } (web/services/vms/drivers/freestyle.ts:1168), so exec is true. The app decodes that object in CloudMachine.Capabilities.init(from:) (Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudMachine.swift:203-216), and allows(.exec) reads it (CloudMachine.swift:287-289; the check is CloudMachineUpdates.allowsExec, CmuxNextApp/Cloud/CloudMachineUpdates.swift:145). The route exists on main at web/app/api/vm/[id]/exec/route.ts. So CloudMachineUpdates.allowsExec(session) passes for a staging machine.

Tests

  • Rust (cmux-ci test, Linux pool): cargo fmt --check passes; cargo test -p cmux-tui machine_pin 3/3 (JSON shape, unavailable/rejected codes, usage); cargo test -p cmux-remote published_pin 4/4. cargo clippy -p cmux-tui -p cmux-remote --all-targets -D warnings stops on two nonminimal_bool errors in crates/cmux-tui/src/cli/cloud_repo.rs:122,125. That file isn't touched here; the errors come from base under the fleet's clippy 1.95. With only that lint allowed (-A clippy::nonminimal_bool), clippy on both crates is clean.
  • Swift: cmux-ci compile-check --app --recipe-version 2 at the head passed (app 71 s, cmux-tui 25 s).
  • Swift tests: CloudLegacyMachineUpdateTests gains aDevBuildTakesThePinItsCLINames (CLI answer parsing, a tree pin accepted with no manifest) and anExecUpdateToTheTreesPublishingCommitIsAnUpdate (.updated(build: treeCommit), not a failure). Existing pin tests now call CloudLegacyPin.

Risk: low. When a manifest is bundled (release builds), the pin choice and its prefix-match rule are unchanged. The new path runs only when no manifest ships and the machine is already too old for machine update.

Agent session: agent session

šŸ¤– Generated with Claude Code