Skip to content

Fix SSH browser loopback fetches across ports - #3820

Merged
austinywang merged 8 commits into
mainfrom
issue-3819-browser-ssh-multi-port
May 11, 2026
Merged

austinywang merged 8 commits into
mainfrom
issue-3819-browser-ssh-multi-port

Conversation

@austinywang

@austinywang austinywang commented May 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Reproduced issue Browser pane: SSH-forwarded frontend can't reach backend on a second SSH port (regression today) #3819 on origin/main in a tagged dev build launched with ./scripts/reload.sh --tag repro-3819-main --launch.
  • Added a regression test first, then the fix, preserving the requested test-first/fix-second structure.
  • Fixed page-initiated SSH browser runtime requests by injecting a document-start bridge for alias-host pages. The bridge rewrites localhost-family http: and cleartext ws: runtime URLs to the existing SSH loopback alias while preserving arbitrary ports and *.localhost subdomains.
  • Kept TLS-bearing https: and wss: URLs on their original hostnames to avoid changing SNI/certificate validation semantics.

Reproduction Evidence

Tagged main build: repro-3819-main.

SSH workspace: cmux ssh austins-macbook-pro, with remote servers on 127.0.0.1:3000 and 127.0.0.1:8000.

Direct browser-pane navigations worked in isolation:

  • http://localhost:8000/ rendered backend-8000
  • http://localhost:3000/ rendered frontend-3000-plain

After replacing the 3000 page with:

fetch('http://localhost:8000/')
  .then(r => r.text())
  .then(t => document.body.innerText = t)

the page stayed at pending fetch.

Exact browser pane DevTools Console errors captured during the main-branch repro:

Failed to load resource: Could not connect to the server. (http://localhost:8000/)
Unhandled Promise Rejection: TypeError: Load failed

The cmux browser CLI also reported:

[error] Load failed

Regression Source

Regressing commit: 8b2edfe2c7be2c835eddfd525946db2478bcbee2, PR #3764, Allow HTTP localhost subdomains in browser, merged 2026-05-09 01:44 UTC.

That change introduced the remote loopback proxy alias path for top-level browser navigations and proxy header rewriting. The missing path was page-initiated browser runtime requests: JavaScript still requested literal localhost, which WebKit treated as local-machine loopback instead of the SSH workspace proxy target.

Checked PR #3801 as requested; it is still open and not merged into main, so it is not the regression on current main.

Architecture

The fix keeps the existing lazy per-request proxy architecture. It does not hardcode common dev ports and does not allocate one tunnel per port. Pages loaded through cmux-loopback.localtest.me get a document-start bridge that maps localhost-family runtime URLs to the same alias host. The existing SOCKS/CONNECT proxy and request/response rewriters then route each arbitrary port back to remote loopback.

Ownership is split by responsibility:

  • RemoteLoopbackProxyAlias owns the loopback alias policy and Swift-native host mapping.
  • RemoteLoopbackRuntimeBridge owns the injected WebKit runtime bridge script generated from that policy.
  • BrowserPanel only installs the bridge and delegates host mapping.

Test Evidence

The test-only branch (issue-3819-browser-ssh-multi-port-test-only) failed as expected before the fix. CI run: https://github.com/manaflow-ai/cmux/actions/runs/25622448205. The macOS unit log showed the new bridge test failing with:

XCTAssertEqual failed: ("Optional("missing bridge")") is not equal to ("Optional("[\"http://cmux-loopback.localtest.me:3000/frontend\",\"http://cmux-loopback.localtest.me:8000/api\",\"http://api.cmux-loopback.localtest.me:8000/v1\",\"https://localhost:9443/secure\"]")")

The fixed PR branch passes on head a38ae275c8345ba6157e05ddede60243a8e5dded: 18 passed, 0 failed, 0 pending, 7 skipped. Passing checks include CodeRabbit, Greptile, Cursor Bugbot, activation-session, CircleCI macOS debug/release builds, CircleCI macOS unit tests, remote-daemon-tests, web typecheck, web DB migrations, workflow guard, Vercel, and Socket.

Local lightweight validation run before pushing the final review fix:

  • Node syntax/evaluation harness for the injected bridge body passed for http://localhost:3000, http://localhost:8000, http://api.localhost:8000, ws://localhost:5173, wss://localhost:5173 passthrough, and https://localhost:9443 passthrough.
  • git diff --check passed.
  • plutil -lint GhosttyTabs.xcodeproj/project.pbxproj passed after adding RemoteLoopbackRuntimeBridge.swift.

An SSH browser page can navigate through the loopback proxy alias, but JavaScript inside that page still emits literal localhost URLs. The regression test loads one remote-alias browser session and verifies that two arbitrary loopback ports from the same page context need to be translated to the proxy alias while HTTPS remains untouched.

Constraint: The repro must cover the browser-pane path without requiring a live SSH host in unit tests.

Rejected: Testing source text for the injected script | project policy requires runtime behavior, not grep-style assertions.

Confidence: high

Scope-risk: narrow

Directive: Keep this as the first commit in the issue-3819 stack so CI can show the test failing before the fix.

Tested: Not run locally; this is the intentionally failing regression-test commit.

Not-tested: Full cmux-unit suite; will use CI per workflow constraints.
The SSH browser proxy already multiplexes arbitrary remote ports once requests use the loopback alias. Page JavaScript was outside that boundary after the aliasing change, so fetches and other runtime APIs kept targeting local-machine localhost and never reached the remote daemon proxy.

This injects a document-start bridge only on cmux loopback alias pages. It rewrites HTTP and WebSocket localhost-family URLs to the same alias host while preserving arbitrary ports, paths, queries, and localhost subdomains. The existing proxy request and response rewriters then map those requests back to remote loopback and keep CORS/cookie headers aligned.

Constraint: Do not hardcode common dev ports; the route must remain lazy and per request.

Rejected: Reopening tunnels for a fixed frontend/backend port pair | users run arbitrary ports and the proxy already supports per-request multiplexing.

Rejected: Reverting alias navigation wholesale | WebKit loopback proxy bypass was the reason the alias exists.

Confidence: medium

Scope-risk: moderate

Directive: Any future remote-browser loopback routing change must cover page-initiated requests, not only top-level omnibar navigations.

Tested: node --check on the injected JavaScript body.

Not-tested: Local xcodebuild/cmux-unit; CI will run the regression test per workflow constraints.
@vercel

vercel Bot commented May 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 10, 2026 8:18am
cmux-staging Ready Ready Preview, Comment May 10, 2026 8:18am

@coderabbitai

coderabbitai Bot commented May 10, 2026 •

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

RemoteLoopbackProxyAlias centralizes loopback-host detection and provides runtimeBridgeScriptSource JS. BrowserPanel injects that script at document start for all frames. BrowserPanel host-rewriting functions delegate to RemoteLoopbackProxyAlias. Tests and a wait helper validate multiple loopback URL rewrite cases.

Changes

Remote Loopback URL Rewriting Bridge

Layer / File(s) Summary
Loopback host detection (Swift)
Sources/RemoteLoopbackProxyAlias.swift
Adds exactLoopbackHosts and isLoopbackHost(_:) to normalize hostnames and detect loopback/localhost patterns.
JavaScript Bridge Implementation
Sources/RemoteLoopbackProxyAlias.swift
Defines runtimeBridgeScriptSource JS that exposes window.__cmuxRewriteRemoteLoopbackURL and patches fetch, XMLHttpRequest, WebSocket, and EventSource to rewrite http:/ws:/wss: loopback hosts to the alias.
WebView Script Injection
Sources/Panels/BrowserPanel.swift
In configureWebViewConfiguration, registers RemoteLoopbackProxyAlias.runtimeBridgeScriptSource as a WKUserScript at .atDocumentStart with forMainFrameOnly: false.
BrowserPanel: host helpers refactor
Sources/Panels/BrowserPanel.swift
Removes BrowserPanel-local loopback host constants/helpers and updates remoteProxyDisplayURL(for:) and remoteProxyLoopbackAliasURL(for:) to call RemoteLoopbackProxyAlias helpers.
Runtime Bridge Verification Tests
cmuxTests/GhosttyConfigTests.swift
Adds testRemoteWorkspaceRuntimeBridgeAliasesMultipleLoopbackPortsFromSamePage which loads an inline page and asserts multiple loopback URL forms (localhost with ports/subdomains, wss) are rewritten to the alias while https://localhost is unchanged.
Test Helper Utilities
cmuxTests/GhosttyConfigTests.swift
Adds waitForBrowserWebViewLoad(_:, timeout:), an async helper that polls WKWebView.isLoading until load completes or timeout.

Sequence Diagram(s)

sequenceDiagram
  participant WebView as WKWebView
  participant Config as WKWebViewConfiguration
  participant Bridge as JS Bridge
  participant Page as Web Page
  participant Alias as Alias Host

  WebView->>Config: configureWebViewConfiguration()
  Config->>WebView: addUserScript(Bridge) atDocumentStart
  Page->>Bridge: network call to http://localhost:PORT/...
  Bridge->>Alias: rewrite host -> cmux-loopback.localtest.me and forward
  Alias-->>Page: response via rewritten host
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • manaflow-ai/cmux#1296: Yes — both PRs touch the same browser remote-loopback proxy/alias code (RemoteLoopbackProxyAlias and BrowserPanel WebView injection/URL rewriting).
  • manaflow-ai/cmux#3764: Main PR changes are related to PR #3764 — both modify BrowserPanel's loopback proxy/alias behavior and introduce/use RemoteLoopbackProxyAlias (including its runtimeBridgeScriptSource).
  • manaflow-ai/cmux#2779: Both PRs modify BrowserPanel.configureWebViewConfiguration to inject a document-start WKUserScript into the web view configuration.

Poem

🐰 A tiny hop, a clever tweak,
Bridge springs up at document peek.
Fetch and WebSocket wear new shoes,
Localhost whispers changed news.
Rewrites hum — the loopback schmooze.

🚥 Pre-merge checks | ✅ 14 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (14 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing SSH browser loopback fetches that previously failed across different ports.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed No Swift 6 actor isolation mistakes. RemoteLoopbackProxyAlias enum has only static immutable members; BrowserPanel changes are private; test additions allowed.
Cmux Swift Blocking Runtime ✅ Passed PR introduces no blocking/timing synchronization in production code. Test-only helper uses Task.sleep in polling loop—explicitly allowed deterministic test scaffolding with timeout protection.
Cmux No Hacky Sleeps ✅ Passed No violations of runtime-no-hacky-sleeps.md. Changes are in Swift code (covered by separate rule) or test-only scaffolding. No TypeScript, JavaScript, shell, or non-Swift production scripts modified.
Cmux Swift Concurrency ✅ Passed No legacy async patterns introduced. New RemoteLoopbackProxyAlias code is synchronous Swift. Test helper using Task.sleep is proper async/await for XCTest polling, not fire-and-forget.
Cmux Swift @Concurrent ✅ Passed Two new async functions are intentionally UI-bound (JavaScript evaluation and polling), perform no CPU/file/network-heavy operations, and comply with concurrent annotation rules.
Cmux Swift File And Package Boundaries ✅ Passed New RemoteLoopbackProxyAlias file is 191 lines with single responsibility; BrowserPanel reduced by 5 lines; no mixed responsibilities; aligns with extraction rules.
Cmux Swift Logging ✅ Passed No logging violations. RemoteLoopbackProxyAlias.swift has no logging. PR changes to BrowserPanel add no logging. Existing NSLog unrelated to loopback changes. Tests have no Swift logging.
Cmux Swiftui State Layout ✅ Passed No SwiftUI state violations. Changes are loopback logic refactoring and JavaScript bridge injection. No new @Published, @StateObject, @Observable, or render-time mutations introduced.
Cmux Architecture Rethink ✅ Passed Pure utility enum with immutable state only. Test-only polling allowed. No new mutable flags, observers, locks, split ownership, or unnamed invariants.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR does not add or materially change standalone cmux-owned windows. Changes are limited to WebView user script injection, JavaScript helpers, and test additions.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering the summary, reproduction evidence, root cause analysis, architecture decisions, and test evidence with clear verification steps.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-3819-browser-ssh-multi-port

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 10, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes cross-port loopback fetch/XHR/WebSocket/EventSource failures in SSH browser panes by injecting a document-start JavaScript bridge (RemoteLoopbackRuntimeBridge) that rewrites localhost-family URLs to the existing cmux-loopback.localtest.me proxy alias; https: and wss: are intentionally left unrewritten to preserve TLS certificate expectations. Loopback host constants and the isLoopbackHost predicate previously duplicated inside BrowserPanel are consolidated into RemoteLoopbackProxyAlias so the injected JS set stays in sync with Swift.

  • RemoteLoopbackRuntimeBridge (new, 141 lines): generates a static let IIFE source string that monkey-patches fetch, XMLHttpRequest, WebSocket, and EventSource in all frames on the alias host; self-gates on window.location.hostname so it no-ops outside the alias origin.
  • RemoteLoopbackProxyAlias: promotes exactLoopbackHosts and canonicalLoopbackHost to public statics; adds isLoopbackHost with normalizer guard; removes duplicate set from BrowserPanel.
  • Regression test: validates multi-port http/ws rewriting, subdomain aliasing (api.localhost), and confirms wss/https pass through unchanged.

Confidence Score: 5/5

Safe to merge; the bridge is correctly self-gated to the alias host and leaves https/wss untouched.

The new RemoteLoopbackRuntimeBridge is a clean, well-scoped JS injection with correct self-gating, deterministic static let initialization, and a regression test covering the exact failure scenario. The loopback-host logic consolidation removes duplication without changing semantics. The only gap is missing U+2028/U+2029 escape sequences in javaScriptStringLiteral, which is theoretical given all current inputs are static ASCII hostnames.

No files require special attention; RemoteLoopbackRuntimeBridge.swift is the only new production file and its escaping helper has a minor defensive gap noted inline.

Important Files Changed

Filename Overview
Sources/RemoteLoopbackRuntimeBridge.swift New 141-line file; generates the document-start JS bridge that monkey-patches fetch/XHR/WebSocket/EventSource. Logic is correct; javaScriptStringLiteral is missing U+2028/U+2029 escapes (minor gap for current static ASCII constants).
Sources/RemoteLoopbackProxyAlias.swift Promotes canonicalLoopbackHost and exactLoopbackHosts from BrowserPanel-private to enum-level statics; adds isLoopbackHost with BrowserInsecureHTTPSettings.normalizeHost guard. Clean consolidation, no regressions.
Sources/Panels/BrowserPanel.swift Removes duplicate loopback-host set and helper, injects RemoteLoopbackRuntimeBridge at document-start for all frames, and updates two call sites to use RemoteLoopbackProxyAlias directly. Straightforward cleanup with no logic regressions.
cmuxTests/GhosttyConfigTests.swift Adds regression test for multi-port rewriting (http/ws/wss/https) and a polling waitForBrowserWebViewLoad helper; Task.sleep is test-only scaffolding, which is explicitly allowed by the blocking-runtime rule.
GhosttyTabs.xcodeproj/project.pbxproj Registers RemoteLoopbackRuntimeBridge.swift in the Xcode project with consistent UUIDs; mechanical change only.

Sequence Diagram

sequenceDiagram
    participant Page as Page JS (alias host)
    participant Bridge as RemoteLoopbackRuntimeBridge
    participant WK as WKWebView / WebKit
    participant Proxy as SOCKS/CONNECT Proxy
    participant Remote as SSH Remote Server

    Note over WK,Bridge: atDocumentStart (forMainFrameOnly: false)
    WK->>Bridge: inject runtimeBridgeScriptSource
    Bridge-->>Page: monkey-patch fetch / XHR / WebSocket / EventSource

    Page->>Bridge: fetch("http://localhost:8000/api")
    Bridge->>Bridge: rewriteLoopbackURL → "http://cmux-loopback.localtest.me:8000/api"
    Bridge->>WK: nativeFetch("http://cmux-loopback.localtest.me:8000/api")
    WK->>Proxy: CONNECT cmux-loopback.localtest.me:8000
    Proxy->>Remote: forward to 127.0.0.1:8000
    Remote-->>Proxy: response
    Proxy-->>WK: response
    WK-->>Page: fetch resolved
Loading

Reviews (6): Last reviewed commit: "Preserve TLS hostnames in remote loopbac..." | Re-trigger Greptile

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment on lines +2017 to +2021
normalizedHost === '::1'
) {
return aliasHost;
}
const suffix = '.localhost';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 wss: connections are silently not rewritten

The rewriteLoopbackURL guard exits early for any protocol other than http: and ws:, which means wss://localhost:… WebSocket connections are left pointing at the literal loopback address and will fail the same way http://localhost fetches did before this fix. Any dev server that exposes WSS for HMR or streaming (e.g. Vite over HTTPS, Next.js with --experimental-https) would break silently — no rewrite, no proxy, connection refused — without the bridge reporting an error. The PR covers four APIs but leaves wss: as a blind spot even though EventSource and plain ws: are fully handled.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in daaf40d by allowing wss loopback URLs through the same alias rewrite path as ws, preserving arbitrary ports for encrypted WebSocket dev servers.

— Claude Code

Comment thread Sources/Panels/BrowserPanel.swift Outdated
Comment on lines +1984 to +1986
static var remoteLoopbackRuntimeBridgeScriptSource: String {
"""
(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 static var computes the entire 130-line string on every access — should be static let

All other script-source declarations in this class (telemetryHookBootstrapScriptSource, dialogTelemetryHookBootstrapScriptSource, etc.) are static let. The only reason remoteLoopbackRuntimeBridgeScriptSource is a computed static var is the \(remoteLoopbackProxyAliasHost) interpolation, but that value is itself a static let constant. The whole body can be a static let that evaluates once at first use, matching the established pattern.

Suggested change
static var remoteLoopbackRuntimeBridgeScriptSource: String {
"""
(() => {
static let remoteLoopbackRuntimeBridgeScriptSource: String = {
"""
(() => {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in daaf40d by changing the bridge source to a static let initializer so it matches the surrounding script constants.

— Claude Code

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmuxTests/GhosttyConfigTests.swift`:
- Line 1340: The loop waiting for the WKWebView should not rely on webView.url
when the content is loaded via loadHTMLString(baseURL:); update the waiting
condition in the loop that currently checks "while webView.isLoading ||
webView.url == nil" to only check webView.isLoading, i.e., replace the OR check
with a single isLoading check so the wait is robust for loadHTMLString(baseURL:)
usage and avoids depending on webView.url.

In `@Sources/Panels/BrowserPanel.swift`:
- Around line 2823-2829: The injected loopback bridge is currently limited to
main frames by configuration.userContentController.addUserScript(...) using
WKUserScript(..., forMainFrameOnly: true); change this to inject into all frames
by setting forMainFrameOnly to false so same-origin iframes get the rewrite and
proxying; the bridge already self-gates on frame hostname inside
Self.remoteLoopbackRuntimeBridgeScriptSource, so widening the injection is safe
and will no-op for third-party frames.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 394f9c8f-3b9d-43e0-8ec3-5d6a47b047c2

📥 Commits

Reviewing files that changed from the base of the PR and between f42270e and a83009a.

📒 Files selected for processing (2)
  • Sources/Panels/BrowserPanel.swift
  • cmuxTests/GhosttyConfigTests.swift

Comment thread cmuxTests/GhosttyConfigTests.swift Outdated
Comment thread Sources/Panels/BrowserPanel.swift
Review feedback found three valid gaps in the first fix: the bridge did not run inside alias-host subframes, encrypted WebSocket localhost URLs stayed outside the proxy alias path, and the WKWebView test helper depended on loadHTMLString updating webView.url.

This keeps the same lazy per-request proxy architecture, but widens the document-start bridge to all frames with the existing hostname self-gate, includes wss URLs in WebSocket-safe rewrites, and makes the test wait only on WebView loading state.

Constraint: Do not inject telemetry-style globals into third-party frames; this bridge still returns before mutating globals unless the frame host is the cmux loopback alias.

Rejected: Leaving wss unaliased | TLS WebSocket dev servers would hit the same local-loopback failure path as the original fetch bug.

Confidence: medium

Scope-risk: narrow

Directive: Keep the bridge host-gated if injection remains all-frame.

Tested: node --check on the updated injected JavaScript body.

Not-tested: Local xcodebuild/cmux-unit; PR CI and dispatched CI are used per workflow constraints.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/Panels/BrowserPanel.swift`:
- Around line 1984-2107: Extract the bridge JS and the localhost-alias mapping
into a new helper (e.g., RemoteLoopbackBridge or LoopbackAliasPolicy) so
BrowserPanel no longer embeds the large JS blob or duplicate alias logic: move
the static let remoteLoopbackRuntimeBridgeScriptSource and the loopback host
mapping logic (currently referenced via remoteLoopbackProxyAliasHost and any
loopbackAliasHost/normalizeHost helpers) into that helper, expose a single
canonical scriptSource string and a Swift-native alias mapping API (e.g.,
RemoteLoopbackBridge.scriptSource and
RemoteLoopbackBridge.rewriteHost(_:)/loopbackAliasHost(_:)), and update
BrowserPanel to call those symbols instead of keeping its own JS string or
duplicate rules. Ensure the helper is used by both the injected JS path and any
native rewrite logic so there is one source of truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5086ffb5-4db2-4523-aeea-13ff5590197a

📥 Commits

Reviewing files that changed from the base of the PR and between a83009a and daaf40d.

📒 Files selected for processing (2)
  • Sources/Panels/BrowserPanel.swift
  • cmuxTests/GhosttyConfigTests.swift

Comment thread Sources/Panels/BrowserPanel.swift Outdated
BrowserPanel should install the remote browser runtime bridge, not own the alias policy that navigation rewriting, header rewriting, and JavaScript runtime rewriting all depend on. Moving the loopback predicate and bridge source into RemoteLoopbackProxyAlias keeps the remote-browser proxy contract in one helper without changing the already validated runtime behavior.

Constraint: Reviewer requested a single owner for the loopback alias bridge after the functional fix was already green.

Rejected: Leave the WK bridge blob in BrowserPanel | duplicates the alias policy next to navigation rewriting and makes future port-scope regressions easier to reintroduce.

Confidence: high

Scope-risk: narrow

Directive: Keep remote localhost alias decisions in RemoteLoopbackProxyAlias so browser navigation and in-page APIs cannot drift.

Tested: Extracted bridge JavaScript with sed, checked it with node --check, and executed the multi-port rewrite harness in Node.

Not-tested: Local XCTest per project policy; PR CI will run the macOS unit suite.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/RemoteLoopbackProxyAlias.swift`:
- Around line 82-97: The JS hard-coded loopback list in the
runtimeBridgeScriptSource should be derived from the Swift exactLoopbackHosts
constant to avoid drift: replace the literal array/conditions inside the
loopbackAliasHost JS function with an interpolated representation of
exactLoopbackHosts (and optionally canonicalLoopbackHost for the suffix) when
building runtimeBridgeScriptSource so the JS string literal is generated from
the Swift constants; locate the JS snippet inside runtimeBridgeScriptSource and
inject a joined/escaped version of exactLoopbackHosts (and the suffix) instead
of the hard-coded 'localhost', '127.0.0.1', '0.0.0.0', '::1' values so any
future changes to exactLoopbackHosts automatically propagate to the embedded
script.
- Around line 99-119: Add an inline comment inside the rewriteLoopbackURL
function (near the protocol allowlist check that tests parsed.protocol !==
'http:' && parsed.protocol !== 'ws:' && parsed.protocol !== 'wss:') explaining
why wss: is rewritten but https: is not: note that tests (e.g.,
testRemoteWorkspaceRuntimeBridgeAliasesMultipleLoopbackPortsFromSamePage)
exercise this asymmetry, that WebSocket upgrade paths (wss) are proxied/handled
differently by the runtime so rewriting works for HMR/WS traffic while plain
HTTPS requests are intentionally left unchanged to avoid interfering with normal
HTTPS routing/certificate semantics, and reference loopbackAliasHost as the
rewriting hook; place the comment immediately above or inline with the protocol
check so future maintainers see the rationale where the decision is implemented.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 68c1b807-3590-4ef6-a7f6-5dd98bff326d

📥 Commits

Reviewing files that changed from the base of the PR and between daaf40d and 9be789e.

📒 Files selected for processing (2)
  • Sources/Panels/BrowserPanel.swift
  • Sources/RemoteLoopbackProxyAlias.swift

Comment thread Sources/RemoteLoopbackProxyAlias.swift Outdated
Comment thread Sources/RemoteLoopbackProxyAlias.swift Outdated
The injected bridge should not grow a second hard-coded list after moving ownership into RemoteLoopbackProxyAlias. Generating the JavaScript loopback set from the Swift constants keeps the native predicate and page-runtime bridge aligned, while documenting the tested wss/https protocol split at the allowlist.

Constraint: CodeRabbit requested removing the remaining drift point between Swift loopback constants and the generated runtime bridge.

Rejected: Leave the JavaScript loopback list literal in place | future host additions would still require two edits and reintroduce policy drift.

Confidence: high

Scope-risk: narrow

Directive: Update exactLoopbackHosts only; the bridge script derives its exact-host allowlist from that constant.

Tested: Extracted bridge JavaScript with sed, checked it with node --check, and executed the multi-port rewrite harness in Node.

Not-tested: Local XCTest per project policy; PR CI will rerun macOS unit tests.
Adding local setup inside the static bridge-script closure changed it from a single-expression closure to a multi-statement closure. Swift then requires an explicit return for the generated script string, which the CI debug build caught.

Constraint: Must fix CI without running local xcodebuild directly.

Confidence: high

Scope-risk: narrow

Tested: Extracted bridge JavaScript with sed, checked it with node --check, and executed the multi-port rewrite harness in Node.

Not-tested: Local XCTest per project policy; PR CI will rerun.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/RemoteLoopbackProxyAlias.swift`:
- Around line 55-189: The file mixes loopback-host policy with a large injected
JS runtime bridge; extract the static runtimeBridgeScriptSource and its helper
javaScriptStringLiteral out of RemoteLoopbackProxyAlias into a dedicated browser
runtime helper (e.g., create a new type RemoteLoopbackRuntimeBridge or
BrowserRuntimeBridge) and move the JS string and escaping logic there; update
RemoteLoopbackProxyAlias to reference
RemoteLoopbackRuntimeBridge.runtimeBridgeScriptSource (adjust access level from
private/static as needed), remove the helper from the enum, and run/update any
callers/tests to import/use the new helper so networking/policy code stays
isolated from platform bridge code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ddc37b1e-96f6-410d-94d9-cd67e4d4e2c8

📥 Commits

Reviewing files that changed from the base of the PR and between 9be789e and ef2350e.

📒 Files selected for processing (1)
  • Sources/RemoteLoopbackProxyAlias.swift

Comment thread Sources/RemoteLoopbackProxyAlias.swift Outdated
The browser SSH loopback bridge had grown inside the alias policy type, which made the policy owner also responsible for a large WebKit runtime script. Split the runtime injection script into its own helper while keeping the alias constants as the single source of truth used by both Swift routing and injected JavaScript.

Constraint: Preserve the existing remote loopback alias invariant and avoid direct xcodebuild per project build policy.

Rejected: Leave the bridge on RemoteLoopbackProxyAlias | keeps alias policy and WebKit runtime script ownership mixed.

Confidence: high

Scope-risk: narrow

Directive: Keep loopback host policy in RemoteLoopbackProxyAlias and browser runtime API shims in RemoteLoopbackRuntimeBridge.

Tested: Node bridge syntax and rewrite harness for localhost 3000/8000, subdomain localhost, ws, wss, and https; plutil lint of project.pbxproj; git diff --check.

Not-tested: Local XCTest and app build per workflow; PR CI and final reload.sh launch will verify.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 1ba943d. Configure here.

Comment thread Sources/RemoteLoopbackRuntimeBridge.swift Outdated
The runtime bridge can safely alias cleartext HTTP and WebSocket URLs because the proxy can remap those hostnames before the remote request is interpreted. TLS-bearing schemes carry hostname expectations into certificate validation, so the bridge now leaves https and wss loopback URLs unchanged and documents that boundary.

Constraint: Avoid changing SNI/certificate expectations for localhost development certificates.

Rejected: Rewrite wss like ws | it reaches the remote port but changes the certificate hostname to the alias.

Confidence: high

Scope-risk: narrow

Directive: Do not add TLS schemes to the runtime alias allowlist without a certificate/SNI strategy.

Tested: Node bridge syntax and rewrite harness for http 3000/8000, api.localhost, ws rewrite, wss passthrough, and https passthrough; git diff --check.

Not-tested: Local XCTest and app build per workflow; PR CI and final reload.sh launch will verify.

This branch was successfully deployed

1 active deployment
Preview – cmux — a38ae275 Deployed May 10, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant