cmux-cloud-config: NEXT Cloud's config, auth and VM agent-key rules in Rust, over the app FFI and in the cmux vm CLI (step A)
Step 1 of moving NEXT Cloud's logic out of Swift into Rust, as the two-step app FFI move the repo uses for every new app FFI rule (step A here; step B re-pins the app FFI and switches Swift).
Leo, 2026-11-04, asked: "does the swift part need to be in/is it good for it to be in swift?" Shown the plan: "Yes, start it". Also Leo, 2026-11-04: "anything that we're pointing at cmux-classic related stuff (that isn't customer related) just point it away from classic".
What moves to Rust
A new pure crate, cmux-tui/crates/cmux-cloud-config (serde only, no I/O), with the Swift tests ported:
config: the backend pick ofCloudConfiguration(+Previews): environment, API origin, sign-in origin, Stack project and key, callback scheme, link source, the token store's names (Keychain service, file fallback path, defaults keys), the owner, share, preview and artifacts APIs, the share-API allowlist, the capture-build test and the VM API base (vm.cmux.dev/vm-staging.cmux.dev).auth: the Auth folder's logic that is not Keychain I/O:DogfoodCredentials' account pick (the app still reads the files and checks the explicit file's mode), the auth launch keys and their merge, the log mask, andActiveTeam's refusal and managed-team rules. Token storage, the Stack client and the team save stay in Swift (the token expiry and refresh logic lives in the sharedCmuxAuthRuntimepackage, outside this folder, and is not touched here).agent_key:vm.agent_key.create's request (VM scopes only, name default and cut, limits refused never dropped), its URL per backend, the answer (addsbaseUrl/teamId, never puts the key in a refusal) and the refusal codes' meaning for the CLI's exit status.
cmux-layout-reducer-ffi's JSON channel (the app's one Rust library, the same channel cmux-cloud-start-core uses) serves them as cloud_config_*, cloud_auth_* and cloud_vm_agent_key_*, with C ABI tests (tests/cloud_config.rs). The app keeps doing the HTTP call and the Keychain read; it sends facts and acts on the answer.
The CLI shares the same code
The cmux vm CLI is already Rust (workers/cmux-vm/cli, linked in process by cmux). Smallest design that truly shares the code: the cmux-vm crate takes a path dependency on cmux-cloud-config, so cmux vm token create with its own credential (CMUX_VM_API_KEY holding a team key or a session token, plus CMUX_VM_TEAM_ID, no app) builds its body with agent_key::body, the exact function the app's vm.agent_key.create will call in step B; and cmux's app-mint path maps refusals through agent_key::outcome. No HTTP client goes into the app library (the app FFI links none), and no new binary. cmux-vm-clients.yml now also runs on cmux-tui/crates/cmux-cloud-config/**.
Behaviour change in the CLI: an empty --name now gets the default name and a name over 200 characters is cut, as the app path already did.
DEV repoint
Leo, 2026-11-04: "ok and so in future, we shouldnt need to get so confused by classic cloud, right? like we dont touch it but we dont have to keep calling the same routes... were doing new cloud with cmux-platform and stuff".
In the Rust rules, DEV, NEXT DEV and every other development build no longer use https://cmux-staging.vercel.app as their Cloud API: Configuration.cloud_api_base_url (the base of the app's machine client, CloudAPIClient) is the development owner API Worker (https://cmux-api-development.debussy.workers.dev, NEXT Cloud, which forwards to cmux-platform). Production keeps cmux.com; a debug build that names an origin (CMUX_VM_API_BASE_URL, CMUX_API_BASE_URL, CMUX_DEV_BACKEND_URL, local mode) keeps it. The owner API, the VM API (vm-staging.cmux.dev), previews and artifacts were already NEXT services. The app takes this in step B, when CloudAPIClient reads cloudAPIBaseURL; this PR changes no app behaviour by itself. Test: no_development_build_uses_the_classic_staging_web_app_as_its_cloud_api.
What stays on the staging web app (api_base_url, auth_web_origin), because cmux-platform and cmux-vm have no route for it yet: the hosted sign-in pages (handler/native-sign-in, handler/after-sign-in, auth/callback), the team API (/api/teams, invites), /api/account, /api/device-tokens, CodeRouter accounts, billing, and Sandboxed runs (/api/runs, which needs a design on /v1/handoffs). The classic machine routes that DEV still sends through CloudAPIClient will hit the Worker after step B; the table below says which have an owner op to repoint to and which wait on a platform route (they already fail on the default link today, since they send NEXT machine ids to the classic app).
CMUX_CLOUD_LINK=legacy / CMUX_NEXT_CLOUD_LINK=legacy: its only users are the .legacy branches listed under "Legacy-link only" below (nothing in CI, scripts or the capture rig sets it; the capture scene registry has a legacy-link scene). It and CloudClassicPageOps get deleted, not moved, in the repoint PR; this PR keeps the rule as is so step A changes no behaviour.
Deploys and publishes on merge (classic untouched)
Leo, 2026-11-04: "apparently the api if we change it it'll auto deploy so we wanna make extra sure we dont change it for clasisc". This PR changes nothing in web/, backend/ or any classic API: git diff --name-only has no path there, and backend.yml and the classic web deploys are not triggered. What a merge into feat-cmux-next does trigger, path by path:
workers/cmux-vm/cli/**(the NEXT cmux-vm CLI crate) triggerscmux-vm.yml, whose push run redeploys the vm-staging Worker. The Worker bundle issrc/index.ts(wranglermain) and its TypeScript imports; nothing insrc/,wrangler.jsonc,package.jsonorscripts/deploy.shrefers tocli/, and no file underworkers/cmux-vm/outsidecli/changes, so that run ships an identical Worker (its check, smoke and rollback gates run as on any push).app-ffi-release.yml: a new path (cmux-tui/crates/cmux-cloud-config/**) and new FFI sources, so the merge publishes one newcmux-app-ffi-<sha>prerelease (make_latest false). That is the NEXT app's Rust library, which nothing links until step B pins it; the existing pin and every build are unchanged.cmux-vm-clients.yml: one new trigger path; its jobs only regenerate and test the cmux-vm CLI and SDK and upload CI artifacts. It never deploys or publishes.cmux-tui/Cargo.lock/Cargo.tomlchanges run the usual NEXT cmux-tui builds (cmux-tui artifacts, cmux-browser-host release, which publishes only on its own tag); same as any cmux-tui dependency change.
Split (why this is not one PR)
- This PR (step A): the Rust rules, the FFI ops, the CLI sharing. No app behaviour change.
- Step B (next, after this lands and the app FFI release publishes): pin
CCmuxAppFFI, makeCloudConfiguration, the Auth helpers andAppControl+VMAgentKeycall the ops, delete the Swift copies and their tests. - The classic-route repoint/delete (table below): its own PR, since it is a different seam (the Swift REST client) and several rows wait on platform routes.
Classic /api/vm and /api/runs calls in NEXT
NEXT macOS app: calls to classic /api/vm and /api/runs
Base e1869abf57c8 (feat-cmux-next). cmux-platform at origin/main 393544a7.
Paths below are relative to Packages/macOS/CmuxNext/Sources/.
Origins used in the "replacement" column:
- OWNER: the owner API Worker,
CloudConfiguration.ownerAPIBaseURL():https://cloud-api.cmux.dev(prod),
https://cmux-api-development.debussy.workers.dev(signed DEV and every non-prod build). Today this is cmux's
backend/apps/apiWorker; cmux-platform has a ported copy (src/server/cloud/Ops.ts"ported from cmux
backend/apps/api/src/http.ts",MachineGuest.ts,AcpmuxRoute.ts). Ops go throughPOST /v1/ops(the app's
appOps/ credential relay); guest I/O is direct/v1/cloud/machines/{m}/.... These are not in platform's
openapi.json(hand-rolled routes). - PLAT: the cmux-platform HttpApi Worker (
openapi.json):cmux-platform-development|staging.debussy.workers.dev.
NEXT reaches it only throughplatformPreviewAPIBaseURL()(Debug + non-prod + appServer only; nil in Release/prod)
andartifactsAPIBaseURL(). - cmux-vm (
workers/cmux-vm, vm.cmux.dev / vm-staging.cmux.dev) is the provider layer cmux-platform itself calls
(src/server/cloud/vmapi/CloudProvider.ts,MeshMaintenance.ts,receiver/Native.ts). Its ids are provider VM ids,
not the app'svm_...machine ids, so it is never the direct repoint target from the app; it is listed only where
it holds the capability the platform has not yet exposed to owners.
Link source: CloudConfiguration.resolve makes .appServer the default for every build (DEV, Debug, prod);
.legacy only with CMUX_NEXT_CLOUD_LINK=legacy / baked CMUX_CLOUD_LINK=legacy. cloud.api is always the
classic client on apiBaseURL (CloudService.swift:176); cloud.machineAPI is the Worker client on appServer and
cloud.api on legacy (CloudService.swift:195). So any cloud.api.X(...) that is not behind a linkSource / session.link
guard sends a Worker vm_... id to cmux-staging.vercel.app / cmux.com /api/vm/... on the default link: that is a
live bug (marked unguarded below), not just legacy debt.
| file:line | method + classic route | Swift method / callers (short) | NEXT replacement (route, origin) | action |
|---|---|---|---|---|
| CmuxNextCloud/API/CloudAPIClient.swift:83 | GET /api/vm | listMachinesAndLimits(); CloudService.swift:528 (legacy branch of refresh). listMachines() :76 has no callers | OWNER /v1/ops cloud.machine.list (already appServerMachines()); limits: cloud.plan.get (appServer sets limits = nil today, so plan sizes are empty) | delete (legacy-only); listMachines delete |
| CloudAPIClient.swift:88 | GET /api/vm/{id} | machine(_:); CloudHandlers+Machine.swift:291 (palette.cloud.status), :325 (palette.cloud.handoff) unguarded; CloudCodexBrokerTarget.swift:123 default isLive (legacy only, appServer passes its own) | OWNER /v1/ops cloud.machine.get | repoint (status, handoff); broker default delete |
| CloudAPIClient.swift:108 | POST /api/vm | createMachine via createWithKey (CloudAPIClient+CreateWithKey.swift:14); CloudService.swift:790 (legacy branch of create) | OWNER /v1/ops cloud.machine.create (already appServerCreate) | delete |
| CloudAPIClient.swift:114 | PATCH /api/vm/{id} | renameMachine; CloudService.swift:879 unguarded, from CloudHandlers+Machine.swift:162 and CloudAppOps.swift:143 (classic page backend) | OWNER /v1/ops cloud.machine.rename | repoint |
| CloudAPIClient.swift:118 | DELETE /api/vm/{id} | deleteMachine; CloudService.swift:831 (legacy branch) | OWNER cloud.machine.delete (already used) | delete |
| CloudAPIClient.swift:123 | POST /api/vm/{id}/pause | pauseMachine; CloudService.swift:844 (legacy branch) | OWNER cloud.machine.pause (already, CloudMachinePower) | delete |
| CloudAPIClient.swift:128 | POST /api/vm/{id}/resume | resumeMachine; CloudService.swift:852 (legacy branch) | OWNER cloud.machine.start (already) | delete |
| CloudAPIClient.swift:135 | PUT /api/vm/{id}/idle-pause | setIdlePause; CloudService.swift:799 (after create of a persistent machine, both links) and :860 unguarded (CloudHandlers+Machine.swift:256 cloudSetIdlePause) | OWNER /v1/ops cloud.machine.idle_policy.set | repoint |
| CloudAPIClient.swift:141 | POST /api/vm/{id}/attach-endpoint | attachEndpoint; CloudMachineLink.swift:212 (legacy link), CloudService.swift:284 (callerOwnsVM legacy branch), CloudCodexBrokerTarget.swift:90 default (legacy only) | app link cloud.machine.connect_info / cloud.machine.link_token; owner check = appLink.ownerLink (already) | delete |
| CloudAPIClient.swift:146 | POST /api/vm/{id}/cmux-remote/approve | approveCmuxRemoteEnrollment; CloudMachineLink.swift:259 (legacy link) | none needed (app link enrolls itself) | delete |
| CloudAPIClient.swift:151 | GET /api/vm/{id}/stats | stats; CloudHandlers+Machine.swift:290 palette.cloud.status unguarded (allows(.stats) is true: appServer records declare no capabilities) | none (no stats op on OWNER; PLAT /v1/machines/usage is billing usage, not live stats) | no replacement yet |
| CloudAPIClient.swift:161 | POST /api/vm/{id}/resize | resize; CloudHandlers+Machine.swift:249 unguarded | OWNER /v1/ops cloud.machine.resize (PLAT /v1/machines/{id}/resize is the team-machines family) | repoint |
| CloudAPIClient.swift:167 | POST /api/vm/{id}/snapshot | snapshot; CloudHandlers+Machine.swift:333 (snapshot), :353 (promote-template) unguarded | OWNER /v1/ops cloud.snapshot.create | repoint |
| CloudAPIClient.swift:173 | GET /api/vm/{id}/snapshots | snapshots; no callers | (OWNER cloud.snapshot.list if ever needed) | delete |
| CloudAPIClient.swift:178 | DELETE /api/vm/{id}/snapshots/{sid} | deleteSnapshot; CloudHandlers+Machine.swift:344 unguarded | OWNER /v1/ops cloud.snapshot.delete | repoint |
| CloudAPIClient.swift:182 | POST /api/vm/restore | restore(snapshotID:); CloudHandlers+Machine.swift:361 unguarded | OWNER /v1/ops cloud.snapshot.restore | repoint |
| CloudAPIClient.swift:189 | POST /api/vm/{id}/fork | fork; CloudHandlers+Machine.swift:369 unguarded | no cloud.machine.fork op in Ops.ts; PLAT POST /v1/machines/{id}/fork exists but on the team-machines family (id namespace vs vm_... not verified) | no replacement yet |
| CloudAPIClient.swift:194 | POST /api/vm/{id}/open-port | openPort; CloudHandlers+Browser.swift:85 (cloudMachineLink, no appLink guard), CloudHandlers+Machine.swift:240 (cloudCopyLink) unguarded | PLAT POST /v1/cloud/machines/{machine}/previews/{port}/links (+ GET .../previews/{port}), via platformPreviewAPIBaseURL() = Debug non-prod only; app-local cloud.port.forward / cloud.browser.open for local forwards | repoint (DEV/Debug); prod blocked until previews have a prod origin |
| CloudAPIClient.swift:218 | POST /api/vm/{id}/acpmux/keyed-start | acpmuxKeyedStart; CloudService+PersonKey.swift:104 (legacyApprovalsRoute, appLink == nil) | OWNER POST /v1/cloud/machines/{m}/acpmux/keyed-start (already, CloudOwnerAPIClient) | delete |
| CloudAPIClient.swift:228 | POST /api/vm/{id}/acpmux/person-holds | acpmuxPersonHolds; CloudService+PersonKey.swift:105 (legacy route) | OWNER POST /v1/cloud/machines/{m}/acpmux/person-holds (already) | delete |
| CloudAPIClient.swift:238 | POST /api/vm/{id}/scp-endpoint | prepareSCP; CloudHandlers+Machine.swift:131 cloudPrepareSCP unguarded | none (OWNER has /v1/cloud/machines/{m}/client-session, a provider session, not an scp endpoint) | no replacement yet |
| CloudAPIClient.swift:253 | POST /api/vm/tunnel | enrollTunnel; CloudTunnelHub.swift:200. Hub built whenever the cmux-tui binary exists (CloudService.swift:258); used by legacy CloudMachineLink, startHubForSSHRoutes after every list refresh when ssh routes exist (CloudService.swift:870), cloudEnrollMachine (CloudHandlers+Network.swift:79/114) | Native network route for this Mac: PLAT GET /v1/host/network/target, PUT /v1/mesh/nodes/register (CloudNativeRouteHolder); backed by cmux-vm meshes/devices. No owner route that enrolls an ssh-route WireGuard hub | legacy link part delete; ssh-routes/enroll-machine no replacement yet |
| CloudAPIClient.swift:258 | DELETE /api/vm/tunnel?deviceId= | revokeTunnel; CloudTunnelHub.swift:128 (sign-out) | as above (PLAT /v1/mesh/nodes/{nodeId}/revoke) | follows the row above |
| CloudAPIClient.swift:262 / :266 / :270 | POST /api/vm/tunnel/network/attach / detach / rotate-key | attachTunnelNetwork / detachTunnelNetwork / rotateTunnelKey; CloudHandlers+Network.swift:13 / :17 / :21 unguarded | none owner-facing (cmux-vm /v1/meshes/{id}/devices, /v1/devices/{id}/rotate-key, platform-internal) | no replacement yet |
| CloudAPIClient.swift:284 | GET /api/vm/network | listNetworks; CloudHandlers+Network.swift:25 unguarded | none (cmux-vm /v1/meshes, platform-internal) | no replacement yet |
| CloudAPIClient.swift:279 / :288 / :294 / :310 | GET /api/vm/firewall[?vpcId..], GET ?ruleId=, POST, DELETE ?ruleId= | listFirewallRules / getFirewallRule / createFirewallRule / deleteFirewallRule; CloudHandlers+Network.swift:32 / :40 / :58 / :47 unguarded | none (cmux-vm /v1/meshes/{id}/acl, platform-internal) | no replacement yet |
| CloudNetworkPolicy.swift:146 / :152 | GET / PUT /api/vm/{id}/network | networkPolicy / setNetworkPolicy; CloudHandlers+Network.swift:62-63 → CloudNetworkAccessActions.swift:24 guard == .legacy | none owner-facing (cloud.vm.network_policy is the guest's own read; cmux-vm public-egress is platform-internal) | legacy-only; no replacement yet (delete if Internet Access is dropped) |
| CloudAPIClient.swift:319 | GET /api/vm/domains | listDomains; CloudHandlers+Domains.swift:10 unguarded | none | no replacement yet |
| CloudAPIClient.swift:325 | GET /api/vm/publications | listPublications; CloudHandlers+Domains.swift:22 unguarded | none (nearest: PLAT previews GET .../previews/{port}, .../links) | no replacement yet |
| CloudAPIClient.swift:335 | POST /api/vm/apis | provisionAPI; CloudHandlers+Domains.swift:44 unguarded | none (cmux-vm https-proxies + api-keys, platform-internal; PLAT preview grants/links are the nearest owner routes) | no replacement yet |
| CloudAPIClient.swift:341 / :347 | GET / DELETE /api/vm/publications/{p}/api-keys[/{k}] | listPublicationAPIKeys / revokePublicationAPIKey; CloudHandlers+Domains.swift:52 / :63 unguarded | none (PLAT GET/DELETE .../previews/{port}/links[/{linkId}] is the nearest) | no replacement yet |
| CloudAPIClient+ChatGrants.swift:29 / :34 / :41 (path :45) | GET / PUT / DELETE /api/vm/{id}/chat-grants[/{user}] | chatGrants / grantChat / revokeChat; CloudMachineShare.swift:30, :35, :48, :56; guarded by session.link != nil (:28) and hidden on appServer (CloudHandlers+Machine.swift:200-203) | PLAT PUT/DELETE /v1/machines/{id}/chat-grants/{user}, GET /v1/machines/{id}/chat-access (caller's own access; no list-of-grants GET); team-machines id namespace | legacy-only; repoint blocked on a grants-list route |
| CloudAPIClient+MachineRoutes.swift:37, 46, 53, 67, 73, 79, 85 | POST exec, GET fs/dir, GET fs/read, POST fs/write, POST fs/mkdir, DELETE fs/remove, GET fs/stat under machinePath (/api/vm on the classic client) | exec/listFiles/readFile/writeFile/makeDirectory/removeFile/statFile; callers use machineAPI (Worker on appServer) except CloudGitHubHandlers.swift:24 cloud.api.exec (Set Up GitHub: classic route on appServer, a bug) | OWNER /v1/cloud/machines/{m}/exec, /fs/* (already the Worker mode of the same methods) | repoint CloudGitHubHandlers to machineAPI; the classic mode goes with legacy |
| CloudAPIClient.swift:372 | POST /api/runs | createSandboxRun; AppControl+Cloud.swift:159 (runs.sandbox.create), also inside runSandbox :392 / sandboxSnapshot :406 from AgentPageChats.swift:174 (Sandboxed send). All link sources | candidate PLAT POST /v1/handoffs (NEXT already has CloudHandoffs.swift for list/wait); different model (ho_ ids, turns, no agent/image) | no replacement yet (needs a Runs-on-handoffs design) |
| CloudAPIClient.swift:379 | GET /api/runs/{id}/wait?timeoutMs= | waitRun; AppControl+Cloud.swift:167, SandboxRunNotices.swift:31 (SandboxRunFollower, path label SandboxRunFollower.swift:48), runSandbox poll | candidate PLAT GET /v1/handoffs/{id}/wait | no replacement yet |
| CloudAPIClient.swift:443 | POST /api/runs/{id}/cancel | cancelRun; AppControl+Cloud.swift:174 | candidate PLAT POST /v1/handoffs/{id}/cancel | no replacement yet |
Not call sites (comments / error labels only): CloudTunnelHub.swift:199, CloudMachineLink.swift:262, CloudAPIClient.swift:243/401/420/423/430/434/438/536, SandboxRunNotices.swift:59, CloudStrings+Errors.swift:47, AgentTabs+Wiring.swift:244/451, CloudHandlers+Account.swift:88, CloudAppOps.swift:47, CloudService.swift:224/298/740/745/751, CloudNetworkAccessActions.swift:23, CloudHandlers+Machine.swift:200, CloudWorkspaceSetupWizard+Size.swift:5. CaptureSceneRegistry.swift:449-454 builds a legacy CloudMachineLink for capture scenes (goes with legacy). CloudClassicPageOps.swift makes no request itself: it runs the CloudAppOps.swift:125-147 closures (cloud.refresh/resume/pause/rename/delete) and is built only when linkSource == .legacy (PageFactory.swift:121-122).
Notes
Blocked: no NEXT route yet (a repoint PR waits on a platform route)
stats(palette.cloud.status),fork,scp-endpoint.- open-port in production: PLAT previews work, but NEXT has no prod preview origin (
platformPreviewAPIBaseURL()is nil outside Debug non-prod). - Tunnel networks (attach/detach/rotate-key), networks list, firewall CRUD, domains, publications,
apis+ API keys, per-machine network policy: cmux-platform calls cmux-vm for all of these internally (vmapi/CloudProvider.tshttps-proxies,public-egress;MeshMaintenance.tsmeshes/devices) but exposes no owner route. - The ssh-routes WireGuard hub (
POST/DELETE /api/vm/tunnel, used bycloud enroll-machineandstartHubForSSHRoutes): the Native route (/v1/host/network/target,/v1/mesh/nodes/register) covers the app link, not this. - Chat grants: PLAT has PUT/DELETE per user and the caller's own
chat-access, but no GET that lists a machine's grants (CloudMachineShare's roster needs it). The team-machines/v1/machines/{id}id namespace is not verified to match the app'svm_...ids. /api/runs(Sandboxed send,cmux run --sandboxed):/v1/handoffsis the nearest, but it needs a design, not just a new path.
Ready to repoint now (the op exists on OWNER /v1/ops, which the app already calls through appOps / appServerApprovedCall): machine get (status, handoff) → cloud.machine.get; rename → cloud.machine.rename; idle pause → cloud.machine.idle_policy.set; resize → cloud.machine.resize; snapshot create/delete/restore → cloud.snapshot.*; Set Up GitHub exec → machineAPI. All of these are unguarded today, so on the default appServer link they send a Worker vm_... id to the classic web app and fail there.
Legacy-link only (delete with .legacy)
GET/POST /api/vm list and create, DELETE /api/vm/{id}, pause, resume, attach-endpoint, cmux-remote/approve, acpmux keyed-start / person-holds, network policy GET/PUT (guarded .legacy), chat grants (guarded session.link), the classic mode of exec/fs, CloudClassicPageOps, the CloudMachineLink half of the tunnel hub, CaptureSceneRegistry's legacy link. Dead code: listMachines(), snapshots(_:).
Sign-in / auth on the classic web origin
CloudConfiguration.signInURL(CloudConfiguration.swift:268-271):authWebOrigin/handler/native-sign-inwrappinghandler/after-sign-in→<scheme>://auth-callback. Served by classicweb/app/handler/{native-sign-in,after-sign-in}. cmux-platform has no equivalent: its/oauth/authorize|token|register|revokeare MCP OAuth, and/v1/auth/challenge|token+/.well-known/jwks.jsonare install-key auth, not a hosted Stack sign-in page.AuthConfigin CloudAuth.swift:104-108 and :178-182:magicLinkCallbackURL = authWebOrigin/auth/callbackandapiBaseURL = configuration.apiBaseURL. In CmuxAuthRuntime thatapiBaseURLis the cmux web API forGET/POST/PATCH/DELETE /api/teams[...]and invitations (CmuxTeamsAPI.swift; used by CloudAuth+Teams.swift: team list/create/detail/invite, the Accounts page), plus/api/account(AccountDeletionClient),/api/auth/email-verificationand/api/billing/recover(EmailVerificationRecoveryClient),/api/device-tokens(PushRegistrationService). cmux-platform serves none of/api/teams,/api/accountor/api/device-tokens.
Other things on CloudConfiguration.apiBaseURL / authWebOrigin besides /api/vm
If DEV's apiBaseURL moves off cmux-staging.vercel.app, all of these break or change too:
- Sign-in pages and magic-link callback (above), and the teams/account APIs through
AuthConfig.apiBaseURL. - Accounts / CodeRouter: AccountsService.swift:90
CodeRouterClient(baseURL: apiBaseURL)→/api/coderouter/accounts,/api/coderouter/claude-upstream[/{id}],/api/coderouter/vm-usage/team(AppControl+Accounts.swift:30-58, CodeRouterClient.swift:66-92). cmux-platform has only an internalCoderouterEdge.ts, no owner route. - Billing through the same client:
POST /api/billing/checkout(AppControl+Billing.swift:34, PaidPlanUpgrade.swift:70),GET /api/billing/plan(AppControl+Billing.swift:57), and theapiBaseURL/dashboard/billingportal page (AppControl+Billing.swift:47-48). OWNER has/v1/checkout/sessionsand/v1/billing/summary, andcloud.plan.getas an op, but not the same contract. - Sandboxed runs
/api/runs(above). CloudMoveAsks.swift:234usesapiBaseURLas the scope key for stored "always allow" move grants (a new origin resets them).- Diagnostics only: AppControl+Cloud.swift:24 and CloudHandlers+Account.swift:511 report it as
backend. - Not on
apiBaseURL, for clarity: GitHub App status (CloudGitHubApp,/v1/integrations/github/status) usesownerAPIBaseURL(FeedService.apiBaseURL), but the Set Up GitHub exec itself goes through the classiccloud.api(row above). Feed, Hands, HomeCloudLease, ModelRouterLease, FreeTierAttest, HostedChief, host replica, credential relay and the app link all useownerAPIBaseURL.
Testing
Through the fleet fast checks at the head (cmux-ci test):
cargo test -p cmux-cloud-config: 33 passed,clippy --all-targets -D warningsclean, and the crate's panic ratchet is clean (no string slicing, no lossy casts).cargo test --test cloud_configincmux-layout-reducer-ffi: 5 passed;cargo test -p cmux-tui --lib cli::vm: 39 passed.cargo test -p cmux-vminworkers/cmux-vm/cli, including the newthe_request_is_the_one_the_app_asks_for: passed.python3 tests/test_classic_cloud_routes.py(the classic-route guard from #35040): passes. The two origins the config keeps (https://cmux.comfor production sign-in and teams,cmux-staging.vercel.appfor development sign-in and teams) moved fromCloudConfiguration.swiftand are allowlisted forconfig.rsaskeep:; step B deletes the Swift copies and theirdebt:entries. Neither is a Cloud API base for DEV or NEXT DEV any more.python3 tests/test_app_crate_licenses.py,scripts/cmux-next/tests/app-ffi-release-paths.test.sh: pass.
Observed / Not run
Observed: the runs above. Not run here: the cmux binary's cli::vm tests (CI), any Swift build (no Swift changes in step A). The app FFI pin check reports this PR's FFI sources as newer than the pin, as every step A does; step B re-pins.
Follow-ups
- #35135: grapheme and URL-validity parity with the Swift rules (review F2-F4), fixed in step B or before.
Changelog
none
risk: normal (pure rules and tests; no app behaviour change until step B).
🤖 Generated with Claude Code
Agent session: agent session