Cloud: provision an API on a machine in one call, with revocable API keys

Merged
AU
6d ago
+4652-1727

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 answers url, token (the key, shown once), cookie, apiKey and publication.
  • API keys per publication. GET|POST /api/vm/publications/:id/api-keys lists and mints keys, and DELETE /api/vm/publications/:id/api-keys/:keyId revokes one. The new table cloud_vm_publication_api_keys stores 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-key cookie 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 answers url, key, cookie, publication_id, api_key_id and access. A person's run shows them in a copyable sheet with a ready-to-run curl line.
    • cmux cloud api-key-list --path <publication> lists keys.
    • cmux cloud api-key-revoke --path <publication> --key <id> --confirm revokes one (marked destructive).
  • Agents and code mode. The relay catalog gains vm.api.provision and vm.publication.api-key.list|create|revoke on fixed routes.

Defaults I picked

  • The key rides in a cookie, not Authorization: Bearer. Freestyle forward auth passes only the caller's Authorization and Cookie headers to our authorizer. Authorization already carries our service credential, and the edge does not strip it before the VM. The __Host-cmux-api-key cookie is added to the shared config's protectedCookies, so the edge withholds it from the VM and the VM cannot overwrite it. A client sends curl -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-keys once 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 with Domain=. 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.ts now expects an adopted config from before API keys to gain the API key cookie.
  • cmux-tui/bindings/typescript/code-mode/cloud-relay.test.mjs covers the new relay routes and rejects a bad key_id.
  • Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/CloudAPIClientProvisionAPITests.swift covers:
    • 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 9aab9e2e63caaf61c0b6f0d9 built the native test host and passed all 14 action/link tests at cd69fac469ff4fa7581ef0ae0986a0331505f955. 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. webviews matches the merged base exactly; its full bun run check passed on Freestyle, including formatting, lint (zero warnings/errors), type, protocol and layer checks.
  • Native regression: the existing menu test failed at dc5f7104dbe59ccd8bb8cc87c39aeaffcee366a6 because provisioning and enrollment shared key.horizontal. Fleet job 8bcdf7c672fa67637f3af9b1 at 69f1a75da132746731ccb20b4b951010e49fb2b0 confirmed 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 (401 versus 429); 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 Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.