Skip to content

Conversation

@ethanndickson
Copy link
Member

@ethanndickson ethanndickson commented Jan 20, 2026

Summary

Unify SSH transports under a single SSHRuntime with pluggable transport abstraction. Adds hidden config option to switch between OpenSSH (default on non-Windows) and SSH2 (always on Windows).

Motivation

  • Windows support: OpenSSH ControlMaster is Unix-only; ssh2 library works cross-platform
  • Coder integration: Coder uses Match host ... !exec blocks in SSH config for ProxyCommand; the ssh2 approach parses and honors these rules directly
  • Cleaner architecture: Single runtime class with transport abstraction instead of parallel class hierarchies

Key Changes

Architecture Refactor

  • Removed SSH2Runtime.ts: No longer a separate class
  • New transport abstraction: SSHTransport interface with OpenSSHTransport and SSH2Transport implementations
  • Unified SSHRuntime: Delegates to transport for exec and PTY operations
  • PtyHandle interface: Common interface for PTY sessions across transports; PTYService no longer branches by transport type

New Files

  • src/node/runtime/transports/ - Transport abstraction layer
  • src/node/runtime/ptyHandle.ts - Unified PTY interface
  • src/node/runtime/ptySpawn.ts - Shared PTY spawn utility
  • src/node/runtime/shellQuote.ts - Shell quoting for Docker paths

SSH2 Auth Improvements

  • Agent-first fallback: try SSH agent first, then default keys (~/.ssh/id_rsa, id_ed25519, etc.) on auth failure
  • Two-pass connection flow in SSH2ConnectionPool.connect()

Bug Fixes

  • Bundle corruption fix: Removed streamProcessToLogger stdout drain that consumed git bundle data before pipe could read it
  • PTY double-quoting fix: expandTildeForSSH already quotes paths; don't wrap again with shellQuotePath
  • PTY fail-fast: Sessions use maxWaitMs: 0 to avoid 2-minute backoff waits
  • Local timeout: resolvePath and bundle upload use local abort-timeout (no remote timeout binary needed)

Config Option

  • Added useSSH2Transport boolean to ~/.mux/config.json
  • Windows: Always SSH2 (config ignored)
  • Other OSs: Defaults to OpenSSH; set "useSSH2Transport": true to use SSH2

Testing

  • make static-check passes
  • Manually validated OpenSSH and SSH2 transports on Linux
  • Workspace creation, terminal sessions, git bundle sync all working

Generated with mux • Model: anthropic:claude-opus-4-5 • Thinking: high • Cost: $52.40

@github-actions github-actions bot added the enhancement New feature or functionality label Jan 20, 2026
@ethanndickson ethanndickson marked this pull request as draft January 20, 2026 06:39
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4125fbd5b3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@ethanndickson
Copy link
Member Author

@codex review

@ethanndickson ethanndickson marked this pull request as ready for review January 20, 2026 10:11
@ethanndickson
Copy link
Member Author

@codex review

The previous comment about identity fallback is already addressed in the code:

  1. baseIdentityFiles captures explicit config (lines 434-435)
  2. fallbackIdentityFiles uses DEFAULT_IDENTITY_FILES (~/.ssh/id_rsa, id_ed25519, etc.) when no explicit files are configured (lines 436-439)
  3. Agent-first flow: if agent exists and no explicit identity files, try agent-only first (lines 547-555)
  4. On auth failure (or no agent), retry with fallbackIdentityFiles which includes default keys (lines 558-559)

This matches the behavior described in the concern - users without an agent but with default key files will have those keys tried automatically.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ded4137ab0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Replace OpenSSH ControlMaster-based SSH runtime with pure JavaScript
ssh2 implementation to support Windows and Coder's ProxyCommand.

Key changes:
- SSH2Runtime: extends SSHRuntime, overrides spawnRemoteProcess() to use
  ssh2 library's client.exec() instead of spawning system ssh command
- SSH2ConnectionPool: manages persistent connections with health tracking,
  exponential backoff, and singleflighting concurrent connection attempts
- sshConfigParser: parses ~/.ssh/config using ssh-config library, handles
  Match host ... !exec blocks for Coder's ProxyCommand configuration
- CoderSSH2Runtime: mixin pattern produces Coder-aware SSH2 runtime
- PTY support: SSH2Pty class wraps ClientChannel for terminal sessions
- Path quoting: shellQuotePath() prevents shell injection in cd commands

SSH2 is now the default on all platforms. The old OpenSSH CLI-based
SSHRuntime remains available but unused by default.
Connections are now closed after 60 seconds of inactivity to match
ControlMaster's ControlPersist=60 behavior. This prevents accumulating
stale connections to many Coder workspaces.

Each acquireConnection() call resets the idle timer, so actively used
connections stay open indefinitely.
TODO(ethan): Revert this after SSH2 runtime testing
…raction

