Skip to content

Conversation

@andya1lan
Copy link

#2643

Implementation

  1. Add platform-aware identity-agent dialing: on Windows prefer SSH_AUTH_SOCK, otherwise fall back to the default OpenSSH agent pipe //./pipe/openssh-ssh-agent.
  2. Keep Unix agent dialing via Unix sockets; unify agent resolution across platforms.
  3. Add cross-platform unit tests covering dialer selection and failure paths.

Copilot AI review requested due to automatic review settings December 7, 2025 09:06
@CLAassistant
Copy link

CLAassistant commented Dec 7, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 7, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This PR adds platform-specific SSH agent dialers: a Windows named-pipe implementation (pkg/remote/sshagent_windows.go) and a Unix domain-socket implementation (pkg/remote/sshagent_unix.go) with corresponding tests. sshclient.go is updated to prefer SSH_AUTH_SOCK from the environment, fall back to a Windows named pipe default or the existing shell-based discovery, and to use the new dialIdentityAgent(agentPath) call with improved logging. connparse.go is refactored to better handle leading // shorthand, WSL/WSH forms, trailing-slash preservation, and centralized preceding-slash logic. go.mod adds github.com/Microsoft/go-winio v0.6.2. Documentation adds an "SSH Agent Detection" section in two places. No public API signatures changed.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

  • connparse.go — substantial parsing refactor; verify all URI branches, WSL/WSH handling, and trailing-slash semantics.
  • sshclient.go — cross-platform agent discovery and error/logging changes; validate fallback ordering and Windows default.
  • sshagent_windows.go / sshagent_unix.go and tests — platform-specific dialing behavior, timeouts, build tags, and resource cleanup.
  • pkg/remote tests — ensure unit tests are robust and correctly gated by build tags.
  • go.mod — confirm go-winio usage/version matches code.
  • docs — duplicate "SSH Agent Detection" content appears in two locations; confirm intended duplication.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: implement SSH agent for Windows with tests' accurately describes the main changes: adding Windows SSH agent support with corresponding unit tests.
Description check ✅ Passed The description clearly relates to the changeset, explaining the platform-aware identity-agent dialing implementation for Windows and Unix with cross-platform tests.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 47a5dfd and 49e90d0.

📒 Files selected for processing (1)
  • pkg/remote/sshclient.go (4 hunks)
🔇 Additional comments (1)
pkg/remote/sshclient.go (1)

616-625: LGTM! Clean refactor for platform-specific agent dialing.

The changes properly abstract platform-specific SSH agent dialing through dialIdentityAgent, add path trimming to handle whitespace, and improve error messages by including the attempted agent path. This implementation supports the Windows named pipe dialing introduced in the platform-specific files.


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
pkg/remote/connparse/connparse.go (2)

100-110: Redundant assignment of rest when URI starts with //.

When uri starts with "//", line 101 sets rest = strings.TrimPrefix(uri, "//"). Then later at lines 141-142, when scheme == "" and strings.HasPrefix(uri, "//") is true, rest is assigned again with the identical value. This is redundant.

Also, line 106's TrimPrefix(split[1], "//") seems unusual—after splitting on "://", the remainder shouldn't typically start with "//". Could you clarify the use case this handles?

 	if strings.HasPrefix(uri, "//") {
 		rest = strings.TrimPrefix(uri, "//")
 	} else {
 		split := strings.SplitN(uri, "://", 2)
 		if len(split) > 1 {
 			scheme = split[0]
-			rest = strings.TrimPrefix(split[1], "//")
+			rest = split[1]
 		} else {
 			rest = split[0]
 		}
 	}

162-166: Consider extracting the complex path condition into a helper for readability.

The condition on line 164 is difficult to parse due to its length. Extracting this to a named helper function would improve maintainability.

+// needsPrecedingSlash returns true if the path needs a "/" prefix
+func needsPrecedingSlash(path string) bool {
+	if len(path) <= 1 {
+		return false
+	}
+	if windowsDriveRegex.MatchString(path) {
+		return false
+	}
+	prefixes := []string{"/", "~", "./", "../", ".\\", "..\\"}
+	for _, p := range prefixes {
+		if strings.HasPrefix(path, p) {
+			return false
+		}
+	}
+	return path != ".."
+}

 	if scheme == ConnectionTypeWsh {
 		if host == "" {
 			host = wshrpc.LocalConnName
 		}
 		if strings.HasPrefix(remotePath, "/~") {
 			remotePath = strings.TrimPrefix(remotePath, "/")
-		} else if addPrecedingSlash && (len(remotePath) > 1 && !windowsDriveRegex.MatchString(remotePath) && !strings.HasPrefix(remotePath, "/") && !strings.HasPrefix(remotePath, "~") && !strings.HasPrefix(remotePath, "./") && !strings.HasPrefix(remotePath, "../") && !strings.HasPrefix(remotePath, ".\\") && !strings.HasPrefix(remotePath, "..\\") && remotePath != "..") {
+		} else if addPrecedingSlash && needsPrecedingSlash(remotePath) {
 			remotePath = "/" + remotePath
 		}
 	}
pkg/remote/sshagent_windows_test.go (1)

10-19: LGTM! Consider adding error type verification for robustness.

The timeout test is well-structured. For more precise validation, you could additionally check that the error indicates a connection failure (e.g., os.IsTimeout or checking for specific error messages) rather than just any error.

 func TestDialIdentityAgentWindowsTimeout(t *testing.T) {
 	start := time.Now()
 	_, err := dialIdentityAgent(`\\.\\pipe\\waveterm-nonexistent-agent`)
 	if err == nil {
 		t.Skip("unexpectedly connected to a test pipe; skipping")
 	}
+	// Optionally verify error indicates connection/timeout failure
+	t.Logf("dialIdentityAgent returned expected error: %v", err)
 	if time.Since(start) > 3*time.Second {
 		t.Fatalf("dialIdentityAgent exceeded expected timeout window")
 	}
 }
pkg/remote/sshclient.go (1)

905-926: Good platform-aware SSH agent resolution.

The priority order (env var → Windows default → shell fallback) is sensible. The Windows pipe path \\.\pipe\openssh-ssh-agent is the standard OpenSSH agent location.

Minor edge case: If the shell command succeeds but SSH_AUTH_SOCK is unset/empty in the shell environment, ExpandHomeDir("") returns ".", causing the agent path to be set to the current directory. This would fail gracefully at dial time, but you could add an emptiness check to avoid the unnecessary dial attempt:

 		} else {
 			shellPath := shellutil.DetectLocalShellPath()
 			authSockCommand := exec.Command(shellPath, "-c", "echo ${SSH_AUTH_SOCK}")
 			sshAuthSock, err := authSockCommand.Output()
-			if err == nil {
-				agentPath, err := wavebase.ExpandHomeDir(trimquotes.TryTrimQuotes(strings.TrimSpace(string(sshAuthSock))))
-				if err != nil {
-					return nil, err
+			if err == nil {
+				trimmedSock := strings.TrimSpace(string(sshAuthSock))
+				if trimmedSock != "" {
+					agentPath, err := wavebase.ExpandHomeDir(trimquotes.TryTrimQuotes(trimmedSock))
+					if err != nil {
+						return nil, err
+					}
+					sshKeywords.SshIdentityAgent = utilfn.Ptr(agentPath)
+				} else {
+					log.Printf("SSH_AUTH_SOCK is empty in shell environment\n")
 				}
-				sshKeywords.SshIdentityAgent = utilfn.Ptr(agentPath)
 			} else {
 				log.Printf("unable to find SSH_AUTH_SOCK: %v\n", err)
 			}
 		}
pkg/remote/sshagent_windows.go (1)

12-16: Clean Windows named pipe implementation using go-winio.

The dialIdentityAgent function correctly handles Windows OpenSSH agent connections via named pipes. The 2-second timeout is reasonable for local agent connections, and the function signature matches the Unix counterpart for cross-platform compatibility.

Ensure the github.com/Microsoft/go-winio dependency is properly declared in go.mod.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 323db7f and 4b12962.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • docs/docs/connections.mdx (1 hunks)
  • go.mod (1 hunks)
  • pkg/remote/connparse/connparse.go (2 hunks)
  • pkg/remote/sshagent_unix.go (1 hunks)
  • pkg/remote/sshagent_unix_test.go (1 hunks)
  • pkg/remote/sshagent_windows.go (1 hunks)
  • pkg/remote/sshagent_windows_test.go (1 hunks)
  • pkg/remote/sshclient.go (4 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-02-20T00:10:31.048Z
Learnt from: esimkowitz
Repo: wavetermdev/waveterm PR: 1998
File: go.mod:37-37
Timestamp: 2025-02-20T00:10:31.048Z
Learning: The golang.org/x/sync package should remain at version v0.11.0 in go.mod, as explicitly confirmed by the maintainer.

Applied to files:

  • go.mod
🧬 Code graph analysis (1)
pkg/remote/sshclient.go (4)
pkg/util/utilfn/utilfn.go (2)
  • SafeDeref (941-947)
  • Ptr (955-957)
pkg/wavebase/wavebase.go (1)
  • ExpandHomeDir (149-163)
pkg/trimquotes/trimquotes.go (1)
  • TryTrimQuotes (21-24)
pkg/util/shellutil/shellutil.go (1)
  • DetectLocalShellPath (83-101)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: CodeQL analysis (go)
  • GitHub Check: merge-gatekeeper
🔇 Additional comments (6)
docs/docs/connections.mdx (1)

170-177: LGTM! Clear documentation for the new SSH agent detection feature.

The resolution order is well-documented and provides actionable guidance for Windows users regarding the OpenSSH Authentication Agent service.

pkg/remote/connparse/connparse.go (1)

115-134: LGTM!

The parseGenericPath and parseWshPath helpers are well-structured. The WSL regex handling correctly extracts the distribution name while preserving the path.

pkg/remote/sshagent_unix.go (1)

7-10: Consider adding a timeout for consistency with Windows implementation.

The Unix dialer has no timeout, while the Windows implementation reportedly uses a 2-second timeout. Unix domain socket connections are typically fast, but adding a timeout could prevent hangs if the socket exists but the agent is unresponsive. This should be verified against the actual Windows implementation to ensure consistency across platforms.

go.mod (1)

6-6: go-winio v0.6.2 is the latest stable version.

The dependency is current and well-chosen for Windows named pipe support.

pkg/remote/sshagent_unix_test.go (1)

11-35: LGTM!

The test correctly validates the Unix socket dialing path. Good use of t.TempDir() for automatic cleanup, proper goroutine synchronization with the channel, and appropriate defer for the listener.

pkg/remote/sshclient.go (1)

616-625: LGTM!

The refactored agent dialing logic correctly uses the platform-specific dialIdentityAgent function. Good improvement to include the agent path in the error log for debugging. The graceful fallback (logging error but continuing) allows authentication to proceed with other methods if the agent is unavailable.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements cross-platform SSH agent support for Windows by adding named pipe connectivity alongside existing Unix domain socket support. The implementation introduces platform-specific dialer functions and prioritizes SSH_AUTH_SOCK environment variable detection across all platforms.

Key changes:

  • Platform-aware SSH agent dialing: Windows uses named pipes via go-winio, Unix systems use domain sockets
  • Unified agent path resolution: prioritizes SSH_AUTH_SOCK, falls back to Windows OpenSSH pipe or Unix shell detection
  • Cross-platform unit tests covering dialer functionality and timeout behavior

Reviewed changes

Copilot reviewed 8 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
pkg/remote/sshclient.go Refactors agent path detection to check SSH_AUTH_SOCK first, adds Windows-specific fallback to OpenSSH named pipe, improves error logging with agent path context
pkg/remote/sshagent_windows.go Implements Windows SSH agent connection via named pipes with 2-second timeout using go-winio library
pkg/remote/sshagent_windows_test.go Tests Windows pipe dialing with timeout verification for non-existent agent
pkg/remote/sshagent_unix.go Implements Unix SSH agent connection via domain sockets
pkg/remote/sshagent_unix_test.go Tests Unix socket dialing with mock listener to verify successful connection
pkg/remote/connparse/connparse.go Refactors URI parsing to handle "//" prefix before scheme splitting, renames variable from split to parts for clarity
go.mod Adds github.com/Microsoft/go-winio v0.6.2 dependency for Windows named pipe support
go.sum Adds checksums for go-winio dependency
docs/docs/connections.mdx Documents SSH agent detection priority and platform-specific behavior for Windows, macOS, and Linux

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@andya1lan
Copy link
Author

@coderabbitai generate docstrings

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 7, 2025

✅ Actions performed

Initiated docstring generation; will generate only if new commits exist.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 7, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #2645

coderabbitai bot added a commit that referenced this pull request Dec 7, 2025
Docstrings generation was requested by @andya1lan.

* #2644 (comment)

The following files were modified:

* `pkg/remote/connparse/connparse.go`
* `pkg/remote/sshagent_windows.go`
* `pkg/remote/sshclient.go`
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
pkg/remote/sshagent_windows_test.go (1)

10-21: Well-structured timeout test.

The test clearly validates that dialing a non-existent Windows named pipe completes within the expected timeout window. The 3-second threshold provides reasonable buffer over the 2-second implementation timeout.

Optionally, consider verifying the error type or message to ensure it's a connection/timeout error rather than an unexpected error type:

 func TestDialIdentityAgentWindowsTimeout(t *testing.T) {
 	start := time.Now()
 	_, err := dialIdentityAgent(`\\.\\pipe\\waveterm-nonexistent-agent`)
 	if err == nil {
 		t.Skip("unexpectedly connected to a test pipe; skipping")
 	}
-	// Optionally verify error indicates connection/timeout failure
 	t.Logf("dialIdentityAgent returned expected error: %v", err)
+	// Verify it's a connection-related error (not a panic or programming error)
+	if err.Error() == "" {
+		t.Fatalf("error has empty message: %v", err)
+	}
 	if time.Since(start) > 3*time.Second {
 		t.Fatalf("dialIdentityAgent exceeded expected timeout window")
 	}
 }
pkg/remote/connparse/connparse.go (1)

28-42: Clarify and possibly extend needsPrecedingSlash for Windows UNC-style paths

The helper looks good and nicely centralizes the old conditional logic, including Windows drive letters and relative/tilde forms. One possible gap is UNC-style Windows paths like \\server\share: for wsh://host/\\server\share this helper would currently return true and cause /\\server\share, which is probably not desired if UNC paths are meant to be supported as-is.

If UNC-style paths should be treated as already “absolute”, consider adding a "\\" prefix to the disallowed list:

-	disallowedPrefixes := []string{"/", "~", "./", "../", ".\\", "..\\"}
+	disallowedPrefixes := []string{"/", "~", "./", "../", ".\\", "..\\", "\\\\"}

If UNC paths are out of scope for WSH URIs, it might still be worth documenting that assumption in a comment to avoid future regressions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b12962 and 47a5dfd.

📒 Files selected for processing (2)
  • pkg/remote/connparse/connparse.go (4 hunks)
  • pkg/remote/sshagent_windows_test.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-08-13T05:34:23.892Z
Learnt from: L1l1thLY
Repo: wavetermdev/waveterm PR: 2249
File: pkg/waveai/anthropicbackend.go:60-62
Timestamp: 2025-08-13T05:34:23.892Z
Learning: Go's path.Join function does NOT treat arguments with leading slashes as absolute paths that discard previous arguments. path.Join(currentPath, "/v1/messages") correctly preserves currentPath and appends the segments, e.g., path.Join("/api", "/v1/messages") returns "/api/v1/messages".

Applied to files:

  • pkg/remote/connparse/connparse.go
🔇 Additional comments (2)
pkg/remote/connparse/connparse.go (2)

116-126: New leading-// and WSH/WSL parsing paths look consistent; consider a couple of edge-case tests

The refactor to:

  • treat leading // as remote shorthand (defaulting scheme to wsh),
  • route //wsl://distro/path and wsh://wsl://distro/path through parseWshPath, and
  • use parseGenericPath everywhere else,

is coherent and keeps the control flow readable.

A few edge cases would be good to explicitly lock in via tests to avoid surprises:

  • URIs with extra slashes, e.g. ///host/path and //wsl://distro (no trailing /path).
  • Plain wsl://distro/path (no wsh:// and no leading //), which now becomes Scheme: "wsl", Host: "distro", Path: "path" via parseGenericPath. Confirm this is intended and doesn’t need WSH-specific handling.
  • Bare // or //host with and without a trailing slash: confirm the desired host/path split and resulting Connection values.

The implementation itself looks correct; this is mostly about ensuring the new behaviours are captured in tests and match the desired UX.

Also applies to: 131-150, 152-167


173-181: Interaction between addPrecedingSlash and needsPrecedingSlash for WSH URIs

The final WSH-specific normalization:

  • Defaults empty host to wshrpc.LocalConnName.
  • Normalizes /~ to ~.
  • Uses addPrecedingSlash && needsPrecedingSlash(remotePath) to decide whether to prefix /.

This cleanly separates:

  • Explicit wsh://... URIs (where addPrecedingSlash stays true) from
  • Shorthand / no-scheme URIs (where addPrecedingSlash is forced false to preserve user-supplied paths like ~/foo, ./foo, Windows drives, etc.).

Given needsPrecedingSlash already guards empty/one-char paths and all the “special” prefixes, this looks safe and resolves the previous ad-hoc condition nicely.

I’d just recommend adding a couple of focused tests around:

  • wsh://host/foo/foo
  • wsh://host/C:/pathC:/path (no leading /)
  • foo / ~/foo / C:\foo (no scheme) remaining unmodified in Path.

@sawka
Copy link
Member

sawka commented Dec 9, 2025

@andya1lan thanks for submitting this! will take a look and try to get it merged

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.

3 participants