-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
cloud: Try Again and cmux.sh buttons on failed port panes, Copy/Open Proxy URL with auto sign-in #12285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
cloud: Try Again and cmux.sh buttons on failed port panes, Copy/Open Proxy URL with auto sign-in #12285
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1784,6 +1784,7 @@ extension CMUXCLI { | |
| workspaceRaw: String?, | ||
| focus: Bool?, | ||
| printOnly: Bool, | ||
| viaProxy: Bool = false, | ||
| client: SocketClient, | ||
| jsonOutput: Bool | ||
| ) throws { | ||
|
|
@@ -1805,7 +1806,7 @@ extension CMUXCLI { | |
| )) | ||
| } | ||
| case .port(let machine, let port): | ||
| try openVMPort(vmId: machine, port: port, printOnly: printOnly, workspaceRaw: workspaceRaw, client: client, jsonOutput: jsonOutput) | ||
| try openVMPort(vmId: machine, port: port, printOnly: printOnly, viaProxy: viaProxy, workspaceRaw: workspaceRaw, client: client, jsonOutput: jsonOutput) | ||
| case .terminal(let machine, let remoteWorkspace, let terminal, let tab): | ||
| // The path contains a remote workspace selector. Resolve it before | ||
| // opening so the catalog can retain the exact placement instead of | ||
|
|
@@ -1935,6 +1936,7 @@ extension CMUXCLI { | |
| vmId: String, | ||
| port: Int, | ||
| printOnly: Bool, | ||
| viaProxy: Bool = false, | ||
| workspaceRaw: String?, | ||
| client: SocketClient, | ||
| jsonOutput: Bool | ||
|
|
@@ -1951,6 +1953,7 @@ extension CMUXCLI { | |
| } | ||
| var params: [String: Any] = ["id": vmId, "port": port] | ||
| if let workspaceRaw { params["workspace_id"] = workspaceRaw } | ||
| if viaProxy { params["proxy"] = true } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
# Expected: either the CLI rejects --proxy with --print, or the print path
# forwards proxy selection to a proxy-aware endpoint.
rg -n -C 10 -- \
'--proxy|viaProxy|printOnly|vm\.open_port|vm\.port_open|runVMOpenTarget|openVMPort' \
CLI/CMUXCLI+VMTui.swift \
CLI/cmux.swift \
Sources/Surfaces/SurfaceSocketCommands.swiftRepository: manaflow-ai/cmux Length of output: 27529 🤖 get_repo_knowledge executed:
Length of output: 44309 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- vm.open_port and vm.port_open implementations ---'
sed -n '180,325p' Sources/Surfaces/SurfaceSocketCommands.swift
printf '%s\n' '--- proxy and open_url contracts ---'
rg -n -C 8 --glob '*.swift' \
'CloudPortProxy|vm\.open_port|open_url|proxyURL|portPreviewUnavailableMessage' \
CLI SourcesRepository: manaflow-ai/cmux Length of output: 43211 Reject or implement
🤖 Prompt for AI Agents |
||
| let payload = try client.sendV2(method: "vm.port_open", params: params, responseTimeout: 120) | ||
| if jsonOutput { | ||
| print(jsonString(payload)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5778,10 +5778,14 @@ struct CMUXCLI { | |
| break | ||
| } | ||
| let printOnly = hasFlag(rest, name: "--print") | ||
| // `--proxy`: the port's public cmux.sh publication (signed in as this | ||
| // account) instead of the private route; the same path as the sidebar's | ||
| // "Open Proxy URL". | ||
| let viaProxy = hasFlag(rest, name: "--proxy") | ||
| let (workspaceOpt, rest1) = parseOption(rest, name: "--workspace") | ||
| let (focusOpt, rest2) = parseOption(rest1, name: "--focus") | ||
| let (windowOpt, rest3) = parseOption(rest2, name: "--window") | ||
| let openArgs = rest3.filter { $0 != "--print" } | ||
| let openArgs = rest3.filter { $0 != "--print" && $0 != "--proxy" } | ||
| let focus: Bool? | ||
| switch focusOpt?.lowercased() { | ||
| case nil: focus = nil | ||
|
|
@@ -5798,7 +5802,7 @@ struct CMUXCLI { | |
| guard case .machine(let vmId) = target, let port = Int(portArg), (1...65535).contains(port) else { | ||
| throw CLIError(message: Self.vmOpenUsage) | ||
| } | ||
| try openVMPort(vmId: vmId, port: port, printOnly: printOnly, workspaceRaw: workspaceOpt, client: client, jsonOutput: jsonOutput) | ||
| try openVMPort(vmId: vmId, port: port, printOnly: printOnly, viaProxy: viaProxy, workspaceRaw: workspaceOpt, client: client, jsonOutput: jsonOutput) | ||
| break | ||
| } | ||
| if case .machine(let vmId) = target { | ||
|
|
@@ -5813,7 +5817,7 @@ struct CMUXCLI { | |
| ) | ||
| break | ||
| } | ||
| try runVMOpenTarget(target, workspaceRaw: workspaceOpt, focus: focus, printOnly: printOnly, client: client, jsonOutput: jsonOutput) | ||
| try runVMOpenTarget(target, workspaceRaw: workspaceOpt, focus: focus, printOnly: printOnly, viaProxy: viaProxy, client: client, jsonOutput: jsonOutput) | ||
|
|
||
| case "status", "info": | ||
| guard let vmId = rest.first else { | ||
|
|
@@ -18585,7 +18589,7 @@ struct CMUXCLI { | |
| <machine>/<ws>[/<term>] (a cmux-tui workspace or one | ||
| terminal — reuses the pane already showing it), | ||
| <machine>:desktop, <machine>:port/<n>. | ||
| open <id> <port> [--print] | ||
| open <id> <port> [--print] [--proxy] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- help context ---'
sed -n '18570,18605p' CLI/cmux.swift
printf '%s\n' '--- command definitions and proxy handling ---'
rg -n -C 4 -- '--proxy|vm open|Mint a private HTTPS|cmux\.sh' CLI/cmux.swiftRepository: manaflow-ai/cmux Length of output: 11047 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- openVMPort implementation ---'
rg -n -C 35 'func openVMPort|openVMPort\(' CLI/cmux.swiftRepository: manaflow-ai/cmux Length of output: 4513 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- definitions and URL construction ---'
rg -n -C 6 'openVMPort|viaProxy|cmux\.sh|proxy URL|private.*URL|public.*URL' CLI --glob '*.swift'Repository: manaflow-ai/cmux Length of output: 50373 Security Misconfiguration Reachability: External Describe the URL selected by When - Mint a private HTTPS URL for an HTTP port on the VM
- and show it in a browser split. --print only prints.
+ Open the VM port in a browser split. By default, use
+ a private HTTPS URL; --proxy uses the public cmux.sh URL.
+ --print prints the private URL.🤖 Prompt for AI Agents |
||
| Mint a private HTTPS URL for an HTTP port on the VM | ||
| and show it in a browser split. --print only prints. | ||
| ssh <id> [--window <id|ref|index>] | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,11 +56,12 @@ extension CmuxTuiSurfaceProvider { | |
| try await forward.warmUpHub() | ||
| try Task.checkCancellation() | ||
| guard self.isCurrentLifecycleGeneration(generation) else { return } | ||
| self.releaseRetryToken(panelID: pane.panelID) | ||
| SurfacePaneFactory.navigate(panelID: pane.panelID, in: pane.workspaceID, to: localURL) | ||
| } catch { | ||
| guard !Task.isCancelled else { return } | ||
| guard self.isCurrentLifecycleGeneration(generation) else { return } | ||
| Self.showFailure(label: label, error: error, pane: pane) | ||
| self.showFailure(resource: resource, label: label, error: error, pane: pane) | ||
| } | ||
| } | ||
| return pane | ||
|
|
@@ -76,11 +77,12 @@ extension CmuxTuiSurfaceProvider { | |
| let url = try await self.controlPlanePreviewURL(port: port) | ||
| try Task.checkCancellation() | ||
| guard self.isCurrentLifecycleGeneration(generation) else { return } | ||
| self.releaseRetryToken(panelID: pane.panelID) | ||
| SurfacePaneFactory.navigate(panelID: pane.panelID, in: pane.workspaceID, to: url) | ||
| } catch { | ||
| guard !Task.isCancelled else { return } | ||
| guard self.isCurrentLifecycleGeneration(generation) else { return } | ||
| Self.showFailure(label: label, error: error, pane: pane) | ||
| self.showFailure(resource: resource, label: label, error: error, pane: pane) | ||
| } | ||
| } | ||
| return pane | ||
|
|
@@ -118,7 +120,7 @@ extension CmuxTuiSurfaceProvider { | |
| } catch { | ||
| self.browserPaneTasks[pane.panelID] = nil | ||
| guard !Task.isCancelled, self.isCurrentLifecycleGeneration(generation) else { return } | ||
| Self.showFailure(label: resource.title, error: error, pane: pane) | ||
| self.showFailure(resource: resource, label: resource.title, error: error, pane: pane) | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -185,11 +187,70 @@ extension CmuxTuiSurfaceProvider { | |
| return pane | ||
| } | ||
|
|
||
| private static func showFailure(label: String, error: any Error, pane: (workspaceID: UUID, panelID: UUID)) { | ||
| /// The failure page, with Try Again (the whole route again, in place) and, for a | ||
| /// port, "Open through cmux.sh instead": the personal publication, which needs | ||
| /// neither the hub nor a private address. The desktop never gets the proxy | ||
| /// button: its VNC page carries a token that must stay on the private route. | ||
| private func showFailure(resource: SurfaceResource, label: String, error: any Error, pane: (workspaceID: UUID, panelID: UUID)) { | ||
| let text = CloudMachineLink.errorText(error) | ||
| SurfacePaneFactory.showPlaceholder(SurfaceBrowserPlaceholder.failed(label, error: text), panelID: pane.panelID, in: pane.workspaceID) | ||
| releaseRetryToken(panelID: pane.panelID) | ||
| let port = resource.id.forwardedPort ?? resource.port | ||
| let proxyAvailable = resource.kind == .browser && port != nil && machine.cloudMachineID != nil | ||
| let token = SurfaceBrowserPlaceholderBridge.shared.register { [weak self] action in | ||
| self?.handlePlaceholderAction(action, resource: resource, label: label, pane: pane) | ||
| } | ||
| browserPaneRetryTokens[pane.panelID] = token | ||
| SurfacePaneFactory.showPlaceholder( | ||
| SurfaceBrowserPlaceholder.failed(label, error: text, token: token, proxyAvailable: proxyAvailable), | ||
| panelID: pane.panelID, | ||
| in: pane.workspaceID | ||
| ) | ||
| #if DEBUG | ||
| cmuxDebugLog("cloud.provider.endpointFailed label=\(label) error=\(String(reflecting: error))") | ||
| #endif | ||
| } | ||
|
|
||
| private func handlePlaceholderAction(_ action: SurfaceBrowserPlaceholderAction, resource: SurfaceResource, label: String, pane: (workspaceID: UUID, panelID: UUID)) { | ||
| guard let paneID = SurfacePaneFactory.paneID(ofPanel: pane.panelID, in: pane.workspaceID) else { return } | ||
|
Comment on lines
+213
to
+214
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🛡️ Analyzed with Security Review | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🤖 get_repo_knowledge executed:
Length of output: 45027 🏁 Script executed: #!/bin/bash
set -eu
file='Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift'
echo '--- target file outline ---'
ast-grep outline "$file" 2>/dev/null || true
echo '--- target file relevant symbols ---'
rg -n -C 12 'handlePlaceholderAction|browserPaneRetryTokens|SurfaceBrowserPlaceholderBridge|showFailure|stop\(|retry|openProxy' "$file"
echo '--- related definitions and callers ---'
rg -n -C 8 'class SurfaceBrowserPlaceholderBridge|struct SurfaceBrowserPlaceholderBridge|enum SurfaceBrowserPlaceholderAction|browserPaneRetryTokens|handlePlaceholderAction|showFailure|SurfaceBrowserPlaceholderBridge' Sources Tests 2>/dev/null || trueRepository: manaflow-ai/cmux Length of output: 43953 🏁 Script executed: #!/bin/bash
set -eu
file='Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift'
sed -n '1,280p' "$file"
echo '--- provider token references ---'
rg -n -C 10 'browserPaneRetryTokens|SurfaceBrowserPlaceholderBridge|stop\(' Sources/Surfaces
echo '--- tests mentioning placeholder/retry/proxy ---'
rg -n -C 8 'Placeholder|placeholder|retry token|browserPaneRetry|openProxy' Tests Sources 2>/dev/null || trueRepository: manaflow-ai/cmux Length of output: 50373 🏁 Script executed: #!/bin/bash
set -eu
rg -n -C 15 'browserPaneRetryTokens|SurfaceBrowserPlaceholderBridge|handlePlaceholderAction|showFailure|func stop|deinit' Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift Sources/Surfaces/CmuxTuiSurfaceProviders.swiftRepository: manaflow-ai/cmux Length of output: 25790 🏁 Script executed: #!/bin/bash
set -eu
sed -n '200,270p' Sources/Surfaces/CmuxTuiSurfaceProvider+PortForward.swift
sed -n '1,120p' Sources/Surfaces/CmuxTuiSurfaceProviders.swift
rg -n -C 8 'browserPaneRetryTokens|register|unregister|consume|SurfaceBrowserPlaceholderBridge' Sources/SurfacesRepository: manaflow-ai/cmux Length of output: 50372 🏁 Script executed: #!/bin/bash
set -eu
echo '--- bridge implementation and parser ---'
sed -n '319,370p' Sources/Surfaces/SurfacePaneFactory.swift
echo '--- provider shutdown and teardown references ---'
sed -n '145,180p' Sources/Surfaces/CmuxTuiSurfaceProviders.swift
rg -n -C 5 'releaseRetryToken|discardMaterialization|browserPaneTasks.*remove|browserPaneRetryTokens.*remove|stop\(\)' Sources/Surfaces/CmuxTuiSurfaceProviders.swift Sources/Surfaces/*.swift
echo '--- relevant tests ---'
rg -n -C 5 'SurfaceBrowserPlaceholderBridge|registeredTokenCount|browserPaneRetryTokens|PlaceholderAction|CloudPortProxy' Tests cmuxTests Sources 2>/dev/null || trueRepository: manaflow-ai/cmux Length of output: 50372 Consume the placeholder token before dispatch.
Keep 🤖 Prompt for AI AgentsSource: Path instructions |
||
| switch action { | ||
| case .retry: | ||
| browserPaneTasks[pane.panelID]?.cancel() | ||
| browserPaneTasks[pane.panelID] = Task { @MainActor [weak self] in | ||
| guard let self else { return } | ||
| do { | ||
| _ = try await self.materializeBrowserPane( | ||
| resource, | ||
| at: .tab(workspaceID: pane.workspaceID, paneID: paneID, index: nil), | ||
| focus: false, | ||
| reusing: pane | ||
| ) | ||
| } catch { | ||
| guard !Task.isCancelled else { return } | ||
| self.showFailure(resource: resource, label: label, error: error, pane: pane) | ||
| } | ||
| } | ||
| case .openProxy: | ||
| guard let vmID = machine.cloudMachineID, let port = resource.id.forwardedPort ?? resource.port else { return } | ||
| browserPaneTasks[pane.panelID]?.cancel() | ||
| browserPaneTasks[pane.panelID] = Task { @MainActor [weak self] in | ||
| guard let self else { return } | ||
| defer { self.browserPaneTasks[pane.panelID] = nil } | ||
| do { | ||
| let url = try await CloudPortProxy.url(vmID: vmID, port: port) | ||
| SurfacePaneFactory.showPlaceholder(SurfaceBrowserPlaceholder.connecting(url.host ?? label), panelID: pane.panelID, in: pane.workspaceID) | ||
| self.releaseRetryToken(panelID: pane.panelID) | ||
| _ = try await CloudPortProxy.open(url, replacing: pane) | ||
| } catch { | ||
| guard !Task.isCancelled else { return } | ||
| self.showFailure(resource: resource, label: label, error: error, pane: pane) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func releaseRetryToken(panelID: UUID) { | ||
| if let token = browserPaneRetryTokens.removeValue(forKey: panelID) { | ||
| SurfaceBrowserPlaceholderBridge.shared.unregister(token) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Print flag drops proxy URL
Medium Severity
--printand--proxyare parsed independently and listed together, butprintOnlyreturns throughvm.open_portand never sendsproxy. Combining them prints the private tokened URL instead of the publiccmux.shpublication, the CLI equivalent of Copy Proxy URL.Additional Locations (2)
CLI/cmux.swift#L5779-L5788CLI/cmux.swift#L18591-L18594Reviewed by Cursor Bugbot for commit e24bef3. Configure here.