cmux NEXT: Cloud machine actions from the CLI need --target and return their result
An agent's cmux cloud kill-machine --confirm true with no --target used to kill whatever machine the person's window showed (or the only Cloud machine). Cloud machine actions run from the CLI, MCP, a script, a page or another client now act only on the machine they name, and they answer with their result instead of opening a sheet on the person's screen.
What changes
- No fallback machine for automation.
CloudHandlers.machinefalls back to the active window's machine (then the only machine) only fororigin == .user. Any other run without--target machine:<id>is refused with the existingCloudStrings.noMachine("…pass --target machine:."). This covers every machine action: kill, pause, resume, resize, snapshot, restore, delete-snapshot, hand-off, exec, files, ports, and the rest. New Cloud Workspace keeps the fallback for every caller, because a missed machine there only opens a workspace. cloud new-machinereturns the machine. A CLI/MCP run answers withresult: {machine_id, title, status, target: "machine:<id>"}once the server returns the machine. A slow server answers{pending: true, creation_id}after 30 s, insideaction.run's 40 s result deadline, so a caller never reads a slow create as a failure and starts a second machine. A declined or dismissed create is refused.cloud execreturns output. For a non-person run on a machine that allows API exec, the command goes throughPOST /api/vm/{id}/exec(25 s cap) and returns{exit_code, stdout, stderr}. It opens no tab. A person's run still types the command into a new tab.- Other Cloud reads return data instead of a sheet when the run is not a person's:
machine-status,machine-ports,machine-tools,hand-off-machine,snapshot-machine,promote-machine-to-template,files-list,file-read,file-stat,prepare-scp,network-list,firewall-list|get,domain-listandpublication-listreturntextplus structured fields.new-terminal-on-machinereturnsworkspace_idand moves the window only when the run asks for focus. A person's run is unchanged. This follows the pattern #23109 uses for diagnostics. action list/action describesay why an action is unavailable. An action whoserequirescontext is missing now carriesmissing_context,available_with_target(for Cloud machine actions,["machine"]) and a localizedcontext_reason("Needs cloudWorkspace in the focused window, or pass --target machine:.").
The CLI already prints action.run's result, so no Rust change is needed. Actions that now answer with data are marked waitsForResult, which gives them the 40 s result deadline.
Files #23109 edits were avoided: no hunk touches cloudDiagnostics/diagnostics(), CloudPresenter.swift, AppControl+Cloud.swift or CmuxNextDaemonService/CloudStrings.swift. The one change in CloudHandlers+Account.swift is in bindCreation, away from the diagnostics region. New strings live in the app module's Cloud.xcstrings and CmuxNextControl/Localizable.xcstrings, with every locale.
Changelog
- Changed: Cloud machine actions run from the CLI or an agent need
--target machine:<id>and return their result (new machine id, command output, status) instead of opening a window.
Tests
CloudCLITargetTests(CmuxNextAppTests): CLI/MCP/script/remote/page runs without a target are refused, a named target resolves, a missing one isnot_found, the person keeps the fallback, a run nested inside a CLI run is refused too, a CLI answer comes back as the run's result, slow work answerspendingwhen its wait ends and quick work answers with its result, a refusal in the work reaches the caller as one, and new-machine answers with the machine,pending, declined and failed.ActionContextGapTests(CmuxNextControlTests): the unavailable reason inaction.list/describe.
Red/green on the fleet (cmux-ci run --class light --script scripts/ci/run-swift-testing-suites.sh --arg Packages/macOS/CmuxNext --red):
- Red, first round: base
feat-cmux-nextplus only the target tests (scratch 80c30ad43e8, step 0237514e).aRunThatIsNotThePersonsNeedsAMachineTargetfails for all 5 origins (cli, mcp, script, remote, page) with "an error was expected but none was thrown and CloudMachineSession was returned". That's the fallback bug. - Red, review fixes: 4d9d68fb3a9 plus only the new tests (scratch 2ce8b4f13f2, step a233d043). Two tests fail:
aNestedRunInsideACLIRunNeedsAMachineTarget: no error is thrown, so a nested run still gets the fallback.slowWorkAnswersPendingWhenItsWaitEnds: the answer took 8.53 s against a 5 s limit, with a 100 ms wait and 8 s of work. The race waited for the work.
- Green: head e7f28eaa458 (step a9594174).
CloudCLITargetTestsandActionContextGapTestspass. - Other suites fail in the same step on both sides: 35 fail on both the red and the green run.
- Only on green:
VideoWallpaperTests, a CoreVideoCVPixelBufferPoolCreatePixelBuffersegfault inVideoJob.frame. That's code this PR doesn't touch. - Only on red: three palette and space-switch timing suites.
- Only on green:
Index out of rangein PR CI'scmux-next swift test. This isAgentPaneImageActionsTests.showInFinderSelectsANamedCopyOfTheImage, not this PR:revealed().countis 4 where the test expects 5. The test then readsrevealed()[4]unchecked (AgentPaneImageActionsTests.swift:75), which crashes the whole single-process run.- The same crash happens on the base-plus-tests scratch run (step 0237514e, base 8697514a20e). This PR doesn't touch that file or module.
- Base push runs skip
cmux-next swift test(itsif:excludes pushes tofeat-cmux-next), so base has no log of its own to compare.
- An earlier red attempt on adfb21f4d11 didn't count: base
CmuxNextAppTestsdidn't compile until 0f6199ce7cd, so the branch now merges base.
Observed / Not run
python3 scripts/cmux-next/prepush-gate.py: no new violations.- Review fixes since the independent review of 4d9d68fb (NO-SHIP):
- Snapshot and promote-template now answer
pendingat 30 s. The oldwithTaskGrouprace awaited the work'sTask.value, which ignores cancellation, so it waited for the whole snapshot. Both sides now yield into oneAsyncStream(firstAnswer). CloudHandlers.machine,answer, exec and new-terminal readActionRunScope.current?.origin ?? invocation.origin, so a nested perform inside a CLI run gets no fallback.- A CLI or agent
cloud execon a machine without API exec is refused withcloud.cli.execUnavailable, in every locale ofCloud.xcstrings. Before, it typed into a tab whose output it couldn't return.
- Snapshot and promote-template now answer
- The
cmux-cloud-vmskill docs say--targetis required and that results come back. ./scripts/localize-changes --catalog …for both catalogs: the new keys are translated in every locale the catalog carries.localization_catalog.py checkshows the same base parity error count with and without this PR (41373 at this head).- Not run: no live app, CLI, socket or MCP calls (by rule). The CLI behavior is read from
ControlRouter+ActionRun.swift(result) andcli/app.rs(prints the reply).
Not in this PR (follow-ups)
cmux machine list/--catalog/machine <vm-id> showdon't list Cloud machines. These are daemon methods (resource_api.rsLocalResourceMachineService), not app methods. The app only feeds--catalogthroughMachineCatalogPublisher→MachineRegistry.catalogRecords(), which drops Cloud machines with nomachineResourceID, andMachineCatalogRecord.validIDaccepts onlymachine_<32hex>, nevervm-…. A full fix needs both sides: Rustmachine.rs/resource_api.rs/machine_catalog.rsacceptvm-ids, a compatibility field, and remote records without--catalog; SwiftMachineCatalogRecordgains those fields. The cheaper alternative is to have the Rust CLI merge the app'scloud.machinesrows intomachine listfor thecmuxsurface. Until then,cmux app call cloud.machineslists them.- Reading a Cloud machine's terminal from the CLI.
cloud execno longer needs it. In general, aterm_…on a Cloud daemon givesselector.not_found, because the CLI talks to one daemon socket, and<session>:term_…givessocket.unreachable, becausefederation.rssays Cloud and SSH sessions "have no CLI transport yet".PublicIDTargetResolver.publicIDalso returns bare ids increated. This needs an app-proxied terminal read or a CLI transport to remote sessions. cmux workspace new-on-machine --machine <id>fails with "requires --machine": the global-flag parser incli.rs(--machineroutes a daemon) takes--machinebefore the action sees it, except afteraction run. This needs a Rust fix that passes--machinethrough for CLI action verbs whose schema has amachineargument.- A paused machine's stage reads
failed.CloudMachineStageInput.stagemapspaused+ idle link to.failed(machinePaused). It should be its own stage. That touches the progress view and sidebar, so it's a separate PR. workspace new-on-machinestill returnscreated: []: its workspace is created on a remote daemon, andscope.createdonly resolves home-daemon objects to public ids.
No capture: CLI, MCP and agent results only (JSON answers, refusals and action describe fields), so nothing on screen changes. A person's own runs still open the same sheets and tabs.
🤖 Generated with Claude Code
Agent session: cosmic-gray