Skip to content

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) - #34982

Merged
teamleaderleo merged 7 commits into
feat-cmux-nextfrom
lane/cloud-config-rust
Nov 4, 2026
Merged

teamleaderleo merged 7 commits into
feat-cmux-nextfrom
lane/cloud-config-rust

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

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 of CloudConfiguration (+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, and ActiveTeam'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 shared CmuxAuthRuntime package, 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 (adds baseUrl/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) triggers cmux-vm.yml, whose push run redeploys the vm-staging Worker. The Worker bundle is src/index.ts (wrangler main) and its TypeScript imports; nothing in src/, wrangler.jsonc, package.json or scripts/deploy.sh refers to cli/, and no file under workers/cmux-vm/ outside cli/ 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 new cmux-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.toml changes 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)

  1. This PR (step A): the Rust rules, the FFI ops, the CLI sharing. No app behaviour change.
  2. Step B (next, after this lands and the app FFI release publishes): pin CCmuxAppFFI, make CloudConfiguration, the Auth helpers and AppControl+VMAgentKey call the ops, delete the Swift copies and their tests.
  3. 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/api Worker; 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 through POST /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 through platformPreviewAPIBaseURL() (Debug + non-prod + appServer only; nil in Release/prod)
    and artifactsAPIBaseURL().
  • 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's vm_... 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.ts https-proxies, public-egress; MeshMaintenance.ts meshes/devices) but exposes no owner route.
  • The ssh-routes WireGuard hub (POST/DELETE /api/vm/tunnel, used by cloud enroll-machine and startHubForSSHRoutes): 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's vm_... ids.
  • /api/runs (Sandboxed send, cmux run --sandboxed): /v1/handoffs is 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-in wrapping handler/after-sign-in → <scheme>://auth-callback. Served by classic web/app/handler/{native-sign-in,after-sign-in}. cmux-platform has no equivalent: its /oauth/authorize|token|register|revoke are MCP OAuth, and /v1/auth/challenge|token + /.well-known/jwks.json are install-key auth, not a hosted Stack sign-in page.
  • AuthConfig in CloudAuth.swift:104-108 and :178-182: magicLinkCallbackURL = authWebOrigin/auth/callback and apiBaseURL = configuration.apiBaseURL. In CmuxAuthRuntime that apiBaseURL is the cmux web API for GET/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-verification and /api/billing/recover (EmailVerificationRecoveryClient), /api/device-tokens (PushRegistrationService). cmux-platform serves none of /api/teams, /api/account or /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 internal CoderouterEdge.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 the apiBaseURL/dashboard/billing portal page (AppControl+Billing.swift:47-48). OWNER has /v1/checkout/sessions and /v1/billing/summary, and cloud.plan.get as an op, but not the same contract.
  • Sandboxed runs /api/runs (above).
  • CloudMoveAsks.swift:234 uses apiBaseURL as 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) uses ownerAPIBaseURL (FeedService.apiBaseURL), but the Set Up GitHub exec itself goes through the classic cloud.api (row above). Feed, Hands, HomeCloudLease, ModelRouterLease, FreeTierAttest, HostedChief, host replica, credential relay and the app link all use ownerAPIBaseURL.

Testing

