Cloud legacy machine update: a DEV build takes its pin from `cmux link machine pin`
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 tomachine updateinlink/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 asmachine update:pinned_linux_payloadis split intoresolve_linux_pinplus a fetch, and the newcmux_remote::pinned_linux_pinstops beforefetch_published, so nothing is downloaded. - App (thin): if no manifest is bundled, the legacy update runs
<cli> link machine pinand 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 ofCmuxNextApp/Cloud/CloudLegacyMachineUpdate.swiftintoCmuxNextCloud/Move/CloudLegacyPin.swift, the same pattern as #30610.CloudLegacyMachineUpdate.Pinstays 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 --checkpasses;cargo test -p cmux-tui machine_pin3/3 (JSON shape, unavailable/rejected codes, usage);cargo test -p cmux-remote published_pin4/4.cargo clippy -p cmux-tui -p cmux-remote --all-targets -D warningsstops on twononminimal_boolerrors incrates/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 2at the head passed (app 71 s, cmux-tui 25 s). - Swift tests:
CloudLegacyMachineUpdateTestsgainsaDevBuildTakesThePinItsCLINames(CLI answer parsing, a tree pin accepted with no manifest) andanExecUpdateToTheTreesPublishingCommitIsAnUpdate(.updated(build: treeCommit), not a failure). Existing pin tests now callCloudLegacyPin.
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