- Replace SSH2Runtime with transport abstraction (OpenSSHTransport, SSH2Transport)
- SSHRuntime now delegates to transport for exec and PTY operations
- Unified PTY handling with PtyHandle interface and spawnPtyProcess utility
- PTYService no longer needs to know transport-specific details
- Narrowed transport config to SSHConnectionConfig for cleaner boundaries

SSH2 auth improvements:
- Agent-first fallback: try SSH agent first, then default keys on auth failure
- Two-pass connection flow in SSH2ConnectionPool.connect()

Bug fixes:
- Fix bundle corruption: remove streamProcessToLogger stdout drain that consumed
  bundle data before pipe could read it
- Fix PTY double-quoting: expandTildeForSSH already quotes, don't wrap again
- PTY sessions fail fast with maxWaitMs: 0 (no 2-minute backoff waits)
- Local abort-timeout for resolvePath/bundle upload (no remote timeout binary needed)

---
_Generated with `mux` • Model: `anthropic:claude-opus-4-5` • Thinking: `high` • Cost: `$82.21`_
@ethanndickson
Copy link
Member Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 40f2913cea

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@ethanndickson
Copy link
Member Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73a6bea899

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@ethanndickson
Copy link
Member Author

@codex review

Regarding the P2 badge about Match host and alias/HostName semantics:

TL;DR: Our implementation correctly matches OpenSSH semantics. No code changes needed.

OpenSSH defines:

  • Match host — evaluated against the target hostname after HostName/CanonicalizeHostname substitution
  • Match originalhost — evaluated against the hostname as specified on the command line (the alias)
  • %h — expands to the resolved HostName
  • %n — expands to the original host argument

So with:

Host myalias
  HostName 10.0.0.5

Running ssh myalias:

  • Match host myalias does not match (because Match host sees 10.0.0.5)
  • Match host 10.0.0.5 matches
  • Match originalhost myalias matches

Our code passes the resolved HostName into applyNegatedExecMatch(), which is correct. Configs that want alias-based matching should use Match originalhost and %n instead of Match host and %h.

Sources: man7.org ssh_config(5), man.openbsd.org ssh_config(5)

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 73a6bea899

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

…d failure

- sshConfigParser: Default %r expansion to local username when no User is
  specified, matching SSH connection behavior
- SSH2Transport: Exit PTY session on cd failure (|| exit 1) to match
  OpenSSH transport behavior (cd ... && exec $SHELL -i)
- Add test for %r expansion with no explicit User
@ethanndickson
Copy link
Member Author

@codex review

Addressed both P2 items:

  1. Default %r to local username — Added getDefaultUsername() helper in sshConfigParser.ts and now pass userOverride ?? userFromConfig ?? getDefaultUsername() into applyNegatedExecMatch() so %r always has a value matching the actual connection user.

  2. Fail SSH2 PTY on cd failure — Changed SSH2Transport.createPtySession() to write cd ${path} || exit 1 instead of just cd ${path}, matching OpenSSH transport behavior.

Added test coverage for the %r fix.

@chatgpt-codex-connector
Copy link

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@ethanndickson
Copy link
Member Author

Tested SSH2 on Windows & Linux, tested OpenSSH on Linux, merging..

@ethanndickson ethanndickson added this pull request to the merge queue Jan 20, 2026
@ethanndickson ethanndickson removed this pull request from the merge queue due to a manual request Jan 20, 2026
…Docker

- Revert temporary Windows build CI overrides (only run on merge-queue/main)
- Revert Windows code signing to only run on main pushes
- Replace --alias:jsonc-parser with --external:jsonc-parser in Dockerfile esbuild
  (matches Makefile ESBUILD_CLI_FLAGS approach, avoids UMD/ESM interop issues)
- Copy jsonc-parser to runtime image alongside @lydell/node-pty
The ssh2 package (new SSH2 runtime) includes optional native .node addons
(cpu-features, sshcrypto) that esbuild cannot bundle. Externalize ssh2 and
copy its runtime dependencies into the Docker image:

- ssh2, asn1, safer-buffer, bcrypt-pbkdf, tweetnacl

Keep jsonc-parser as --alias (ESM version) since it bundles fine — only
ssh2 needs externalization due to native addons.

Also includes the Windows CI revert from the previous commit.
@ethanndickson ethanndickson added this pull request to the merge queue Jan 20, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 20, 2026
@ethanndickson ethanndickson added this pull request to the merge queue Jan 20, 2026
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Jan 20, 2026
SSH2 transport may have flaky behavior in CI. Use OpenSSH transport
(ControlMaster) for CoderSSHRuntime tests unless explicitly testing SSH2.
@ethanndickson ethanndickson added this pull request to the merge queue Jan 20, 2026
@ethanndickson ethanndickson removed this pull request from the merge queue due to a manual request Jan 20, 2026
@ethanndickson ethanndickson merged commit 6380980 into main Jan 20, 2026
22 checks passed
@ethanndickson ethanndickson deleted the ethan/ssh2-runtime branch January 20, 2026 14:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant