Skip to content

Fix remote daemon upload without SFTP - #8434

Merged
austinywang merged 16 commits into
mainfrom
issue-7851-synology-scp-sftp
Jul 21, 2026
Merged

austinywang merged 16 commits into
mainfrom
issue-7851-synology-scp-sftp

Conversation

@austinywang

@austinywang austinywang commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Replace the remote-helper SCP/SFTP dependency with a single SSH exec-channel upload: stream the local cmuxd-remote file into remote cat over stdin.
  • Keep the existing transactional install contract: same-directory temporary file, chmod 755, atomic mv, and cleanup on every post-transfer failure.
  • Use file-backed process stdin so the 7.4 MB helper is not buffered in memory.
  • Pin StdinNull=no at the file-backed SSH seam so caller/host configuration cannot silently discard the helper while remote cat exits successfully.
  • Preserve useful remote command failures while local process-launch failures use sanitized, localized user-facing descriptions.

Regression history

The bug coverage and fixes remain separately visible in the commit history:

  • e254283025: failing test proving helper bootstrap breaks when SCP/SFTP is unavailable
  • 34bbdf7d0b: SSH-exec/file-stdin uploader implementation
  • 8a15caec82: full upload/finalize/cleanup transaction assertions
  • 0ff4f29088: failing StdinNull=yes regression
  • 2514438599: StdinNull=no upload override
  • 87f68d6866 / c32014da6b: finalization-failure cleanup coverage and fix
  • b8dd1b8494: failing coverage for arbitrary local process-error disclosure
  • 8e79c8fe27: sanitized/localized upload process errors with English and Japanese catalog entries

Final head: 4c6bfb0c0cb5cf71a3d847b96621c46408152037
Merged base: 98a701ffd95a9a9f237ca882aed8bbacf92ec167

Exact-head build and tests

  • Tagged Debug build: ./scripts/reload.sh --tag issue-7851-synology-scp-sftp --launch
  • Bundle: com.cmuxterm.app.debug.issue.7851.synology.scp.sftp, version 0.64.20 (100)
  • Embedded commit: 4c6bfb0c0
  • App executable SHA-256: 5793722afe1a21338756097c91488b8e932e2d8a92143188d0c9a4248da236de
  • Strict deep code-signature verification: passed
  • Full native-arm64 CmuxRemoteSession SwiftPM suite: 111 tests in 17 suites passed
  • git diff --check, package lockfile policy, workspace package grouping, pbxproj test wiring, cmux/Aziz policy, and warning budget all passed
  • Warning budget: 133 actual / 221 allowed; zero new warnings introduced
  • Both historically tracked Swift files over 500 lines shrink; every new Swift file is under 500 lines; neither budget TSV changed. The former file-length checker/TSV were removed from current main, so the final audit also checks the last enforced budgets.
  • Localization audit passed: all six new upload-error keys have English and Japanese translations and the catalog compiled in the exact-head build.

Live SFTP-disabled verification

A deterministic loopback fixture uses OpenSSH 10.2 with SSH exec enabled and no SFTP subsystem. The SCP probe writes outside the app's cold remote home.

  • normal SSH exec succeeds
  • default SFTP-mode scp fails with subsystem request failed (exit 255)
  • legacy scp -O succeeds byte-identically
  • from an explicitly recorded empty remote home, the real tagged app reaches connected, daemon-ready, and proxy-ready in 19.125 seconds
  • exactly one transport=ssh-stdin upload installs an executable 7,432,928-byte helper byte-identical to the local binary
  • upload uses a same-directory temporary path followed by chmod and atomic mv; no temporary residue remains
  • a real terminal round-trip succeeds, and a second liveness command succeeds 107.790 seconds after ready while the workspace remains connected/ready
  • warm reuse reaches ready in 6.033 seconds without an upload or helper hash/mtime change; its terminal round-trip succeeds
  • an 18-byte corrupt executable triggers one hello retry and one SSH-stdin reinstall, restores the exact helper, leaves no residue, and supports a terminal round-trip
  • an unwritable destination surfaces a clear remote Permission denied error, makes only the bounded 4/8/16-second retries, and publishes neither a final helper nor a partial file
  • with caller configuration StdinNull=yes, the uploader still transfers one complete byte-identical helper with zero residue; this is intentionally upload-only because that caller option disables the later long-lived stdin protocol
  • after a real tagged-app process restart, both cold and warm workspaces restore connected/daemon-ready/proxy-ready, reuse the unchanged helper without upload, and complete fresh terminal round-trips
  • cleanup leaves no task-owned app/SSH processes, fixture listeners, tag socket, SwiftPM scratch directory, or upload temporary files

