Cloud: provision an API on a machine in one call, with revocable API keys
Merged
AU
6d ago+4652+4652+0123456789012345678901234567890123456789-1727-1727-0123456789012345678901234567890123456789
Fixes #25594
What
One call turns a port on a Cloud machine into a working, access-controlled HTTPS API, and its keys can be revoked.
- Server.
POST /api/vm/apis {vmId, port, accessMode?, name?, expiresAt?}publishes the port under a generated hostname, protected (personal by default, or team on a team VM). It reuses the port's live publication if one exists, then mints an API key. It answersurl,token(the key, shown once),cookie,apiKeyandpublication. - API keys per publication.
GET|POST /api/vm/publications/:id/api-keyslists and mints keys, andDELETE /api/vm/publications/:id/api-keys/:keyIdrevokes one. The new tablecloud_vm_publication_api_keysstores only the key's SHA-256 and its last four characters. A key can have a name and an expiry, and each publication holds at most 25 live keys. - Edge check. Forward auth accepts a live key sent in the
__Host-cmux-api-keycookie and answers 204. A key that is revoked, expired, unknown or malformed gets a plain 401, never a sign-in redirect. A key acts for the owner who minted it, so on a team VM it stops working once the owner leaves the team, even without a revoke. Deleting the publication or the account removes its keys. - cmux NEXT.
- Provision API on Machine… (
cmux cloud provision-api --target machine:<id> --port N [--name ci]) is in the machine menu's Tools folder and in the palette. For a CLI or agent run it answersurl,key,cookie,publication_id,api_key_idandaccess. A person's run shows them in a copyable sheet with a ready-to-runcurlline. cmux cloud api-key-list --path <publication>lists keys.cmux cloud api-key-revoke --path <publication> --key <id> --confirmrevokes one (marked destructive).
- Provision API on Machine… (
- Agents and code mode. The relay catalog gains
vm.api.provisionandvm.publication.api-key.list|create|revokeon fixed routes.
Defaults I picked
- The key rides in a cookie, not
Authorization: Bearer. Freestyle forward auth passes only the caller'sAuthorizationandCookieheaders to our authorizer.Authorizationalready carries our service credential, and the edge does not strip it before the VM. The__Host-cmux-api-keycookie is added to the shared config'sprotectedCookies, so the edge withholds it from the VM and the VM cannot overwrite it. A client sendscurl -H 'Cookie: __Host-cmux-api-key=<key>' <url>. - The protected cookie rolls out safely. Before any key is minted, the shared forward-auth config is brought current, so the cookie is protected before a key exists. A config that still has the older two-cookie list is recognized as ours and updated in place, not duplicated.
- Generated hostnames only. Provisioning uses a generated hostname. A custom domain can still get keys through
POST /api/vm/publications/:id/api-keysonce its publication is active. Public access is refused for provisioning and for minting keys, because a key would restrict nothing. - Going public revokes keys. Making a publication public revokes its keys before the edge gate is removed. Once the rule has no forward auth the cookie is no longer withheld, so a key never reaches the VM, and no key comes back if the publication is protected again. A retry that finishes an interrupted switch to public revokes them too.
- The cookie name resists planting. It is
__Host-cmux-api-key, so another publication under the same generated zone cannot plant it withDomain=. Only a well-formed key skips the browser sign-in handoff. - A retry keeps its access. Provisioning a port that is already published reuses that publication with its current access. Asking explicitly for different access is a clear 409 (
publication_access_mismatch), not a silent reuse. - Revoke keeps an audit trail. Revoke sets
revoked_at; it does not delete the row. Lists show only live keys.
Tests
web/tests/vm-publication-api-keys.test.ts(new) covers:- key shape;
- mint order (forward auth is made current before the key exists) and hash-only storage;
- refusal on a public publication, a bad name or a past expiry;
- revoke scoping, including malformed ids and team VMs the caller has left;
- provisioning, which refuses public access before publishing anything;
- forward auth: allow for a live key, 401 with no redirect for a dead one, the key stops working when its owner leaves the team, and the sign-in handoff still works without a key;
- the route reading the cookie;
- error copy.
web/tests/vm-publication-provider.test.tsnow expects an adopted config from before API keys to gain the API key cookie.cmux-tui/bindings/typescript/code-mode/cloud-relay.test.mjscovers the new relay routes and rejects a badkey_id.Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudAPIClientProvisionAPITests.swiftcovers:- the client's routes and bodies;
- decoding the server's answers;
- the fields a CLI or agent run gets back, and the curl line in the sheet;
- catalog wiring.
Observed / Not run
Head: e55638e6ba7ffbfe42fef2e58eb6046cdf26c5f5. Ordinary merges include feat-cmux-next through 2dbca164fb4a06a95cf754a821b88b0672e684d9, including the Unix-only dependency fix for Windows (#29075); no pushed history was rewritten.
- Catalogs: fleet job
9aab9e2e63caaf61c0b6f0d9built the native test host and passed all 14 action/link tests atcd69fac469ff4fa7581ef0ae0986a0331505f955. Its official exports are committed in this head. A strict comparison preserves all 1,290 base action rows unchanged and adds only the three Cloud API actions. Open as Page and its tab menu row are preserved. Counts: 1,293 actions, 769 CLI verbs, 685 MCP tools, 1,240 palette entries and 189 shortcuts. Official web generators and all 14 docs/shortcut tests passed on Freestyle. - Web: latest merged source passed typecheck, complexity, managed relay freshness, changed-file ESLint, all 103 publication tests and all 11 code-mode relay tests on Freestyle.
webviewsmatches the merged base exactly; its fullbun run checkpassed on Freestyle, including formatting, lint (zero warnings/errors), type, protocol and layer checks. - Native regression: the existing menu test failed at
dc5f7104dbe59ccd8bb8cc87c39aeaffcee366a6because provisioning and enrollment sharedkey.horizontal. Fleet job8bcdf7c672fa67637f3af9b1at69f1a75da132746731ccb20b4b951010e49fb2b0confirmed the Cloud duplicate was gone, every symbol drew, and outline checks passed. Only the same three base duplicate pairs remained; all three Cloud API client tests passed. Subsequent commits merge base changes and repair generated references without changing this feature's runtime source. - Compilation and review: exact-head Release compile passed through cmux-ci (
f0ffdf9228dc7baab76e948b, 109 seconds). Fresh independent correctness/security review of this exact head returned SHIP. - Static/localization: actual pre-push gates passed with no added violations. The localization ratchet passed all 158 catalogs before this final base-only merge; the independent final-head review verified the feature catalogs remain consistent. The five network/domain/publication messages moved into DaemonService retain their complete original translation dictionaries; the feature's Cloud action/API messages are localized.
- Classified CI findings: intermediate-head backend failure was the unchanged Google hooks rate-limit test (
401versus429); the same failure passed on the prior head's single retry (run). The intermediate database lane failed the unchanged Hexclave establishment-lock test, not migration application (job). Current-head checks are evaluated separately. - Previously established base failures: crash-class on PR and base name the same three missing OmnibarFieldEditor probes. Clippy on PR and base have the same indexing-slicing counts. Release-notary on PR and base fail the same runner/signing tests. These older-head comparisons do not replace current-head classification.
- Not run: live staging/Postgres with the migration, authenticated GUI provisioning, or a real signed-in Cloud capture. Database tests use fakes. No app was launched and no heavy build/test ran on this Mac.
Changelog
- Added: Provision API on Machine… gives a Cloud machine port an HTTPS URL and a revocable API key in one call (
cmux cloud provision-api,api-key-list,api-key-revoke).
No capture: the change is a CLI and palette action whose sheet is the standard Cloud answer sheet; it needs a signed-in Cloud account with a running machine, which the capture minis do not have.
🤖 Generated with Claude Code
![View with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v3-3x.png)
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Need help on this PR? Tag @codesmith-bot with what you need.