cmux NEXT Cloud: the Cloud page works on the classic link instead of saying it waits to reconnect

Closed
TE
7d ago
+725-8

Leo, 2026-10-29: "let's just get the cloud and its respective ui up to snuff" and "as well as the ui usability of the cloud stuff... plus having less stupid spacing on this. imo, project and cloudness at the top."

Friction

On a fresh signed-in launch on the classic Cloud link (the release and default path), the Cloud page says "Cloud is not available until cmux reconnects." and Retry does nothing. In staging capture run 41004738189 (build of cd927d88), the page stayed that way for the whole 7-minute run, and two fresh page instances failed the same way. Over the same run the CLI listed machines, opened a terminal, paused and resumed, and the sidebar listed both machines.

Root cause

The Cloud page sends every op to the Cloud app server (apps-run cmux/cloud). That server reaches its backend only through the host credential relay, and the relay registers only on the app server link path (CloudService.startCredentialProvider, guarded on linkSource == .appServer since the cx-t2rz security review, P3-1). On the classic link, cloud.auth.status and cloud.machine.list therefore always fail with relay_unavailable, which CloudPageProvider turns into cmux.protocol.transport, which the page shows as "disconnected". Retry runs the same ops, so it fails the same way every time. The CLI and the sidebar work because they use CloudService's /api/vm path. The fixture's machines also live only in /api/vm, so even a working relay would list none of them.

Fix

On the classic link, the page now reads what the CLI and the sidebar read. The relay policy doesn't change.

  • CmuxNextCloud/Page/CloudClassicPageOps.swift (new, in the Cloud package): answers cloud.auth.status, cloud.machine.list, cloud.machine.watch, and cloud.plan.get (only when the list carries a machine cap), and runs start (resume), pause, rename and delete. Delete runs only with origin user, after the native sheet, as on the app server. Every other op answers cmux.cloud.unsupported, which the page already shows as not available (snapshots, files, ports, migration, teams). That includes create: the page's create is not one of the Cloud page's confirmed ops (PageDescriptor.cloud.confirmedOps), so it never reaches the provider on either link.
  • The app's Cloud gate holds: while CloudService.unavailableReason is set (turned off by the organization, no managed team, a local-only backend), every op except cloud.auth.status is refused with cmux.cloud.unavailable and the reason, no /api/vm call goes out, and the feed lists no machine.
  • Classic errors keep the codes the page acts on: plan_required (with details.plan, so "See plans" shows), not_found (a machine that's already gone counts as done) and auth_required. Anything else is cmux.cloud.failed with the localized text.
  • Live state: while the page watches, an observation of the registry's machines (no timer) turns each change into upsert/removed events at a new revision. That covers refreshes, CLI pause/resume, and machines added or removed. It stops when the last watcher leaves.
  • CloudPageProvider takes an optional feed for those events. With a feed it doesn't also announce a mutation's answer, so each change reaches the stream once. PageFactory.cloudProvider picks the classic ops on .legacy and keeps account.appOps on .appServer (no change there).
  • CloudAppOps.swift: the App adapter (JSON in and out, refusal codes) and the backend built from CloudService and MachineRegistry.

#27834 (open, Leo lane) makes the page re-read on its own after a link refusal and hides the disconnected state during a grace period. That's complementary: on the classic link the re-reads would still never succeed without this PR.

Risk

Medium, contained to the Cloud page on the classic link. The .appServer path is unchanged. Delete keeps the native confirmation and the person-only gate. Plan usage on the classic link shows the list's machine cap and running count. It shows no snapshot allowance (0 of 0) because /api/vm names none.

Evidence

  • Red on base: CloudClassicPageRedTests (branch leo/cloud-page-legacy-link-red, base 7986c3fb plus one test file) builds the provider the app built before (account.appOps) and calls cmux.cloud.auth.status on the classic link. Red: fleet step 2379e0df8eda6402d9f6ac48 (aws-m4pro-7) failed with PageError(code: "cmux.protocol.transport", message: "not connected to cmux-tui", retryable: true), the page's "disconnected" state.
  • Green at head 6d6d998f: CloudClassicPageTests (9 tests: the app's own provider answers with no relay; list records; signed-out refusal; registry changes reach the watch stream once each and stop with the last watcher; pause/start; delete person-only and create not served; unavailable Cloud refuses every call and lists no machine; classic errors keep the page's codes; plan cap and unsupported ops), plus the existing CloudPaneTests and PageProviderTests. All 3 suites passed: fleet step 72c14ce01e551d17fe101135 (aws-m4pro-1).
  • Before: staging capture run 41004738189 (build of cd927d88). The page reads "Cloud is not available until cmux reconnects. Retry" for the whole run.
  • After: cloud-capture fixture route (HQ pr-capture-cloud.yml), state cloud-audit-ab with --video, build e11de3346d005244e468837f of this head. Pending: the first attempt found no Blacksmith macOS runner on any of the four labels within 240 s each.
  • UI review checklist: no webview or native UI code changed. The page's existing states now get real data.

Agent session: agent session

šŸ¤– Generated with Claude Code