The retained nonvisual acceptance packet is bound to the exact head with a per-state SHA manifest. All 93 retained artifact hashes verify; its sensitive-data scan passed across all 94 files with zero secret/private-key matches and no zero-byte artifacts.

CI and review

Exact-head workflow: https://github.com/manaflow-ai/cmux/actions/runs/29784495752

The change-relevant jobs passed, including remote-daemon-tests, diff-sidecar-check, web tests/typecheck/migrations, React checks, agent-session resources, and change detection. The repository-wide workflow stops at two strict determinism findings in Packages/Shared/CmuxIrohTransport/Tests/CmuxIrohTransportTests/CmxIrohRelayCredentialCoordinatorTests+Refresh.swift (lines 43 and 46). That file is absent from this PR diff and has the identical blob 0a953b3b51b37b7c0cc6d23b5b3d338f79d5e054 in the PR head and merged base; downstream preflight/test/aggregate failures are consequences of that guard.

GitHub has no configured required checks for this branch. All PR-attached checks pass (CodeRabbit, Greptile, Socket, Cubic; Vercel is neutral). Canonical structured autoreview reports zero actionable findings, cmux policy is clean, Greptile reports 5/5 safe to merge, all actionable review threads are resolved, and GitHub reports MERGEABLE / CLEAN.

Coverage limit

No physical Synology DSM appliance was available. The live fixture deterministically covers the reported bug class—OpenSSH 10.2 with working SSH exec and a broken/unavailable SFTP subsystem—and exercises the real tagged app, remote filesystem transaction, daemon, proxy, terminal, retry, and restore paths. CUA/video was excluded from final acceptance at the requester's direction.

Closes #7851

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

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

The remote daemon installer now uses SSH exec with a local file streamed over stdin instead of SCP/SFTP. Process requests support file-backed stdin, coordinator execution helpers are reorganized, and tests cover successful uploads and detailed remote execution failures.

Changes

Remote daemon upload

Layer / File(s) Summary
File-backed process input
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Process/RemoteProcessRequest.swift, Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Process/RemoteSessionProcessRunner.swift, Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionProcessRunnerTests.swift
Adds stdinFile request support, streams the file through Process.standardInput, and verifies the behavior with /bin/cat.
Coordinator process execution helpers
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+ProcessExecution.swift, Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator.swift, Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+Bootstrap.swift
Moves SSH, SCP, and generic process execution helpers into a coordinator extension while removing the previous daemon upload implementation from bootstrap.
SSH daemon installation and validation
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+DaemonUpload.swift, Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteDaemonUploadTests.swift, Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionSSHRemoteCommandOverrideTests.swift
Installs the daemon through SSH cat >, applies chmod and atomic mv, cleans up failed temporary uploads, reports remote output, and adds injectable process responses plus success and failure tests.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RemoteSessionCoordinator
  participant RemoteSessionProcessRunner
  participant SSH
  RemoteSessionCoordinator->>RemoteSessionProcessRunner: run SSH mkdir command
  RemoteSessionProcessRunner->>SSH: Execute remote directory creation
  RemoteSessionCoordinator->>RemoteSessionProcessRunner: run SSH cat > with stdinFile
  RemoteSessionProcessRunner->>SSH: Stream daemon binary
  RemoteSessionCoordinator->>RemoteSessionProcessRunner: run SSH chmod and mv
  RemoteSessionProcessRunner->>SSH: Finalize daemon installation
Loading

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (2 errors, 1 warning)

