Cloud: exec and file routes on a Cloud machine, in classic /api/vm's shapes (cmux-platform#89 B)
Leo, 2026-10-30, on building the app-server cloud path now instead of after the Saturday demo: "well why can't we just.... do our best now?"
Part of manaflow-ai/cmux-platform#89, piece B: exec, file and secret routes for app-server (CloudDO) machines. #27778 stays on HOLD until pieces A–C land.
Why
On the app-server link, cmux cloud deliver-secrets, apply-machine-config and credential recovery go through classic cmux.com /api/vm/{id}/fs and /exec. A CloudDO machine has no row there, so delivery fails at its first read and Claude starts signed out. Classic has no secrets route: deliver-secrets reads the machine config through fs, writes a receiver and a ticket through fs, then types the values over the daemon link. So exec plus fs is the whole gap.
What changes
New plain HTTP routes in cmux-api (cloud-machine-guest.ts), with the same requests and answers as classic, so the app reuses its classic decoding (piece C, cloud-np-c):
| Route | Body / query | Answer |
|---|---|---|
POST /v1/cloud/machines/{m}/exec | {command, timeoutMs?} | {exitCode, stdout, stderr}. Runs as the work user; 30 s default, 5 min max (the provider's inline exec limit; classic allows 15), 64 KiB command, at most 1 MiB of each output stream |
GET …/fs/dir?path= | {entries: [{name, kind, size, mode, modifiedAt}]} | |
GET …/fs/read?path= | {path, dataBase64, size} (8 MiB max) | |
GET …/fs/stat?path= | {path, kind, size, mode, modifiedAt} | |
POST …/fs/write | {path, dataBase64, mode?} | {ok: true, path}. Atomic temp file and rename; an existing file keeps its owner and (without mode) its mode; a new file without mode is 0600, as classic |
POST …/fs/mkdir | {path} | {ok: true, path} (recursive) |
DELETE …/fs/remove?path= | {ok: true, path} |
- Errors:
{ok: false, error: {code, message}}, with the classic code also in thex-cmux-vm-errorheader. Codes:vm_not_found,vm_file_not_found,vm_invalid_path,vm_invalid_command,vm_command_too_large,vm_file_too_large,vm_invalid_file_body,vm_invalid_file_mode,vm_not_running,vm_unavailable,vm_file_operation_failed. - Who may call: the same caller check as
client-session(callerFor, now exported with a refusal argument). That means a person's session, or a cli or mac install whose grant has execute, through team select and the SSO gate. Agent tokens and VM installs are refused. - Which machines:
CloudDO.ownerExec. The machine must be in the selected team (another team's machine is not found), and the caller must be able to use that team's machines now: their personal team, or a member withteam.resourcesand no departure heard during the read. Classic lets any member of the owning team do the same. cloud-np-a (piece A) agreed this rule. A'svm_kind: "owner"answer is a stricter rule of its own (the creator on their personal team), so the two never disagree. - Paths are trimmed and then checked by classic's rule: absolute, no NUL, no
..segment, at most 4096 bytes. - Exec output is capped on the machine by a small python3 wrapper. It reads both streams to the end, keeps 1 MiB of each, keeps the command's exit code and adds a note on stderr. A command that writes without end costs the machine CPU until its timeout, never memory or answer size.
- How the files are handled: a small python3 program run as root on the machine. Classic's file API also writes as root, and the client already chowns afterwards. The path is shell-quoted as one argument, and file content goes over stdin only.
- Audit: each call writes one
owner.execaudit row with who, the machine and the exit code. No command, file content or secret is ever written to an op, an event, a log line or the audit. - automationExec: now goes through the same
guestExechelper and stays creator-only. Its behaviour is unchanged, and a test pins it. - Fake provider: gains an opt-in scripted guest reply (
fakeControl({guest_reply})) and records each such call. Without a reply set it refuses other commands, as before.
Why cmux-api and not cmux-platform: CloudDO and its rows live here, and cmux-platform serves nothing authenticated outside development (scripts/check-inert.ts). cmux-platform's port doc also says "a fix goes to cmux first and comes along with the port". The area owner (cloud-demo) and piece A agreed on this. Staging gets these routes as soon as cmux-api deploys.
Risk
risk:high (auth): a new way to run a command as the work user, or a file operation as root, on a machine. The scope is no wider than what the same person already has through classic /api/vm and the daemon link's terminals on their team's machine.
- A machine can be reached only through its own team's CloudDO.
- Machine tokens and agent tokens are refused, and the caller's membership is read fresh on every call.
- Bodies are capped (about 11 MiB for a write) and read with a bounded reader.
remove /is refused.
No auto-resume: a paused machine answers 409, where classic would resume it.
Tests
test/cloud-machine-guest.test.ts(workerd), new:- Classic path, base64 and timeout rules, and the path quoting.
CloudDO.ownerExec: a machine still binding is refused, the audit carries no command, and a provider failure comes back asprovider_failed.- A teammate may; a caller from another team finds nothing; a member who has left is refused, and so is a creator who has left.
automationExecstays creator-only. - Over HTTP: classic shapes for exec (work user) and files (root, content over stdin only), and the file program's exit codes mapped to classic errors.
- Refused before any guest call: no auth, another person's machine (404), wrong method, a blank or oversized command, a bad path,
..,remove /, bad base64, a bad mode, a machine that is not running.
- The output cap, run locally: an endless writer was cut at the cap with exit code 7 kept and the note on stderr; a quote in the command passed through; a signal gave 143. A new file written without
modecame out0600. - The file program itself, run locally against a temp folder: mkdir, write (mode, then an overwrite that keeps the mode), stat, read, dir, plus missing file → 44, missing parent → 44, wrong kind → 45, and remove.
Review
- Round 1 (codex gpt-6-astra high) at
4f0df1ee: FIX. Four findings: new files0644, no output cap, timeouts above the provider's 300 s limit, untrimmed paths. All four are fixed in993d47bb. It found authorization, team selection, agent and VM-install rejection, departure checks and shell quoting sound.
Observed / Not run
- Ran on a lane host:
bun --bun x vitest runon cloud-machine-guest, automation-machine, platform-machines, cloud-client-session, cloud-providers and the cloud-acpmux/env/do/bind files, all passed.tsc -p .,check-file-sizeandprepush-gateare clean. - Not run: the full backend suite (CI runs it), and a live call against a real Freestyle VM on development (after merge).
Changelog
none
🤖 Generated with Claude Code
Agent session: agent session