Skip to content

Fix SSH remote CLI wrapper and proxy follow-ups - #1596

Merged
lawrencecchen merged 4 commits into
mainfrom
task-ssh-followup-review-fixes
Mar 17, 2026
Merged

lawrencecchen merged 4 commits into
mainfrom
task-ssh-followup-review-fixes

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Mar 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • dispatch wrapper-style remote daemon invocations into CLI mode unless they use a daemon entry subcommand
  • buffer loopback HTTP request headers before rewriting and flush buffered bytes on local EOF
  • stop duplicating the final proxy payload across data and eof stream events

Testing

  • CGO_ENABLED=0 go test ./cmd/cmuxd-remote -run "TestWrapperBinaryDispatchesIntoCLI|TestProxyStreamEOFPayloadIsNotDuplicatedAcrossDataAndEOFEvents"
  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -destination "platform=macOS" -derivedDataPath /tmp/cmux-task-ssh-followup-review-fixes-tests test -only-testing:cmuxTests/GhosttyConfigTests
  • ./scripts/reload.sh --tag task-ssh-followup-review-fixes

Summary by cubic

Fixes CLI wrapper dispatch for the SSH remote binary and makes loopback proxy header rewriting robust for streamed requests. Prevents header corruption and removes duplicate EOF payloads in proxy streams.

  • Bug Fixes
    • Wrapper dispatch: cmuxd-remote now runs in CLI mode when invoked via cmux or without a daemon entry subcommand (serve, version, cli).
    • Loopback HTTP requests: Buffer request bytes across chunks until full headers arrive (or EOF), rewrite once, then forward; flush buffered bytes on local EOF.
    • Proxy streams: Stop duplicating the final payload across proxy.stream.data and proxy.stream.eof (EOF now sends empty data_base64).

Written for commit d8a968c. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Added streaming HTTP request header rewriting, enabling incremental header manipulation as data arrives.
  • Bug Fixes

    • Improved EOF handling for proxy streams with more precise state management.
    • Fixed header rewriting to work across multiple buffered data chunks.
  • Tests

    • Added tests validating streaming header rewriting behavior across chunked data inputs.
    • Added tests ensuring proper EOF event payload handling.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@vercel

vercel Bot commented Mar 17, 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 Mar 17, 2026 10:02am

@greptile-apps greptile-apps Bot left a comment

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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented Mar 17, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56cefacf-9c13-4176-b06a-aa4e928a3dff

📥 Commits

Reviewing files that changed from the base of the PR and between dfcbaa3 and d8a968c.

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

📝 Walkthrough

Walkthrough

Adds a streaming HTTP header rewriter (RemoteLoopbackHTTPRequestStreamRewriter) wired into the proxy tunnel to incrementally rewrite loopback HTTP request headers across chunks and handle EOF; introduces CLI-invocation routing helpers (shouldRunCLIForInvocation/isDaemonEntryCommand) and changes proxy EOF event payload behavior to avoid duplicating payloads.

Changes

Cohort / File(s) Summary
Streaming HTTP Header Rewriting
Sources/Workspace.swift
Added RemoteLoopbackHTTPRequestStreamRewriter with rewriteNextChunk(_:, eof:) to buffer and incrementally rewrite loopback HTTP request headers. Wired into WorkspaceRemoteDaemonProxyTunnel.ProxySession via loopbackRequestHeaderRewriter. Updated forwardToRemote signature and forwarding calls to accept eof and pass rewritten chunks.
Tests for Stream Rewriter
cmuxTests/GhosttyConfigTests.swift
Added unit tests validating buffered header rewriting across chunk boundaries and EOF flush behavior for the new stream rewriter.
CLI Dispatch & EOF Handling
daemon/remote/cmd/cmuxd-remote/main.go, daemon/remote/cmd/cmuxd-remote/main_test.go
Added shouldRunCLIForInvocation and isDaemonEntryCommand to route wrapper binary invocations into CLI when appropriate. Modified EOF handling in proxy stream events to avoid duplicating payload by sending empty payload on EOF. Added test helpers and tests (including eofWithPayloadConn, wrapper dispatch test, and EOF payload duplication test).

Sequence Diagram(s)

sequenceDiagram
    participant Upstream as Upstream Stream
    participant ProxySession as ProxySession
    participant Rewriter as RemoteLoopbackHTTPRequest<br/>StreamRewriter
    participant Remote as Remote Host

    rect rgba(100, 150, 200, 0.5)
        Note over Upstream,Remote: Streaming Header Rewrite Flow
    end

    Upstream->>ProxySession: Data Chunk 1 (partial HTTP request)
    ProxySession->>Rewriter: rewriteNextChunk(chunk1, eof: false)
    Rewriter->>Rewriter: Buffer partial headers
    Rewriter-->>ProxySession: No output (headers incomplete)
    
    Upstream->>ProxySession: Data Chunk 2 (header continuation + body)
    ProxySession->>Rewriter: rewriteNextChunk(chunk2, eof: false)
    Rewriter->>Rewriter: Combine buffer, rewrite headers
    Rewriter-->>ProxySession: Rewritten data (Host/Origin/Referer updated)
    ProxySession->>Remote: Forward rewritten data (eof: false)
    
    Upstream->>ProxySession: EOF
    ProxySession->>Rewriter: rewriteNextChunk(empty, eof: true)
    Rewriter-->>ProxySession: Flush buffered state / trailers
    ProxySession->>Remote: Forward EOF (with any trailing data)
Loading
sequenceDiagram
    participant Wrapper as Wrapper Binary
    participant Main as main.go
    participant Decision as shouldRunCLIForInvocation
    participant CLI as CLI Handler
    participant Server as Server Handler

    rect rgba(150, 150, 100, 0.5)
        Note over Wrapper,Server: CLI Dispatch Decision Flow
    end

    Wrapper->>Main: Invocation (argv0, args)
    Main->>Decision: Check argv0 and args
    alt Dispatch to CLI
        Decision-->>Main: true
        Main->>CLI: Run CLI handler
        CLI-->>Wrapper: CLI output
    else Run as server
        Decision-->>Main: false
        Main->>Server: Start server path
        Server-->>Wrapper: Server starts normally
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #239: Extends the same workspace remote proxy/tunnel types (WorkspaceRemoteDaemonProxyTunnel.ProxySession) that are wired here to support streaming header rewriting.
  • PR #1296: Related to loopback alias and proxy handling; this change builds on the loopback HTTP header rewrite behavior introduced there.

Poem

🐰 Hop hop, chunks arrive in parts,

I buffer headers, fix their hearts;
Streams flow steady, EOF I meet,
CLI decides which road to beat,
Rewritten hops — a tidy feat! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 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 (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix SSH remote CLI wrapper and proxy follow-ups' clearly and concisely summarizes the main changes: wrapper dispatch fixes, loopback header buffering, and proxy EOF handling.
Description check ✅ Passed The description covers summary (what changed and why), testing (specific test commands provided), and the checklist items, meeting the template requirements despite omitting the demo video section which is not applicable for non-UI changes.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-ssh-followup-review-fixes
📝 Coding Plan
  • Generate coding plan for human review comments

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.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
daemon/remote/cmd/cmuxd-remote/main.go (1)

1031-1036: ⚠️ Potential issue | 🟠 Major

Empty EOF payload can drop buffered tail bytes in downstream rewriters.

At Line 1035, forcing DataBase64 to empty may break consumers that flush pending buffered bytes on EOF payload. This introduces a data-loss edge case for partial-header streams.

Suggested fix (no duplication, preserves EOF tail compatibility)
-        if len(data) > 0 {
-            _ = s.frameWriter.writeEvent(rpcEvent{
-                Event:      "proxy.stream.data",
-                StreamID:   streamID,
-                DataBase64: base64.StdEncoding.EncodeToString(data),
-            })
-        }
+        if len(data) > 0 && readErr == nil {
+            _ = s.frameWriter.writeEvent(rpcEvent{
+                Event:      "proxy.stream.data",
+                StreamID:   streamID,
+                DataBase64: base64.StdEncoding.EncodeToString(data),
+            })
+        }

         if readErr == io.EOF {
+            eofPayload := ""
+            if len(data) > 0 {
+                eofPayload = base64.StdEncoding.EncodeToString(data)
+            }
             _ = s.frameWriter.writeEvent(rpcEvent{
                 Event:      "proxy.stream.eof",
                 StreamID:   streamID,
-                DataBase64: "",
+                DataBase64: eofPayload,
             })
         } else if !errors.Is(readErr, net.ErrClosed) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@daemon/remote/cmd/cmuxd-remote/main.go` around lines 1031 - 1036, The EOF
handling in the read loop sets rpcEvent.DataBase64 to an empty string which can
drop buffered tail bytes; instead, when readErr == io.EOF use the actual
buffered payload (the same variable used for prior chunks) and include it
(base64-encoded) in the rpcEvent passed to s.frameWriter.writeEvent so
downstream rewriters receive any trailing bytes on EOF; modify the EOF branch in
the read loop that calls s.frameWriter.writeEvent(rpcEvent{...}) to populate
DataBase64 from the existing payload buffer rather than forcing "" while
preserving StreamID and Event "proxy.stream.eof".
🧹 Nitpick comments (1)
cmuxTests/GhosttyConfigTests.swift (1)

870-902: Add an EOF-flush regression case for incomplete headers.

This test only exercises the “headers complete before EOF” path (eof: false on both chunks). Please add a case where EOF arrives before \r\n\r\n and assert buffered bytes are flushed exactly once on EOF.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmuxTests/GhosttyConfigTests.swift` around lines 870 - 902, Add a regression
test that covers EOF arriving before the header terminator: create a new test
(e.g., testBuffersFlushOnEOFWhenHeadersIncomplete) that uses
RemoteLoopbackHTTPRequestStreamRewriter and feeds it two chunks where the second
chunk does NOT contain the full "\r\n\r\n" sequence and call
rewriteNextChunk(firstChunk, eof:false) then rewriteNextChunk(secondChunk,
eof:true); assert the first call returns empty, the second call returns the
buffered bytes exactly once (contains the rewritten Host/Origin/Referer
replacements you expect), that the output includes the remaining body data as a
suffix, and that no additional bytes are emitted on subsequent calls — this
ensures rewriteNextChunk(...) flushes buffered bytes once when EOF is true even
if headers were incomplete.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Workspace.swift`:
- Around line 1402-1435: The code buffers incoming bytes in
RemoteLoopbackHTTPRequestStreamRewriter.rewriteNextChunk into pendingHeaderBytes
until it sees "\r\n\r\n", which can grow unbounded for malformed input; add a
max header size constant (e.g., maxHeaderBytes = 64 * 1024) and after appending
data check if pendingHeaderBytes.count > maxHeaderBytes, and if so set
hasForwardedHeaders = true, take payload = pendingHeaderBytes, clear
pendingHeaderBytes, and return
RemoteLoopbackHTTPRequestRewriter.rewriteIfNeeded(data: payload, aliasHost:
aliasHost) to flush and stop further buffering; keep references to
pendingHeaderBytes, rewriteNextChunk, hasForwardedHeaders, and
RemoteLoopbackHTTPRequestRewriter in the change.

---

Outside diff comments:
In `@daemon/remote/cmd/cmuxd-remote/main.go`:
- Around line 1031-1036: The EOF handling in the read loop sets
rpcEvent.DataBase64 to an empty string which can drop buffered tail bytes;
instead, when readErr == io.EOF use the actual buffered payload (the same
variable used for prior chunks) and include it (base64-encoded) in the rpcEvent
passed to s.frameWriter.writeEvent so downstream rewriters receive any trailing
bytes on EOF; modify the EOF branch in the read loop that calls
s.frameWriter.writeEvent(rpcEvent{...}) to populate DataBase64 from the existing
payload buffer rather than forcing "" while preserving StreamID and Event
"proxy.stream.eof".

---

Nitpick comments:
In `@cmuxTests/GhosttyConfigTests.swift`:
- Around line 870-902: Add a regression test that covers EOF arriving before the
header terminator: create a new test (e.g.,
testBuffersFlushOnEOFWhenHeadersIncomplete) that uses
RemoteLoopbackHTTPRequestStreamRewriter and feeds it two chunks where the second
chunk does NOT contain the full "\r\n\r\n" sequence and call
rewriteNextChunk(firstChunk, eof:false) then rewriteNextChunk(secondChunk,
eof:true); assert the first call returns empty, the second call returns the
buffered bytes exactly once (contains the rewritten Host/Origin/Referer
replacements you expect), that the output includes the remaining body data as a
suffix, and that no additional bytes are emitted on subsequent calls — this
ensures rewriteNextChunk(...) flushes buffered bytes once when EOF is true even
if headers were incomplete.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 96a7944d-9600-44f1-8096-50626ebc5537

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc4bcb and dfcbaa3.

📒 Files selected for processing (4)
  • Sources/Workspace.swift
  • cmuxTests/GhosttyConfigTests.swift
  • daemon/remote/cmd/cmuxd-remote/main.go
  • daemon/remote/cmd/cmuxd-remote/main_test.go

Comment thread Sources/Workspace.swift

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

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/Workspace.swift">

<violation number="1" location="Sources/Workspace.swift:1414">
P1: Add a size limit for `pendingHeaderBytes` before continuing to buffer. If the header delimiter never arrives, this currently accumulates data without bound and can exhaust memory while blocking upstream forwarding.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread Sources/Workspace.swift
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Addressed the follow-up review notes in d8a968c.

  • added a 64 KiB cap to RemoteLoopbackHTTPRequestStreamRewriter so malformed requests stop buffering and flush through
  • added an EOF regression that proves buffered loopback request headers flush once on EOF even without \r\n\r\n
  • pulled in origin/main as merge commit 01bec8b7 before re-running verification

Local verification:

  • CGO_ENABLED=0 go test ./... in daemon/remote
  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -destination "platform=macOS" -derivedDataPath /tmp/cmux-task-ssh-followup-review-fixes-tests test -only-testing:cmuxTests/GhosttyConfigTests
  • ./scripts/reload.sh --tag task-ssh-followup-review-fixes

@lawrencecchen
lawrencecchen merged commit f585461 into main Mar 17, 2026
11 of 13 checks passed
@lawrencecchen
lawrencecchen deleted the task-ssh-followup-review-fixes branch March 17, 2026 10:02
bn-l pushed a commit to bn-l/cmux that referenced this pull request Apr 3, 2026
…p-review-fixes

Fix SSH remote CLI wrapper and proxy follow-ups

This branch was successfully deployed

1 active deployment
Preview — d8a968c6 Deployed Mar 17, 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