Check name Status Explanation Resolution
Cmux User-Facing Error Privacy ❌ Error Daemon upload errors still interpolate error.localizedDescription and remote stderr into user-facing NSError text, exposing raw upstream details. Replace those NSError descriptions with sanitized, localized user copy and keep upstream stderr/ssh diagnostics in logs or internal telemetry; update tests.
Cmux Full Internationalization ❌ Error Daemon upload adds user-facing NSError descriptions as raw English literals in production; CmuxRemoteSession has no matching .xcstrings/localized entry. Replace those strings with String(localized:defaultValue:) (or equivalent) and add matching translated catalog entries for every supported locale.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (22 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement SSH-based helper upload without SFTP, preserve cleanup/error handling, and add matching regression coverage for #7851.
Out of Scope Changes check ✅ Passed The code changes stay focused on SSH-based remote daemon upload, stdinFile support, and related tests, with no clear unrelated additions.
Cmux Swift Actor Isolation ✅ Passed New file-backed stdin and daemon-upload helpers stay queue-confined; no new MainActor/UI-bound models or shared mutable Sendable refs were introduced in production.
Cmux Swift Blocking Runtime ✅ Passed PASS: The PR adds file-backed stdin/SSH upload helpers only; no new production semaphores, sleeps, syncs, polling, or locks were added, and the existing runner wait was unchanged.
Cmux Browser Automation Off-Main ✅ Passed PR only changes remote-session upload plumbing/tests; no browser.* commands or browser automation files were touched, so the rule is not triggered.
Cmux Expensive Synchronous Load ✅ Passed PASS: The PR only adds SSH/file-backed stdin plumbing on a utility-queue coordinator; no agent-history/JSON/transcript loads or new MainActor interactive path appear.
Cmux Cache Substitution Correctness ✅ Passed The PR only adds SSH/file-stdin upload plumbing and tests; no change replaces a fresh authoritative read with a stale cache in a persistence/history/undo/snapshot path.
Cmux No Hacky Sleeps ✅ Passed PR only changes Swift source/tests; the only sleep found is sleep 30 in a test, which the rule allows. No covered runtime script delays introduced.
Cmux Algorithmic Complexity ✅ Passed The production diff only adds single-command SSH/file-streaming helpers and one-element cleanup; it introduces no nested scans, sorts, or repeated filtering over scalable collections.
Cmux Swift Concurrency ✅ Passed Diff only adds file-backed stdin and synchronous SSH upload helpers/tests; no new DispatchQueue/Task/completion-handler pattern was introduced.
Cmux Swift @Concurrent ✅ Passed PASS: The diff adds only synchronous queue-confined SSH/process helpers; no new nonisolated async work or @concurrent annotations, and call sites already hop to the coordinator queue.
Cmux Swift Package Boundaries ✅ Passed The new upload logic lives in the CmuxRemoteSession SwiftPM package target, not the app target’s root Sources/, so no package-boundary violation is introduced.
Cmux Swiftpm Lockfiles ✅ Passed PR changes only a Swift source file; no .gitignore, workflow, Xcode project, or Package.resolved files were touched.
Cmux Swift Logging ✅ Passed PASS: The PR only adds a DEBUG-gated debugLog(...) in RemoteSessionCoordinator+DaemonUpload.swift; no print/NSLog/ad hoc stdout logging or new Loggers were introduced.
Cmux Swiftui State Layout ✅ Passed No SwiftUI view/state-layout patterns were introduced; the PR only touches coordinator/process/test code, with no ObservableObject/GeometryReader/lazy-row state violations.
Cmux Architecture Rethink ✅ Passed The PR adds a single SSH/stdinFile bridge for daemon upload and tests it; it doesn’t introduce timing hacks, extra state owners, or split lifecycle wiring.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed The PR only changes remote-session/process code and tests; no added or modified NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers usage was found.
Cmux Source Artifacts ✅ Passed All changed paths are intentional Swift source/test files under Packages/macOS/CmuxRemoteSession; no logs, screenshots, temp dirs, caches, or build artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed Only DaemonUpload changed in production; no added DEBUG/test-named seam or wrapper accessor appears in the PR diff.
Cmux No Ambient Global State ✅ Passed The production changes are all type/extension members or stored properties; no new file-scope funcs, mutable globals, static namespaces, or singletons were added.
Title check ✅ Passed The title clearly matches the main change: replacing the remote daemon upload path to work without SFTP.
Description check ✅ Passed The description covers summary and testing well; it is mostly complete despite missing the demo video block and checklist items.
✨ 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-7851-synology-scp-sftp

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.

@greptile-apps

greptile-apps Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces the SCP/SFTP-based daemon upload with a pure SSH exec channel approach: the local cmuxd-remote binary is streamed through cat > on the remote side, so helper installation works even when the SFTP subsystem or a remote scp executable is absent. The file is backed by a FileHandle rather than an in-memory Data buffer, keeping large binaries off the heap.

  • SSH stdin upload (RemoteSessionCoordinator+DaemonUpload.swift, +ProcessExecution.swift): three-step sequence — mkdir -p, cat > tmpPath (file-backed stdin), chmod 755 + mv — with cleanupUploadedRemotePaths called on every failure path after the temp file is created.
  • StdinNull=no guard (+ProcessExecution.swift line 29): file-backed sshExec overload prepends -o StdinNull=no before caller options so OpenSSH's first-value-wins rule prevents silent stdin discard when the host or caller sets StdinNull=yes.
  • RemoteProcessRequest.stdinFile (RemoteProcessRequest.swift, RemoteSessionProcessRunner.swift): new stdinFile: URL initializer and FileHandle wiring; defer { try? stdinFileHandle?.close() } fires after waitUntilExit(), keeping the fd alive for the child's full lifetime.
  • Localisation (Resources/Localizable.xcstrings): six new upload-error keys with en/ja translations using String(localized:defaultValue:) with interpolated defaultValue to supply substitution arguments for the %@ placeholders in translated strings.

Confidence Score: 5/5

Safe to merge — the upload path is self-contained, all failure branches clean up the temp file, and the StdinNull fix is well-guarded and tested.

The change replaces a well-scoped upload routine with a structurally identical one that uses a different transport. Every failure branch after temp-file creation calls cleanupUploadedRemotePaths, the FileHandle lifecycle is correctly bounded by waitUntilExit, the StdinNull override is positionally enforced and regression-tested, and all six new localised strings cover both supported locales. No actor isolation, blocking primitives, ambient globals, or test seams in production source are introduced.

No files require special attention.

Important Files Changed

Filename Overview
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+DaemonUpload.swift New file implementing SSH-stdin-based daemon upload; correctly handles mkdir, cat-based transfer, chmod+mv finalize, temp-path cleanup on every failure path, and properly localised error strings for both en and ja.
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+ProcessExecution.swift Process-execution helpers extracted from RemoteSessionCoordinator.swift; adds file-backed sshExec overload that correctly prepends -o StdinNull=no before caller options so OpenSSH's first-wins rule prevents silent discard.
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Process/RemoteSessionProcessRunner.swift Adds stdinFile FileHandle opening with a properly scoped defer-close; FileHandle lifecycle is correct because defer fires after waitUntilExit() completes, and the if/else chain correctly prioritises file-backed stdin over Data/null-device input.
Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Process/RemoteProcessRequest.swift Adds stdinFile: URL? field and a dedicated initializer that enforces mutual exclusivity with stdin: Data? at the type level via separate init overloads; clean public API extension.
Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteDaemonUploadTests.swift Comprehensive upload tests covering SFTP-absent success path, upload/finalize failure with cleanup, process-throw sanitisation, and StdinNull override; all previously-flagged gaps are addressed.
Resources/Localizable.xcstrings Six new upload-error string keys added with en and ja translations; format-specifier strings (%@) correctly pair with the defaultValue interpolation arguments used in Swift's String(localized:defaultValue:) API.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant C as RemoteSessionCoordinator
    participant PE as ProcessExecution
    participant PR as RemoteSessionProcessRunner
    participant SSH as /usr/bin/ssh
    participant R as Remote Shell

    C->>PE: uploadRemoteDaemonBinaryLocked(localBinary:, location:)
    PE->>SSH: ssh ... "sh -c 'mkdir -p dir'"
    SSH->>R: mkdir -p dir
    R-->>SSH: exit 0
    SSH-->>PE: RemoteCommandResult(status:0)

    PE->>PE: sshExec(arguments:, stdinFile: localBinary)
    Note over PE: Prepends -o StdinNull=no
    PE->>PR: run(RemoteProcessRequest(stdinFile: localBinary))
    PR->>PR: FileHandle(forReadingFrom: localBinary)
    PR->>SSH: "ssh -o StdinNull=no ... "sh -c 'cat > tmpPath'""
    Note over PR,SSH: streams binary bytes via stdin
    SSH->>R: "cat > tmpPath"
    R-->>SSH: exit 0
    SSH-->>PR: exit 0
    PR-->>PE: RemoteCommandResult(status:0)

    PE->>SSH: ssh ... "sh -c 'chmod 755 tmpPath and mv tmpPath dstPath'"
    SSH->>R: chmod + mv
    R-->>SSH: exit 0
    SSH-->>PE: RemoteCommandResult(status:0)
    PE-->>C: success

    Note over C,R: On any failure after temp upload: cleanupUploadedRemotePaths([tmpPath])
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant C as RemoteSessionCoordinator
    participant PE as ProcessExecution
    participant PR as RemoteSessionProcessRunner
    participant SSH as /usr/bin/ssh
    participant R as Remote Shell

    C->>PE: uploadRemoteDaemonBinaryLocked(localBinary:, location:)
    PE->>SSH: ssh ... "sh -c 'mkdir -p dir'"
    SSH->>R: mkdir -p dir
    R-->>SSH: exit 0
    SSH-->>PE: RemoteCommandResult(status:0)

    PE->>PE: sshExec(arguments:, stdinFile: localBinary)
    Note over PE: Prepends -o StdinNull=no
    PE->>PR: run(RemoteProcessRequest(stdinFile: localBinary))
    PR->>PR: FileHandle(forReadingFrom: localBinary)
    PR->>SSH: "ssh -o StdinNull=no ... "sh -c 'cat > tmpPath'""
    Note over PR,SSH: streams binary bytes via stdin
    SSH->>R: "cat > tmpPath"
    R-->>SSH: exit 0
    SSH-->>PR: exit 0
    PR-->>PE: RemoteCommandResult(status:0)

    PE->>SSH: ssh ... "sh -c 'chmod 755 tmpPath and mv tmpPath dstPath'"
    SSH->>R: chmod + mv
    R-->>SSH: exit 0
    SSH-->>PE: RemoteCommandResult(status:0)
    PE-->>C: success

    Note over C,R: On any failure after temp upload: cleanupUploadedRemotePaths([tmpPath])
Loading

Reviews (9): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

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

Caution

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

⚠️ Outside diff range comments (1)
Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteDaemonUploadTests.swift (1)

23-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the full upload transaction in these tests.

The runner returns success for every non-SCP request, so the tests would still pass if directory creation, chmod && mv finalization, or failure cleanup were removed. Return per-step responses and assert the expected command sequence—including the cleanup SSH command after upload failure.

Also applies to: 47-55, 68-78, 86-101

🤖 Prompt for 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.

In
`@Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteDaemonUploadTests.swift`
around lines 23 - 32, Update the test runner setup in the
RemoteDaemonUploadTests cases to return distinct responses for directory
creation, SCP upload, finalization, and cleanup commands instead of succeeding
for every non-SCP request. Assert the complete expected command sequence for
each transaction, including the cleanup SSH command after upload failure, so
removing any upload step causes the tests to fail.
🤖 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.

Outside diff comments:
In
`@Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteDaemonUploadTests.swift`:
- Around line 23-32: Update the test runner setup in the RemoteDaemonUploadTests
cases to return distinct responses for directory creation, SCP upload,
finalization, and cleanup commands instead of succeeding for every non-SCP
request. Assert the complete expected command sequence for each transaction,
including the cleanup SSH command after upload failure, so removing any upload
step causes the tests to fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa0a2c53-0e68-4ec2-89f8-a937db5f4f63

📥 Commits

Reviewing files that changed from the base of the PR and between 34bbdf7 and beb9da1.

📒 Files selected for processing (2)
  • Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteDaemonUploadTests.swift
  • Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionSSHRemoteCommandOverrideTests.swift

@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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
`@Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator`+DaemonUpload.swift:
- Around line 69-72: Sanitize the installation failure in
`RemoteSessionCoordinator` by replacing the raw `error.localizedDescription`
interpolation with a safe, user-friendly `String(localized:defaultValue:)`
message. Update
`Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+DaemonUpload.swift`
lines 69-72 and apply the same localization/sanitization to the adjacent
unchanged error at line 77 if it exposes raw details; update
`Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteDaemonUploadTests.swift`
lines 186-190 to assert the sanitized localized string or its `defaultValue`.
🪄 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: fbd6fa4e-7458-4838-ab63-ff36285c600e

📥 Commits

Reviewing files that changed from the base of the PR and between 8a15cae and c32014d.

📒 Files selected for processing (4)
  • Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+DaemonUpload.swift
  • Packages/macOS/CmuxRemoteSession/Sources/CmuxRemoteSession/Session/RemoteSessionCoordinator+ProcessExecution.swift
  • Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteDaemonUploadTests.swift
  • Packages/macOS/CmuxRemoteSession/Tests/CmuxRemoteSessionTests/RemoteSessionSSHRemoteCommandOverrideTests.swift

@austinywang
austinywang merged commit 97c3986 into main Jul 21, 2026
17 of 21 checks passed
ejc3 added a commit to ejc3/cmux that referenced this pull request Jul 24, 2026
…ed scp

Two tests waited on an scp invocation that no longer happens. manaflow-ai#8434 moved the daemon upload off scp
and onto the ssh exec channel, streaming the binary into `cat >`, and did not touch these tests.
Their stubs only fulfilled inside an `executable == "/usr/bin/scp"` branch, so the expectation
could never fire, the wait spent its whole budget, and the unwrap on the next line reported nil.

Both now capture the upload from the ssh branch. The property each one is about is unchanged: the
daemon still has to land on an absolute path under the remote HOME, that path just travels inside
the remote command instead of an scp destination, so the assertion moved with it.

The scp branch is kept and fails loudly. If the upload ever returns to scp, that should be a
sentence in the failure output rather than a silent timeout, which is precisely how these two broke.

The reinstall test also now records how many capability hellos preceded the upload and requires at
least one. Retargeting alone would have let it pass on a first install, which is not the
missing-pty-capability path it is named for.

Renamed the first test off "ScpDestination" since it no longer describes what is asserted.
ejc3 added a commit to ejc3/cmux that referenced this pull request Jul 25, 2026
…ed scp

Two tests waited on an scp invocation that no longer happens. manaflow-ai#8434 moved the daemon upload off scp
and onto the ssh exec channel, streaming the binary into `cat >`, and did not touch these tests.
Their stubs only fulfilled inside an `executable == "/usr/bin/scp"` branch, so the expectation
could never fire, the wait spent its whole budget, and the unwrap on the next line reported nil.

Both now capture the upload from the ssh branch. The property each one is about is unchanged: the
daemon still has to land on an absolute path under the remote HOME, that path just travels inside
the remote command instead of an scp destination, so the assertion moved with it.

The scp branch is kept and fails loudly. If the upload ever returns to scp, that should be a
sentence in the failure output rather than a silent timeout, which is precisely how these two broke.

The reinstall test also now records how many capability hellos preceded the upload and requires at
least one. Retargeting alone would have let it pass on a first install, which is not the
missing-pty-capability path it is named for.

Renamed the first test off "ScpDestination" since it no longer describes what is asserted.
ejc3 added a commit to ejc3/cmux that referenced this pull request Jul 31, 2026
…ed scp

Two tests waited on an scp invocation that no longer happens. manaflow-ai#8434 moved the daemon upload off scp
and onto the ssh exec channel, streaming the binary into `cat >`, and did not touch these tests.
Their stubs only fulfilled inside an `executable == "/usr/bin/scp"` branch, so the expectation
could never fire, the wait spent its whole budget, and the unwrap on the next line reported nil.

Both now capture the upload from the ssh branch. The property each one is about is unchanged: the
daemon still has to land on an absolute path under the remote HOME, that path just travels inside
the remote command instead of an scp destination, so the assertion moved with it.

The scp branch is kept and fails loudly. If the upload ever returns to scp, that should be a
sentence in the failure output rather than a silent timeout, which is precisely how these two broke.

The reinstall test also now records how many capability hellos preceded the upload and requires at
least one. Retargeting alone would have let it pass on a first install, which is not the
missing-pty-capability path it is named for.

Renamed the first test off "ScpDestination" since it no longer describes what is asserted.
austinywang added a commit that referenced this pull request Aug 4, 2026
…test host (#9572)

* cmuxTests: derive the theme reload target from a dash-free socket suffix

The CLI derives a theme reload target from the socket file name, collapsing every run of
non-alphanumerics in the slug to a dot. #6452 made this fixture's socket path unique with a raw
UUID to stop two runs colliding in /tmp, which put the UUID's dashes into the derived identifier
as dots, so the expected literal could no longer match and the test waited out its five seconds.
The stdout assertion kept passing because the derived id still has the expected value as a
prefix, which is why this read as a timeout rather than a string mismatch.

Keeps the unique suffix hex-only so the expected identifier stays a plain template instead of a
call into the CLI's own helper, which would agree by construction.

* cmuxTests: drop two palette assertions for a gate that no longer exists

#8173 replaced the fork-probe reuse gate: `!cachedResultHadFallback` became
`cachedResultIsFresh`, and the fallback case is now re-verified against SharedLiveAgentIndex at
the call site instead of being refused outright. The parameter stayed in both signatures, so
these two assertions still compiled while asserting the opposite of what the product does, and
WorkspaceForkConversationContextMenuTests asserts the new contract in both directions a few
files away.

Removes the two assertions whose only purpose was the removed term, and renames the clear-side
test to say what it still covers.

* cmuxTests: stop the remote-connection suite killing its own test host

Three separate problems, in order of blast radius.

Two assertions indexed `operations` right after asserting its count. A count assertion does not
stop execution, so on failure the next line trapped with Index out of range and took the shared
test host down, and every remaining test in the shard never ran. Measured twice in one run.

Four @mainactor tests waited on a DispatchSemaphore. configureRemoteConnection enqueues its
session transition as a main-actor Task, so blocking the main actor stopped the very work being
waited on from ever being scheduled. They now use expectations, which pump the run loop.

Fifteen fixtures passed an unresolved %C control template. The broker deliberately refuses to
own a path it cannot resolve, so no lease was ever taken and cleanup could not run; six inverted
expectations were passing vacuously as a result. They now use the resolved form ssh -G produces,
and a new test pins the unowned-template policy so the fixtures cannot quietly regress to it.

Two more read activeRemoteSessionControllerID straight after configureRemoteConnection and now
await the transition instead.

* cmuxTests: point the daemon-upload tests at the transport that replaced scp

Two tests waited on an scp invocation that no longer happens. #8434 moved the daemon upload off scp
and onto the ssh exec channel, streaming the binary into `cat >`, and did not touch these tests.
Their stubs only fulfilled inside an `executable == "/usr/bin/scp"` branch, so the expectation
could never fire, the wait spent its whole budget, and the unwrap on the next line reported nil.

Both now capture the upload from the ssh branch. The property each one is about is unchanged: the
daemon still has to land on an absolute path under the remote HOME, that path just travels inside
the remote command instead of an scp destination, so the assertion moved with it.

The scp branch is kept and fails loudly. If the upload ever returns to scp, that should be a
sentence in the failure output rather than a silent timeout, which is precisely how these two broke.

The reinstall test also now records how many capability hellos preceded the upload and requires at
least one. Retargeting alone would have let it pass on a first install, which is not the
missing-pty-capability path it is named for.

Renamed the first test off "ScpDestination" since it no longer describes what is asserted.

* cmuxTests: fix three CLI tests that could not pass, and stop one hiding why

Three separate causes, all in the fixtures rather than the product.

Two socket-selection tests replied to the CLI with a bareword. SocketClient only treats OK, OK …,
PONG, ERROR: … or JSON as a complete single-line reply, so a bareword sends it into the multiline
drain pass, where reconfiguring the receive timeout on a socket whose peer already hung up fails with
EINVAL — and the CLI reports "Invalid argument" instead of the reply it already had buffered. The
replies are now OK-framed. These were the only two barewords in the suite, which is why eleven
near-identical siblings pass.

Both now also assert which responder received the request. That is the property they exist for —
the tagged socket is chosen and the stable one is not — and unlike the stdout comparison it cannot
be made vacuous by a future change to the reply.

A fork-diagnostics fixture passed agent "project-agent", which is not in the CLI's catalog, so the
command exited before emitting any JSON. The test has never passed; it went in already red alongside
the pi-family gate it is meant to cover. It now uses grok, a catalog agent that is neither pi-family
nor one of the transcript-walking agents, so the basename gate is still what is under test.

The shared helper turned all of that into a JSON decoding failure, because it only expected a zero
exit before parsing. It now requires the exit status and a completed run, so the next fixture mistake
reports the CLI's own error text instead of a parse error.

* cmuxTests: pair the pi-basename fixture with an agent that can actually fork

The pi-family basename test asked for fork_command_available, fork_supported and
fork_startup_input_available, but its fixture stored the record under a grok
launcher pair. A captured launch command is only used when its launcher describes
the requested agent, so the grok/omo pair was dropped as untrusted, no fork argv
was built for any agent, and all four assertions failed on
agent_has_no_fork_command without ever reaching the rule under test.

Store the record under opencode instead, whose wrapper launcher is omo. The
capture is now trusted, the fork argv resolves through the omo launcher, and the
executable basename stays /tmp/pi so the disagreement between the structured
identity and the basename is still what the test measures. The omo launcher also
answers fork support before the opencode executable probe, so the result does not
depend on a /tmp/pi existing on the machine running the test.

* cmuxTests: assert the stderr-closed CLI does not crash, instead of a CLI that no longer exists

This test asserted exit 1 and a "Usage:" banner on stdout. Neither has been true since #f48922aa94:
an unknown command exits 2 with a single line and no usage dump, and that line goes to stderr — which
the test closes with 2>&-. So it could not pass, and the crash it was written for was not what it
checked.

The regression is still worth guarding. cc4a610 replaced FileHandle.standardError.write, which
raises and aborts when stderr is closed, with a raw Darwin.write that returns -1 on EBADF. The oracle
is therefore that the CLI exited on its own terms rather than dying from a signal, so ProcessRunResult
now carries terminationReason and both runners set it. Without that, a signalled process is
indistinguishable from an ordinary non-zero exit, because its terminationStatus is just the signal
number.

The command now runs under exec, so the process being waited on is the CLI rather than the shell. A
shell reports a signalled child as a normal exit with status 128+signal, which would have hidden
exactly the crash being tested.

It also pins CMUX_SOCKET_PATH and the home directory. Socket resolution otherwise consults a
machine-global marker file, and a spawn with a pristine temp home was measured reaching a real running
app — which would make the exit code depend on what is running on the machine. With the socket pinned
the unknown-command path is a single branch, so the test asserts exit 2 exactly rather than settling
for non-zero.

* cmuxTests: isolate the CLI regression suite from the machine's own cmux

A CLI spawned from this suite with a pristine temp home and a scrubbed
environment still reached a real running app. CFFIXED_USER_HOME moves the socket
directory but not socket discovery: the CLI also reads the machine-wide
/tmp/cmux-last-socket-path marker, and for an untagged debug build it scans /tmp
for cmux-debug-*.sock and connects to what it finds. Resolution runs before the
command dispatches, so even `claude-teams --help` did this. Every spawn site that
is not itself testing resolution now pins CMUX_SOCKET_PATH to a per-run path, the
three stable-variant tests write the marker inside their own temp home, and
runShell takes an explicit environment instead of handing the child everything
the test host was launched with.

Two tests bound a responder on /tmp/cmux.sock, the release app's socket path, and
UnixSocketResponder unlinks before it binds, so a run could take the control
socket away from a release app in use. The early returns meant to prevent that
raced the app, disagreed about whether a dangling symlink counts as present, and
turned the tests into silent passes. The symlink fallback case moves to the
user-scoped stable path inside its temp home. The legacy case keeps the part that
needs the real path, that /tmp/cmux.sock is classified as a stable implicit
default, and no longer creates, binds, or removes it. Three more guards tested
paths inside a freshly created temp home and could never fire, so they are gone.

stderr was pointed at the stdout pipe while about thirty tests parse stdout as
JSON or compare it to an exact reply, so one diagnostic line from the runtime
broke a content check instead of naming itself. stderr now has its own pipe,
failure messages carry both streams, and the negative checks that meant "the CLI
never said this anywhere" read both rather than silently narrowing to stdout.
Readers for both pipes start before the wait, because reading after
waitUntilExit deadlocks once a child fills a pipe buffer and that looks like a
hang inside the CLI. A launch failure is reported on stdout as well as stderr,
since five sibling suites share this runner and print only stdout.

Runs that assert nothing about latency no longer carry a 5s cap and take a 60s
guard instead, which still fails a stuck CLI rather than passing slowly. The two
browser-download tests keep their 3s and 16s caps, where the deadline is the
assertion. The two theme tests with fixed bundle identifiers now scope them per
run, since the reload notification goes out machine-wide; for the nightly one
that means scoping the socket file name too, because the identifier is derived
from it.

* cmuxTests: assert the exit code this fixture actually produces

The stderr-closed test asserted exit 2, the unknown-command code. Measured, it exits 1: the pinned
socket has no listener, so the CLI fails at connect and the top-level handler returns before the
unknown-command arm runs. That ordering makes the fixture a better exercise of what the test guards,
not a worse one, because the connect error is written to the stderr the test has closed. The run
confirmed the guard itself holds — termination reason was a normal exit, not a signal.

* cmuxTests: report stderr in sessions helper failures

* cmuxTests: preserve restore assertions after stream split

* cmuxTests: close review gaps in process and upload fixtures

* cmuxTests: align remote fixtures with streamed input and scoped identity

* cmuxTests: yield main actor while awaiting daemon upload

* cmuxTests: repair CLI regression fixtures and child lifetimes

* cmuxTests: isolate daemon bootstrap fixtures from ControlMaster

* cmuxTests: keep theme notification state nonisolated

* cmuxTests: detach live argv fixture from test host

* cmuxTests: own Go discovery in daemon reinstall fixture

* cmuxTests: make subprocess and bootstrap fixtures deterministic

* cmuxTests: remove detached fixture wall clock

* cmuxTests: use async-safe scoped locking

* cmuxTests: make off-host process work concurrent

* cmuxTests: keep blocking process wait off cooperative executor

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
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.

cmux ssh fails on Synology DSM with OpenSSH 10 because scp defaults to SFTP

1 participant