Skip to content

Cloud: open detected ports through private live previews - #30334

Merged
teamleaderleo merged 3 commits into
feat-cmux-nextfrom
lane/cloud-live-previews
Nov 1, 2026
Merged

teamleaderleo merged 3 commits into
feat-cmux-nextfrom
lane/cloud-live-previews

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

Cloud Machine Ports discovers listeners on configured NEXT Cloud machines and offers an Open in Browser chooser. cmux cloud machine-ports returns the same private preview links, and cloud open-browser --port N --path /docs opens the authenticated preview in the app browser while preserving its sign-in bootstrap.

Leo: “yeah we have bits and pieces of this with the computer use preview but i think we can do better” (HQ #5846, item 9).

Preview discovery and ingress belong to cmux-platform. The client calls its new POST /v1/cloud/machines/{id}/previews/refresh directly with the existing person access token and team. Debug builds using the staging Cloud owner select platform staging; an explicit debug HTTPS origin can configure another development preview API. Builds without preview ingress keep their existing browser and port behavior. Classic routes and the computer-use card keep their existing behavior.

The existing port strings and catalog move to CmuxNextCloudUI, keeping the app module smaller. Design: leo-workspace design/cloud.md. Prior art read: #25284 (port/browser actions), #28279 (pending port actions). pr-overlap reported no overlapping open PR.

Observed / Not run

  • Passed: prepush static gate, XCStrings structure, localization default parity, test wiring, scoped CloudPorts localization (within existing debt), and feature policy.
  • Added behavioral coverage for the exact new API route and account/team headers, production fallback, bootstrap token/path preservation, insecure URL refusal, the native chooser, and CLI result fields. Fleet Linux app compile passed at 924af0f3f43c718748aa7f1a4cfef9a10d2392bb (job 47d9a55263d66246d01b72b9, 65 seconds). Fleet native test binary compiled successfully; CloudLivePreviewTests (7 tests) and CloudBrowserActionTests (13 tests) both passed (job c779c9df80c95f10964ae260, exit 0). The first fleet compile caught a pane-controller/model mismatch; this head fixes it.
  • Local Swift parsing is unavailable because this lane has no Swift compiler. Broad preflight also reports existing localization debt and missing CmuxDeviceHub workspace grouping at base 88080e84e81a83f29ef29495c5a716bd6b588bdd; this PR changes neither package membership nor those catalogs.
  • No capture: the real Cloud preview route needs the matching platform deployment and authenticated DEV probe. This PR alone is not item-9 delivery; the owner will read back the real preview in DEV before calling the item working.

Risk: the configured staging path now uses private HTTPS ingress; production keeps its existing path. No new timers or observers.

Agent session: agent session

Connect configured Cloud preview ingress to the existing machine-port actions and browser pane. Preserve the existing proxy route for builds without platform preview ingress.

Validation: scoped CloudPorts catalog check passes within existing base debt; XCStrings structure, default parity, test wiring and feature policy pass. Native compile and behavioral tests require the fleet; this lane has no Swift compiler.
Agent-Session: https://cmux.com/deeplink/agent?id=aid-a71eddb5268a31d81df5
Move existing Cloud port labels and their catalog out of the app module. Keep preview wiring in existing action and service adapters so the feature does not grow the app module.

Agent-Session: https://cmux.com/deeplink/agent?id=aid-a71eddb5268a31d81df5
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 924af0f: 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=pr30334 -f ref=lane/cloud-live-previews -f platform=macos -f backend_mode=local

Not verified: macOS jobs did not run: compile and app tests were skipped on 924af0f3f4; ci-status is green without them.

CI passes on 924af0f3f4 (run 41281900177 attempt 1).
macOS jobs did not run: compile and app tests were skipped: Fast static checks skipped (the macos job needs both to succeed). Re-run CI once they pass, before merging.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

Default changes

This pull request changes 36 defaults at 924af0f3f43c (what everyone gets without configuring anything):

  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:browserFailed: "2338807e162d3e797342058016cbcd7e88593957e1fc2c6fa0d10350c19f759c" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:classicLink: "69fbf66d0d52bf96be3cb4f22b92eaa621e51883f27ded0026de8b0fa1a67004" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:failed: "3f8e296007e662ca5618539f15005b9e3390a26f702b53b937bfd16073e46b2b" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:invalidPort: "e66887d9666e3c4d4bce4e3a206108338a6037cdc8e95157d9f4a33b13876013" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:limit: "c3252bd979b7a5dc5d071cb88db91b94932b8362075c6de865f4141c3881f69d" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:localhostOff: "86bf512479980e4a5e0d5ee71f365df2aa46b4859a3ab53e400b9f0000a37908" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:noForward: "cf5d07ada7be3159b4f5bac80b24909074d72698a8c075f8705d1e921f5796e2" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:noLink: "b08b40bae60752196d8a794ecb86d1456bb2c00c013c9a3cea190639ee2d43e9" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:noWindow: "6341f92e8ed2b5f2e3807c70484de717880ecc01319dccb88c18d676704b7298" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:none: "8ea62d6501cc322ddcb19e77a0438c4150100b36b5b3f25d077dc7627099ac17" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:pathInvalid: "f3a8be1a67baa5bab87dfc84ad887899e726d0e3685a446a983ec034c4b3f9ae" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:routeRefused: "a7c6a3baa2d41c974ec024d9c3274f43bff9dd9ccb6b623605405d5d2b7da04b" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:title: "6b0ef50f4fc996727f000f2208f743679b0a099d7018da74ba9eb1ac8790dd96" -> (none)
  • tunable-source.CmuxNextApp/Cloud/CloudPortStrings.swift:updateMachine: "c7d384e4db11d0e056a1668d1fe76eab37e7cb1714a879c3690d2dd90ebdaf5b" -> (none)
  • tunable-source.CmuxNextApp/Handlers/CloudHandlers+Browser.swift:livePreviewPorts: (none) -> "bda2e799d6c28427b12069ff04260a2c3e4de4f5f8828b8d8ed444402b507e87"
  • tunable-source.CmuxNextApp/Handlers/CloudHandlers+Browser.swift:openBrowser: "db8d3055e6efb19a78719dc66fb0ddd8e82859cb15ead66548bf8dd0c9b84e86" -> "ec2d82f07fa0cd52417d093372174466ee063bdb83956e6b8e31a66e77994239"
  • tunable-source.CmuxNextApp/Handlers/CloudHandlers+Browser.swift:openPreview: (none) -> "538598f74ba80b26ef30346ebac20077553dad753977f290a76374b925b54138"
  • tunable-source.CmuxNextApp/Handlers/CloudHandlers+Browser.swift:previewAnswer: (none) -> "2243cc9c11736d19f56beff300402e247cee902d9f0ad08a630af3f37899bda0"
  • tunable-source.CmuxNextApp/Handlers/CloudHandlers+Browser.swift:previewPicker: (none) -> "29441cfb9a1ae501837a70ea24844047ee06163fc3b430eabb5ee227c1178a78"
  • tunable-source.CmuxNextApp/Handlers/CloudHandlers+Browser.swift:refreshPreviews: (none) -> "01466d0be659d8da1f52209a1b0a536b310f52248a7b2265cae2e2e5b1dda703"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:browserFailed: (none) -> "2338807e162d3e797342058016cbcd7e88593957e1fc2c6fa0d10350c19f759c"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:classicLink: (none) -> "69fbf66d0d52bf96be3cb4f22b92eaa621e51883f27ded0026de8b0fa1a67004"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:failed: (none) -> "3f8e296007e662ca5618539f15005b9e3390a26f702b53b937bfd16073e46b2b"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:invalidPort: (none) -> "e66887d9666e3c4d4bce4e3a206108338a6037cdc8e95157d9f4a33b13876013"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:limit: (none) -> "c3252bd979b7a5dc5d071cb88db91b94932b8362075c6de865f4141c3881f69d"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:localhostOff: (none) -> "86bf512479980e4a5e0d5ee71f365df2aa46b4859a3ab53e400b9f0000a37908"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:noForward: (none) -> "cf5d07ada7be3159b4f5bac80b24909074d72698a8c075f8705d1e921f5796e2"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:noLink: (none) -> "b08b40bae60752196d8a794ecb86d1456bb2c00c013c9a3cea190639ee2d43e9"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:noPreviews: (none) -> "578b2128a4d6e59e30199e7572fe245e8b04678eadabc8ebd7f9a171eb74cfb6"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:noWindow: (none) -> "6341f92e8ed2b5f2e3807c70484de717880ecc01319dccb88c18d676704b7298"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:none: (none) -> "8ea62d6501cc322ddcb19e77a0438c4150100b36b5b3f25d077dc7627099ac17"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:openPreview: (none) -> "bb4a5c8969e89d490fb875ad98e811243cec0b15e2b05958f2334dfaf2767853"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:pathInvalid: (none) -> "f3a8be1a67baa5bab87dfc84ad887899e726d0e3685a446a983ec034c4b3f9ae"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:routeRefused: (none) -> "a7c6a3baa2d41c974ec024d9c3274f43bff9dd9ccb6b623605405d5d2b7da04b"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:title: (none) -> "6b0ef50f4fc996727f000f2208f743679b0a099d7018da74ba9eb1ac8790dd96"
  • tunable-source.CmuxNextCloudUI/CloudPortStrings.swift:updateMachine: (none) -> "c7d384e4db11d0e056a1668d1fe76eab37e7cb1714a879c3690d2dd90ebdaf5b"

Signed off on this commit by review (claude-opus-5-5 medium, cloud-reviewer).

Merge receipt: merged as a4c17758a0bf with these defaults changed, signed off by review (claude-opus-5-5 medium, cloud-reviewer).

Regenerate the snapshot with python3 scripts/ci/defaults_guard.py snapshot.

Shipped in:

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 12 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="release-notes/next/highlights/cloud-live-previews.md">

<violation number="1" location="release-notes/next/highlights/cloud-live-previews.md:4">
P2: This note does not tell users where to find the preview chooser, so the advertised feature is difficult to discover from What's New. Say that `Cloud Machine Ports` is available in the Cloud machine's action palette, and name the CLI command if it is part of the entry.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudMachinePreviews.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudMachinePreviews.swift:53">
P2: Encode `id` as one path segment before constructing this request. A machine ID containing `/`, `?`, `#`, or percent-encoded separators can otherwise address a different route or inject query/fragment data.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudActionDescriptions.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudActionDescriptions.swift:53">
P2: `machine-ports` only returns `previews`, `listening`, and `unavailable` when preview ingress is available. Qualify this sentence so agents do not expect those fields from builds using the existing fallback.</violation>

<violation number="2" location="Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudActionDescriptions.swift:54">
P2: This sentence incorrectly says `resume-machine` connects the classic-link machine. The ports handler still requires an established daemon connection, while resume may only start the VM and return before attachment; document that the machine must be connected and that paused machines should be resumed first, then wait for the link.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudPortActionCatalog.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudPortActionCatalog.swift:13">
P2: `cloudOpenBrowser` does not always use a private preview URL, and its fallback can still return `pending: true`. Describe the preview as conditional on ingress availability and document the fallback result.</violation>
</file>

<file name="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/CloudHandlers+Browser.swift">

<violation number="1" location="Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/CloudHandlers+Browser.swift:341">
P2: `openPreview` bypasses the Chromium availability guard used by `openBrowser`. When a user selects a still-valid preview while CEF is unavailable, this attempts a pinned CEF tab instead of returning the supported Chromium-unavailable refusal; add the same guard here.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

title: Open Cloud web previews in the browser
category: improved

Cloud Machine Ports discovers your running servers and lets you open a private preview in the app's browser. The same preview links are available from the CLI.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This note does not tell users where to find the preview chooser, so the advertised feature is difficult to discover from What's New. Say that Cloud Machine Ports is available in the Cloud machine's action palette, and name the CLI command if it is part of the entry.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At release-notes/next/highlights/cloud-live-previews.md, line 4:

<comment>This note does not tell users where to find the preview chooser, so the advertised feature is difficult to discover from What's New. Say that `Cloud Machine Ports` is available in the Cloud machine's action palette, and name the CLI command if it is part of the entry.</comment>

<file context>
@@ -0,0 +1,4 @@
+title: Open Cloud web previews in the browser
+category: improved
+
+Cloud Machine Ports discovers your running servers and lets you open a private preview in the app's browser. The same preview links are available from the CLI.
</file context>
Suggested change
Cloud Machine Ports discovers your running servers and lets you open a private preview in the app's browser. The same preview links are available from the CLI.
From a Cloud machine's action palette, choose Cloud Machine Ports to find running servers and open a private preview in the app's browser. Run `cmux cloud open-browser --target machine:<id> --port <port>` from the CLI.

guard !sendsStackSession else {
throw CloudAPIError.http(status: 501, code: "machine.previews_unsupported", message: nil)
}
return try await send("POST", "\(machinePath)/\(id)/previews/refresh", timeout: .seconds(30), as: CloudMachinePreviews.self)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Encode id as one path segment before constructing this request. A machine ID containing /, ?, #, or percent-encoded separators can otherwise address a different route or inject query/fragment data.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextCloud/API/CloudMachinePreviews.swift, line 53:

<comment>Encode `id` as one path segment before constructing this request. A machine ID containing `/`, `?`, `#`, or percent-encoded separators can otherwise address a different route or inject query/fragment data.</comment>

<file context>
@@ -0,0 +1,55 @@
+        guard !sendsStackSession else {
+            throw CloudAPIError.http(status: 501, code: "machine.previews_unsupported", message: nil)
+        }
+        return try await send("POST", "\(machinePath)/\(id)/previews/refresh", timeout: .seconds(30), as: CloudMachinePreviews.self)
+    }
+}
</file context>
Suggested change
return try await send("POST", "\(machinePath)/\(id)/previews/refresh", timeout: .seconds(30), as: CloudMachinePreviews.self)
return try await send("POST", "\(machinePath)/\(pathSegment(id))/previews/refresh", timeout: .seconds(30), as: CloudMachinePreviews.self)

Comment on lines +54 to +55
ports; its menu opens a detected port in the app browser. On the classic link, connect first \
with `cmux cloud resume-machine --target machine:<id>`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This sentence incorrectly says resume-machine connects the classic-link machine. The ports handler still requires an established daemon connection, while resume may only start the VM and return before attachment; document that the machine must be connected and that paused machines should be resumed first, then wait for the link.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudActionDescriptions.swift, line 54:

<comment>This sentence incorrectly says `resume-machine` connects the classic-link machine. The ports handler still requires an established daemon connection, while resume may only start the VM and return before attachment; document that the machine must be connected and that paused machines should be resumed first, then wait for the link.</comment>

<file context>
@@ -50,8 +50,9 @@ nonisolated enum CloudActionDescriptions {
-            ports (comma-separated). Needs the machine connected: resume a paused one first with \
-            `cmux cloud resume-machine --target machine:<id>`.
+            ports (comma-separated). New Cloud also returns private previews, listening and unavailable \
+            ports; its menu opens a detected port in the app browser. On the classic link, connect first \
+            with `cmux cloud resume-machine --target machine:<id>`.
             """,
</file context>
Suggested change
ports; its menu opens a detected port in the app browser. On the classic link, connect first \
with `cmux cloud resume-machine --target machine:<id>`.
ports; its menu opens a detected port in the app browser. On the classic link, the machine must be connected; if paused, run \
`cmux cloud resume-machine --target machine:<id>` and wait for it to connect.

id: "cloudOpenBrowser",
title: String(localized: "action.cloudOpenBrowser", defaultValue: "Open Machine Port in Browser…", table: "CloudPortActions", bundle: .module),
description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id; on the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's. A tab not shown within 15 s answers pending: true.",
description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id. New Cloud uses its private preview URL. On the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: cloudOpenBrowser does not always use a private preview URL, and its fallback can still return pending: true. Describe the preview as conditional on ingress availability and document the fallback result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudPortActionCatalog.swift, line 13:

<comment>`cloudOpenBrowser` does not always use a private preview URL, and its fallback can still return `pending: true`. Describe the preview as conditional on ingress availability and document the fallback result.</comment>

<file context>
@@ -10,7 +10,7 @@ nonisolated enum CloudPortActionCatalog: ActionCatalogGroup {
                 id: "cloudOpenBrowser",
                 title: String(localized: "action.cloudOpenBrowser", defaultValue: "Open Machine Port in Browser…", table: "CloudPortActions", bundle: .module),
-                description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id; on the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's. A tab not shown within 15 s answers pending: true.",
+                description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id. New Cloud uses its private preview URL. On the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's.",
                 keywords: ["vm", "cloud", "browser", "port", "localhost", "preview", "web"], category: .cloud, symbol: "safari",
                 surfaces: [.palette, .contextMenu], arguments: [CatalogArgument.portInt, CatalogArgument.optionalPathString],
</file context>
Suggested change
description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id. New Cloud uses its private preview URL. On the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's.",
description: "Opens --port N (and --path, default /) of the machine (--target machine:<id>) in a Chromium tab and returns url and surface_id. New Cloud uses its private preview URL when preview ingress is available; otherwise its app-link browser route may return pending: true if the tab is not shown. On the classic Cloud link the tab opens in the machine's workspace (workspace_id) and its localhost is the machine's.",

/// A platform preview is an HTTPS website, independent of the Mac's loopback proxy routes.
static func openPreview(_ preview: CloudMachinePreview, session: CloudMachineSession, path: String = "/", viewChange: Bool,
_ context: AppActionContext) async throws -> [String: ActionResultValue] {
let names = CloudPortNames(session, port: preview.port)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: openPreview bypasses the Chromium availability guard used by openBrowser. When a user selects a still-valid preview while CEF is unavailable, this attempts a pinned CEF tab instead of returning the supported Chromium-unavailable refusal; add the same guard here.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextApp/Handlers/CloudHandlers+Browser.swift, line 341:

<comment>`openPreview` bypasses the Chromium availability guard used by `openBrowser`. When a user selects a still-valid preview while CEF is unavailable, this attempts a pinned CEF tab instead of returning the supported Chromium-unavailable refusal; add the same guard here.</comment>

<file context>
@@ -264,3 +273,86 @@ extension CloudHandlers {
+    /// A platform preview is an HTTPS website, independent of the Mac's loopback proxy routes.
+    static func openPreview(_ preview: CloudMachinePreview, session: CloudMachineSession, path: String = "/", viewChange: Bool,
+                            _ context: AppActionContext) async throws -> [String: ActionResultValue] {
+        let names = CloudPortNames(session, port: preview.port)
+        guard let pane = context.services.windows.active?.focusedPane else { throw ActionFailure(message: CloudPortStrings.noWindow(names)) }
+        let url = try await wrapped(names) { try preview.browserURL(path: path) }
</file context>
Suggested change
let names = CloudPortNames(session, port: preview.port)
let names = CloudPortNames(session, port: preview.port)
if let reason = context.services.cache.browserTabs.cefUnavailable() {
throw ActionFailure(message: CloudPortStrings.browserFailed(names, detail: BrowserTabService.message(reason)))
}

Comment on lines +53 to +54
ports (comma-separated). New Cloud also returns private previews, listening and unavailable \
ports; its menu opens a detected port in the app browser. On the classic link, connect first \

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: machine-ports only returns previews, listening, and unavailable when preview ingress is available. Qualify this sentence so agents do not expect those fields from builds using the existing fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxNext/Sources/CmuxNextActions/Catalog/CloudActionDescriptions.swift, line 53:

<comment>`machine-ports` only returns `previews`, `listening`, and `unavailable` when preview ingress is available. Qualify this sentence so agents do not expect those fields from builds using the existing fallback.</comment>

<file context>
@@ -50,8 +50,9 @@ nonisolated enum CloudActionDescriptions {
             Lists the TCP ports listening on the machine (--target machine:<id>) and returns machine_id and \
-            ports (comma-separated). Needs the machine connected: resume a paused one first with \
-            `cmux cloud resume-machine --target machine:<id>`.
+            ports (comma-separated). New Cloud also returns private previews, listening and unavailable \
+            ports; its menu opens a detected port in the app browser. On the classic link, connect first \
+            with `cmux cloud resume-machine --target machine:<id>`.
</file context>
Suggested change
ports (comma-separated). New Cloud also returns private previews, listening and unavailable \
ports; its menu opens a detected port in the app browser. On the classic link, connect first \
ports (comma-separated). When preview ingress is available, New Cloud also returns private previews, listening and unavailable \
ports; its menu opens a detected port in the app browser. On the classic link, connect first \

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (subagent claude-opus-5-5 medium, for claude-sr2-coord): SHIP at e84d0fa

Read the full diff plus CloudConfiguration.resolve/ownerAPIBaseURL, CloudAPIClient.send/workerMachines, CloudService.machineAPI and the existing openBrowser/browserPath at this head.

  • Staging origin / production. platformPreviewAPIBaseURL returns nil unless linkSource == .appServer && isDebugBuild && !isProductionAuth. resolve forces isProductionAuth = true for every non-debug build, so a release build can't reach the staging platform or the env override. Production keeps the existing cloud.browser.open and daemon port list unchanged. The test covers each gate, but it flips the fields on a hand-mutated config. Good enough.
  • Account/team transport. previewClient reuses the Worker client's token and team closures in Worker mode (sendsStackSession = false). So only Authorization: Bearer <access> and x-cmux-team go out, with no Stack refresh token, and the team is the same cmux team as the machine API. A legacy (/api/vm) client is refused with a 501 before any request. A token only leaves for another origin through the debug-only CMUX_NEXT_PLATFORM_PREVIEW_API_URL, and that URL must be bare https with no userinfo, path, query or fragment.
  • return_to. browserURL requires https, a host and no userinfo on the server's bootstrap URL. It replaces any existing return_to and keeps the token. I checked that URLComponents percent-encodes ?, & and # inside the value, so a path can't inject query items. The path has already passed browserPath (leading /, no whitespace or control characters, ≤2048). The client does not reject //host or /\host, so open-redirect safety rests on the platform's relative-path check, as the comment says.
  • Browser routing. A preview opens as a pinned CEF tab in the active window's focused pane, with the URL shown in the result. The picker labels show only the origin, not the bootstrap token (tested). An expired selection re-refreshes through openBrowser.

No blocking findings.

Follow-ups:

  1. [follow-up] Reject //… and /\… paths on the client before they become return_to, so the client doesn't rely only on the server to stop an open redirect. Optionally require that preview.url's host equals preview.origin's host (or a platform-owned suffix), so a misconfigured platform can't steer the tab to an arbitrary https site.
  2. [follow-up] Debug staging builds now send cloud machine-ports and open-browser only to the platform route. If /previews/refresh isn't deployed or returns an error, those builds lose the daemon port list and cloud.browser.open, with no fallback. Falling back on 404/501 would keep staging dogfood working until the platform side lands.
  3. [follow-up] The default staging origin is a *.workers.dev hostname hard-coded in the client. Prefer a cmux-owned hostname (like cloud-api-staging.cmux.dev) so the endpoint can move without a client release.
  4. [follow-up] The CLI/MCP answer (text and previews[].url) carries the short-lived bootstrap token, so it lands in agent transcripts and on the clipboard. That is the stated design, but say so in the action description, and keep the token's lifetime and single use enforced server-side.
  5. [follow-up] The tests cover the API client, the config gate, URL building, the picker spec and answer formatting. No test drives the palette.cloud.ports / cloudOpenBrowser handler branch (previewAPI != nil → openPreview) or the expired-preview path. A handler-level test with a stub preview API would cover the routing switch itself.
  6. [follow-up] The new logic is small Swift glue (URL rewriting, inventory mapping). That's fine for now. If preview selection or expiry policy grows, it belongs on the Rust/web side.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (claude-opus-5-5 medium, cloud-reviewer): SHIP at 924af0f

Delta review over the prior SHIP at e84d0fa. The delta is the single commit 924af0f ("Fix preview browser target pane type"). Its parent is e84d0fa, and it changes one line in CloudHandlers+Browser.swift.

  • openPreview used to pass windows.active?.focusedPane (a PaneController) into browserTabs.open(_:in:url:activate:). It now passes focusedPane?.pane, the pane model. This is the same idiom TabLifecycle.swift (new-tab target) and StashAppHost.swift use for the same API, so the type now matches.
  • Behaviour is unchanged. When there is no active window or focused pane, it still throws the same noWindow error. The URL building, token handling and pinned-CEF routing are untouched.
  • No new logic, secrets, logging or persistence. Only a compile fix. build / release-compile passes at this head.

No blocking findings.

Follow-ups: none new. The prior review's follow-ups 1-6 still stand.

@blacksmith-sh

blacksmith-sh Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Found 14 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp_e_8cdkx/summary-missing.json (live):/summary-missing.json (live): View Logs
/tmp/tmprvuxh0gm/summary-fail.json (live):/summary-fail.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
convention violations found (ERROR lines above)./
convention violations found (ERROR lines above).
View Logs
nightly must build and hand off the macOS 26 helper and Xcode 26.5 app before publishin
g/
nightly must build and hand off the macOS 26 helper and Xcode 26.5 app before publishin
g
View Logs
test_cmux_next_follows_the_committed_target_graph (main.FocusedTestMap.test_cmux_ne
xt_follows_the_committed_target_graph)/
test_cmux_next_follows_the_committed_target_graph (main.FocusedTestMap.test_cmux_ne
xt_follows_the_committed_target_graph)
View Logs
test_every_part_of_bun_run_check_runs_in_the_warn_branch (main.WebviewsWarnSplitCov
ersCheck.test_every_part_of_bun_run_check_runs_in_the_warn_branch)/
test_every_part_of_bun_run_check_runs_in_the_warn_branch (main.WebviewsWarnSplitCov
ersCheck.test_every_part_of_bun_run_check_runs_in_the_warn_branch)
View Logs

...and 4 more test failures. View all on Blacksmith

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

lawrencecchen added a commit that referenced this pull request Nov 1, 2026
… (highlights, capabilities, step coverage, arm64 recorder) (#30624)

* base red: tooling-tests, register test_ci_blacksmith_linux_sizes.py as manual and the unregistered tests

tooling-tests failed on every feat-cmux-next pull request at
"validate_test_execution_registry.py --pr-lanes feat-cmux-next":
  tests/test_ci_blacksmith_linux_sizes.py: linux-guard lane is not run by any workflow
#30360 (d57f2fc) registered the test as linux-guard but added no
ci-guards.yml step that runs it. CLAUDE.md forbids new CI tests (Lawrence
2026-10-23), so it becomes a manual entry with how to run it.

The registry also warned about six tests with no entry. test_nightly_dedicated_mini.py
is already run by ci-guards.yml, so it takes linux-guard; the other five are run
by no workflow and enter as manual, each with its own run command.

Red at 03423de:
  python3 scripts/ci/validate_test_execution_registry.py --pr-lanes feat-cmux-next --base-sha HEAD^1   exit 1
Green here: same command, exit 0 (594 tests, manual=31).

* base red: Checks job structure, the preflight tests read the preflight step by id

tests/test_cmux_next_checks_workflow.py read the preflight script as the last
step of cmux-tui-artifacts.yml's tree-preflight job. #30221 (5bdd4c9) added
"Record tree owner observation" and an upload step after it, so 14 tests failed
with KeyError: 'run' on every pull request. They now take the step by its id
(push-head-preflight's `head`, tree-preflight's `inspect`); the assertions do
not change.

Red at 03423de: python3 tests/test_cmux_next_checks_workflow.py   FAILED (errors=14)
Green here: same command, 165 tests OK.

* base red: Tier routing, an agent pane on a dev server URL loads its own document

test_no_suite_runs_page_javascript_on_a_built_bundle flagged
AgentPageHibernationTests.swift after #30312 (b16b20c) made it call
waitUntilLoaded. That suite's agent pane loads AgentPaneSource.devURLVariable,
a loopback port nobody serves (Debug test builds honor it), not the built
bundle, so it reads no bundle content. The check now counts
AgentPaneSource.devURLVariable as a suite's own document, beside
loadHTMLString and the fixture roots.

Red at 03423de: python3 tests/test_cmux_next_route.py   FAILED (failures=1)
Green here: same command, 112 tests OK.

* base red: daemon capabilities export lists agent-session-start-no-cwd-v1

#30363 (efd30f5) added the optional capability agent-session-start-no-cwd-v1
to DaemonEndpoint.swift without the export, which reddened "Daemon capabilities
export is fresh" and the daemon-capabilities-fresh.test.sh script test.
Regenerated with the check's own renderer (--write), as base-autofix does.

Red at 03423de: python3 scripts/cmux-next/check-daemon-capabilities-fresh.py   exit 1
Green here: same command, exit 0; bash scripts/cmux-next/tests/daemon-capabilities-fresh.test.sh ok.

* base red: String tables, store.updates.available has English's plural forms in every language

#30288 (e6bfd26) made store.updates.available a plural with `one` and
`other`, but ja, km, ko, th, vi, zh-Hans and zh-Hant got only `other`, and
check-l10n.sh requires English's forms in every language. Their `one` form is
their `other` text, as home.stopAll.couldNotStop does.

Also adds the 12 languages #27883 (16b7cc2) left out of
action.sidebar.compactList.toggle (style debt CI only warns about), marked
needs_review.

Red at 03423de: scripts/cmux-next/check-l10n.sh .   7 errors (19 without L10N_STYLE_WARN_MAX)
Green here: same command, 0 errors.

* base red: release-notary, three highlights link their pull requests

scripts/whats-new/test_digest.py needs a try-it action, docs or a pr link on
every highlight. chat-title-plain (#29920), cloud-live-previews (#30334) and
working-harness-tab-icons (#27905) had none; each now links its pull request,
as #29939 did for priority-view-drag.

Red at 03423de: python3 scripts/whats-new/test_digest.py   FAILED (failures=1)
Green here: same command, OK.

* base red: release-notary, the static capability check skips agent-session-start-no-cwd-v1

#30363 (efd30f5) added agent-session-start-no-cwd-v1, which cmux-tui's
identify_capabilities pushes at runtime (capabilities.rs), so the bundled static
manifest never lists it. tests/test_cmux_next_static_capability_runtime_only.py
(release-notary, "Validate nightly push throttle") failed, and every nightly's
check-daemon-capabilities.sh --static-only would refuse the build. It joins
runtime_only, as #30447 did for agent-session-settings-v1.

Red at 03423de: python3 tests/test_cmux_next_static_capability_runtime_only.py   FAILED (failures=1)
Green here: same command, OK.

* l10n: compact workspace list uses this table's word for workspace (it, pl, pt-BR, uk, th)

* base red: release-notary, nightly step coverage follows #30200's visual verdict steps

#30200 (86fd376) removed find-nightly-next-recovery's "Read the visual
gate's verdicts" and renamed visual-gate-nightly-verdict's "Fail with the gate
when it holds the build" to "Record failed advisory verdict" (same script,
visual-gate-fail-when-held.sh). .github/nightly-step-coverage.json kept the old
names, so tests/test_nightly_step_coverage.py failed. The stale entry goes and
the renamed step keeps its plumbing entry.

Red at 03423de: python3 tests/test_nightly_step_coverage.py   FAILED (failures=1)
Green here: same command, OK.

* base red: release-notary, the nightly arm64-only recorder has the cache classifier the Release step sources

#30107 (2379dd9) made nightly.yml's "Build nightly app (Release)" source
scripts/cmux-next/lib/compilation-cache.sh. tests/test_nightly_next_arm64_only.py
runs that step in a scratch tree with recorder executables, and the tree had no
such file, so the step exited 1 before xcodebuild's recorder and the test failed
with CalledProcessError. The scratch tree now gets the real file, as it does
scripts/lib/rust-targets.sh. Every assertion is the same.

Red at 03423de: GITHUB_STEP_SUMMARY=/tmp/s python3 tests/test_nightly_next_arm64_only.py   CalledProcessError
Green here: same command, PASS: nightly-next arm64 only.

* base red: release-notary, nightly-next identity reads a YAML-anchored job env as its value

#30278 (fe4577e) turned the publish jobs' NIGHTLY_FEEDS into a YAML anchor
(`&pub-feeds ${{ needs.decide.outputs.feeds }}`) and aliases (`*pub-feeds`).
tests/test_nightly_next_identity.py reads job env as raw text, so
test_each_app_reads_the_feed_its_publisher_writes failed on the anchor prefix.
job_env now resolves an alias to its one anchor and drops an anchor name, as
YAML does; the asserted values are the same.

Red at 03423de: python3 tests/test_nightly_next_identity.py   7/8 passed
Green here: same command, 8/8 passed.

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 924af0f3 Deployed Nov 1, 2026 by teamleaderleo via wait for same-key tree publisher #94355
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant