agentHost: honor SSH ProxyJump when connecting - #334849
Girish Konda (kondv) wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Host-key alias handling, IPC and artifact cleanup, and localization issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds SSH proxy support to Agent Host connections, including native askpass prompting and secure proxy lifecycle handling.
Changes:
- Supports
ProxyJump,ProxyCommand,ProxyUseFdpass, andHostKeyAlias. - Adds native proxy transports and renderer-mediated authentication prompts.
- Adds parsing, security, cleanup, and reconnection tests.
File summaries
| File | Description |
|---|---|
src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostActions.ts |
Forwards resolved proxy configuration. |
src/vs/platform/agentHost/test/node/sshRemoteAgentHostService.test.ts |
Tests proxy transport, askpass, cleanup, and reconnection. |
src/vs/platform/agentHost/test/electron-browser/sshRemoteAgentHostService.test.ts |
Tests renderer prompt handling. |
src/vs/platform/agentHost/test/common/sshConfigParsing.test.ts |
Tests proxy configuration parsing. |
src/vs/platform/agentHost/node/sshRemoteAgentHostService.ts |
Implements proxy transports and askpass IPC. |
src/vs/platform/agentHost/electron-browser/sshRemoteAgentHostServiceImpl.ts |
Presents proxy authentication prompts. |
src/vs/platform/agentHost/common/sshRemoteAgentHost.ts |
Extends SSH configuration and IPC contracts. |
src/vs/platform/agentHost/common/sshConfigParsing.ts |
Parses effective proxy configuration. |
Review details
Suppressed comments (1)
src/vs/platform/agentHost/node/sshRemoteAgentHostService.ts:1874
- This newly surfaced failure message is also not localized. Use a localized template with placeholders for
proxyKindanddetail, consistent with the other user-facing proxy errors in this method.
socket.destroy(new Error(`${proxyKind} process exited before the SSH connection was established (${detail}).`));
- Files reviewed: 8/8 changed files
- Comments generated: 4
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
8de3d6f to
beffb0b
Compare
📬 CODENOTIFYThe following users are being notified based on files changed in this PR: Ladislau Szomoru (@lszomoru)Matched files:
|
8de3d6f to
dca870d
Compare
dca870d to
16e5021
Compare
|
@microsoft-github-policy-service agree company="Microsoft" |
0a67175 to
9cf4775
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Valid SSH URI jump hosts must be supported, and helper stderr should provide actionable diagnostics.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Balanced
9cf4775 to
ad4a64a
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Three moderate proxy handling and error-reporting issues remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Balanced
ad4a64a to
340725a
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Helper cleanup gaps can leak processes, while eager close handling can discard the diagnostic stderr the new path intends to preserve.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Balanced
340725a to
8725561
Compare
d2da7c8 to
2ae55a6
Compare
2ae55a6 to
6828d89
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Bracketed IPv6 jump hosts are passed to OpenSSH with brackets intact, causing hostname resolution failure.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 1
- Review effort level: Balanced
6828d89 to
e346d4e
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Cross-platform subprocess and stream lifecycle behavior warrants final human review despite the extensive regression coverage.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Balanced
e346d4e to
66ddb39
Compare
There was a problem hiding this comment.
🟢 Approval recommended
The implementation is narrowly scoped and comprehensively covers configuration propagation, transport behavior, failure reporting, and lifecycle cleanup.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Balanced
66ddb39 to
a048931
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
Native process lifecycle and cross-platform OpenSSH behavior warrant final human review despite extensive regression coverage.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Balanced
There was a problem hiding this comment.
🟢 Approval recommended
The implementation is focused, defensively handles proxy lifecycle and diagnostics, and includes comprehensive regression coverage.
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Fixes #317445
Problem
Agent Host resolves effective OpenSSH configuration with
ssh -G, but did not carryProxyJumpinto thessh2connection. A host reachable only through its configured jump host was therefore dialed directly from the Agents window and failed withENOTFOUNDorEHOSTUNREACH, while native OpenSSH and Remote SSH succeeded against the same config.Change
Preserve the resolved
ProxyJumpvalue through initial and reconnect configuration. For a single jump host, start native OpenSSH withBatchMode=yesand-W, pass its standard streams tossh2throughConnectConfig.sock, and stop the owned helper on connection teardown. The destination is passed after--so a config-derived value can never be read as ansshoption.ssh -GreportsProxyJumpwith its percent tokens unexpanded, so%%,%h,%n,%pand%rare expanded here. Expansion happens per component after the value is split, because a token standing in for an IPv6 address would otherwise be mistaken for a port separator —%h:24321against an IPv6HostNamebecomes::1:24321. Thessh://URI form is accepted directly as well, thoughssh -Gnormalizes it away.A bracketed IPv6 jump host is passed to
sshbare while the-Wtarget keeps its brackets, matching whatssh -Jitself emits: glibc and Darwingetaddrinforeject[...]for a numeric IPv6 destination, so forwarding the brackets fails name resolution everywhere except Windows.The helper's stderr is the only place DNS, host-key and authentication failures for the jump host are reported; without it they all collapse into
ssh2's generic "Connection lost before handshake". A bounded tail is captured and attached to the surfaced error. It is attached by annotating the existing error rather than replacing it, so anSSHHostKeyDeniedErrorkeeps thenamethe renderer dispatches on. That annotation is the single place it is added — the helper's own exit error carries the code and signal only — so a jump-host failure does not repeat the same OpenSSH diagnostic twice in the message the user sees.The jump alias, not a pre-resolved hostname, is handed to OpenSSH, so the jump host's own
HostName,User,PortandIdentityFilestill apply. Final-host authentication, host-key verification, Agent Host bootstrap and WebSocket handling are untouched.Single-hop only. Comma-separated chains,
ProxyCommand, interactive jump-host authentication, askpass IPC andHostKeyAliasbehavior are unchanged, and there are no dependency, build or pipeline changes.Validation
npm run compile-client,typecheck-client,valid-layers-check, fulleslint, and hygiene on the changed files — all cleanvs/platform/agentHost/test/{common,node}— 6787 passing, 0 failing;remoteAgentHost/test— 280 passing. An earlier revision of this section reported threeCopilotAgentstate-file failures as pre-existing. They were not.getCopilotHomePath()prefers an ambientCOPILOT_HOMEover the injecteduserHome, so the value set in my shell escaped the test's sandbox — and failed on unpatchedmainfor the same reason, which is what made it look pre-existing. Cleared, the suite is green.ProxyJumpmapping in the configured-host action, the token expansion, the expand-after-split ordering, theclose-instead-of-exitread, the annotate-in-place error handling, theconnectedgate on helper disposal, the bounded stderr wait, the single attachment of the helper diagnostic, or the IPv6 bracket strip each fails a specific regression test, and the suite returns to green when restored.dca870d(base7b763e653ee2), using disposable profiles, real key-authenticated jump and target SSH servers, and the actual Agent Host WebSocket relay:proxyjump-target.invalid:2222through the configured jump host.mainused the same Agents UI action, dialed the target directly, failed withgetaddrinfo ENOTFOUND, and never contacted the jump or target servers.--terminator was added after that run, so this revision is not byte-identical to the Electron-tested one and the Electron harness was not re-run. It is covered separately: against OpenSSH 9.5p2,ssh -o BatchMode=yes -W target.invalid:22 -Vprints the version and exits 0, while the same command with-- -Vtreats it as a hostname and exits 255; ordinaryhostanduser@hostdestinations are unaffected. A unit regression asserts--is always immediately before the destination, and it fails without the production change.ssh -Greturnsproxyjump %n-bastionandproxyjump bastion-%hliterally, unexpanded.ssh2rejects on the stdout EOF is covered directly against the proxy transport: one test asserts it does not settle before the helper's stdio closes and that the diagnostic is complete afterwards, another asserts it gives up rather than hanging when the helper never closes. MakingwhenStderrSettled()resolve immediately fails the first.d2da7c8d(this branch before the stderr de-duplication): 176 passing / 1 failing, then 175 / 2; merge-base7b7e49c8176 / 1 both times. Every failure on both sides is the sameTimeout waiting for response to vscode/collectAgentHostDebugLogs (id=6, 8000ms), attributed by mocha to whichever test is running when it lands, which is why the named test moves between runs. The E2E server's import closure is 679 modules and contains none of the files this PR changes.disposeProxyTransport(sshClient)calls added to the relay-replacement timeout catch and the initial-connect catch. That suite's harness wires up no proxy transport, so mutating either line passes for the wrong reason instead of failing. The call is idempotent — aWeakMapdelete plusdispose()'s own guard — so the repeated-path case is safe.