Through the fleet fast checks at the head (cmux-ci test):

  • cargo test -p cmux-cloud-config: 33 passed, clippy --all-targets -D warnings clean, and the crate's panic ratchet is clean (no string slicing, no lossy casts).
  • cargo test --test cloud_config in cmux-layout-reducer-ffi: 5 passed; cargo test -p cmux-tui --lib cli::vm: 39 passed.
  • cargo test -p cmux-vm in workers/cmux-vm/cli, including the new the_request_is_the_one_the_app_asks_for: passed.
  • python3 tests/test_classic_cloud_routes.py (the classic-route guard from ci: guard NEXT code against new classic Cloud route calls #35040): passes. The two origins the config keeps (https://cmux.com for production sign-in and teams, cmux-staging.vercel.app for development sign-in and teams) moved from CloudConfiguration.swift and are allowlisted for config.rs as keep:; step B deletes the Swift copies and their debt: 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

Changelog

none

risk: normal (pure rules and tests; no app behaviour change until step B).

🤖 Generated with Claude Code

Agent session: agent session

…n Rust, over the app FFI and in the cmux vm CLI (step A)

The rules behind CloudConfiguration (the backend pick, the API origins, the
Stack project, the token store's names, the VM API), the Auth folder's
non-Keychain logic (DogfoodCredentials' account pick, the auth launch keys,
ActiveTeam's refusal and managed-team pick) and AppControl+VMAgentKey (the
agent-key request, its URL per backend and the answer's refusal codes) move
into a new pure crate, cmux-cloud-config, with the Swift tests ported.
cmux-layout-reducer-ffi's JSON channel serves them as cloud_config_*,
cloud_auth_* and cloud_vm_agent_key_*.

The cmux vm CLI links the same rules: cmux-vm's token create builds its
body with agent_key::body, and cmux-tui's app-mint exit codes read
agent_key::outcome.

Step A of the two-step FFI move: step B re-pins the app FFI, makes the
Swift side call these ops and deletes the Swift copies and their tests.
No app behavior change here.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-86478c59962584e11925
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review b619377: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr34982 -f ref=lane/cloud-config-rust -f platform=macos -f backend_mode=local

web and size checks: green

Shipped in:

@github-actions github-actions Bot added risk:high Risk tier (docs/ci/risk-tiers.md) and removed risk:normal Risk tier (docs/ci/risk-tiers.md) labels Nov 4, 2026
…T Cloud's Worker as their Cloud API, not the classic staging web app

Configuration gains cloud_api_base_url, the base the app's machine client
(CloudAPIClient) uses: the development owner API Worker for every
development build (production keeps cmux.com; a debug build that names an
origin keeps it). api_base_url stays the web app for sign-in, teams,
account, billing, CodeRouter and Sandboxed runs, which have no
cmux-platform route yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-86478c59962584e11925
@blacksmith-sh

blacksmith-sh Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Found 6 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmpwh3f1qec/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpxy9ia3f0/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…ice put in its text

Takes the answer's own key and anything shaped like cmuxvm_sk_... out of
the refusal message (review F1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-86478c59962584e11925
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6.1-sol high): SHIP at 002c49a
Visual verdict: unchanged by this diff
The redaction covers the response key and all cmuxvm_sk_-shaped values before truncation. Boundary slicing is safe, the loop advances, and all refusal paths remain sanitized.

Findings: none.

Follow-ups: none (F2-F4 from the first review at 207f611 are filed separately).

…, the env-file quotes or the URL split (panic ratchet)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-86478c59962584e11925
teamleaderleo and others added 2 commits November 4, 2026 08:39
…he two origins moved from Swift (keep, step B deletes the Swift copies)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-86478c59962584e11925
@cursor

cursor Bot commented Nov 4, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6.1-sol high): SHIP at a836082 (carried from the SHIP at 660b640)
Visual verdict: unchanged by this diff
Delta since 660b640 is mechanical: a merge of feat-cmux-next (brings the classic-route guard, #35040), exact_integer turning a whole f64 into u64 by printing it instead of an as cast (panic ratchet: no lossy casts in a new crate), and two keep: allowlist entries for the two origins config.rs moved from CloudConfiguration.swift. Fast checks at a836082: cmux-cloud-config 33 passed, clippy clean, panic ratchet clean, FFI cloud_config 5 passed, cli::vm 39 passed, tests/test_classic_cloud_routes.py OK.

Findings: none.

Follow-ups: #35135 (F2-F4 from the review at 207f611).

Finding outcome: board-review-207f611e/F1 fixed 002c49a

…config-rust

# Conflicts:
#	cmux-tui/Cargo.lock
#	cmux-tui/Cargo.toml
@cursor

cursor Bot commented Nov 4, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6.1-sol high): SHIP at b619377 (carried from the SHIP at 660b640, via a836082)
Visual verdict: unchanged by this diff
The only change since a836082 is a merge of feat-cmux-next; its one conflict was the workspace dependency list and lockfile, where cmux-cloud-config and the base's new cmux-cloud-environment (#35133) both stay (cargo metadata --locked passes). No file of this PR changed.

Findings: none.

Follow-ups: #35135. Base red, not this PR's: tests/test_classic_cloud_routes.py fails on feat-cmux-next itself for cmux-tui/crates/cmux-cloud-environment/src/lib.rs (#35133 landed after the guard #35040), a file this PR does not touch.

@teamleaderleo
teamleaderleo merged commit 2da7f07 into feat-cmux-next Nov 4, 2026
100 of 105 checks passed
@teamleaderleo
teamleaderleo deleted the lane/cloud-config-rust branch November 4, 2026 08:51
teamleaderleo added a commit that referenced this pull request Nov 4, 2026
…te guard (#35160)

#35133 added cmux-tui/crates/cmux-cloud-environment/src/lib.rs with the
cmux.com and cmux-staging origins after #35040 landed the guard, so
tests/test_classic_cloud_routes.py is red on base. Add keep: entries
like #34982's for cmux-cloud-config.


Agent-Session: https://cmux.com/deeplink/agent?id=aid-86478c59962584e11925

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-bot[bot] <348531757+cmux-bot[bot]@users.noreply.github.com>

This branch was successfully deployed

2 inactive (outdated) deployments
ci-routing-agent — b6193777 Deployed Nov 4, 2026 by teamleaderleo via cmux-tui tree preflight #110786
artifacts — 002c49af Deployed Nov 4, 2026 by teamleaderleo via publish tree-addressed cmux-tui #110607
sentry-dsyms — 002c49af Deployed Nov 4, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #110607
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant