Skip to content

Tear down SSH tunnel when removing a remote#311992

Merged
roblourens merged 2 commits intomainfrom
roblou/agents/ssh-remote-disconnect-issue
Apr 22, 2026
Merged

Tear down SSH tunnel when removing a remote#311992
roblourens merged 2 commits intomainfrom
roblou/agents/ssh-remote-disconnect-issue

Conversation

@roblourens
Copy link
Copy Markdown
Member

Removing an SSH-backed agent host previously only disposed the renderer-side protocol client and SSHRelayTransport. SSHRelayTransport.dispose() removed the IPC listeners but did not tell the shared-process side to close the SSH tunnel, so the tunnel leaked and the next connect silently reused it.

Fix

Give IRemoteAgentHostService a generic ownership hook for transport-level teardown:

  • Rename addSSHConnectionaddManagedConnection and add an optional transportDisposable parameter. The platform service registers it on the same per-entry DisposableStore as the protocol client, so it runs on removeRemoteAgentHost, on config-driven reconciliation, and on service disposal.
  • SSHRemoteAgentHostService passes a transport disposable that synchronously drops the renderer-side handle from its _connections map, fires the change event, marks the handle closed, disposes it, then best-effort tells the shared-process service to disconnect(). The early return existing branches in connect() / reconnect() are now safe across remove → reconnect because the map is cleared synchronously.
  • Web and desktop tunnel renderers just pick up the rename. Their existing transports (TunnelConnectionTransport.dispose, TunnelRelayTransport.dispose) already close the underlying connection, so no behavior change there.

Tests

  • New sshRemoteAgentHostService.test.ts covers the renderer lifecycle: connect registers a transport disposable, removal tears down both the renderer handle and the shared-process tunnel, reconnect after removal does not reuse the old handle, and main-process close cleans up without double-disconnecting.
  • Extended remoteAgentHostService.test.ts proves the transportDisposable runs on entry removal, replacement, and service dispose.
  • Full **/agentHost/**/*.test.js suite passes (714 / 1 pending / 0 failing).

(Written by Copilot)

Removing an SSH-backed agent host previously only disposed the renderer-side
protocol client and SSHRelayTransport. SSHRelayTransport.dispose() removed
the IPC listeners but did not tell the shared-process side to close the SSH
tunnel, so the tunnel leaked and the next connect silently reused it.

Fix this by giving IRemoteAgentHostService a generic ownership hook for
transport-level teardown:

- Rename addSSHConnection -> addManagedConnection and add an optional
  transportDisposable parameter. The platform service registers it on the
  same per-entry DisposableStore as the protocol client, so it runs on
  removeRemoteAgentHost, on config-driven reconciliation, and on service
  disposal.
- SSHRemoteAgentHostService passes a transport disposable that
  synchronously drops the renderer-side handle from its connections map,
  fires the change event, marks the handle closed, disposes it, then
  best-effort tells the shared-process service to disconnect. The early
  'return existing' branches in connect()/reconnect() are now safe across
  remove -> reconnect because the map is cleared synchronously.
- Web/desktop tunnel renderers just pick up the rename. Their existing
  transports (TunnelConnectionTransport.dispose, TunnelRelayTransport.dispose)
  already close the underlying connection, so no behavior change there.

Adds renderer-side lifecycle tests covering connect/remove/reconnect and
main-side close, plus platform-side tests proving the transport disposable
runs on entry removal, replacement, and service dispose.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 22, 2026 20:04
Copy link
Copy Markdown
Contributor

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 ensures SSH-backed remote agent host tunnels are properly torn down when a remote is removed by introducing a transport-level teardown hook owned by IRemoteAgentHostService, and wiring SSH connections to use it.

Changes:

  • Renames IRemoteAgentHostService.addSSHConnection to addManagedConnection and adds an optional transportDisposable that is disposed with the entry lifecycle (remove/reconcile/service dispose).
  • Updates SSH and tunnel/web-tunnel connection registration to use addManagedConnection; SSH now supplies a transport disposable that drops renderer handles and best-effort disconnects the shared-process tunnel.
  • Adds/extends unit tests to validate transport teardown on removal/replacement/service disposal and SSH lifecycle behavior (remove → reconnect, main close cleanup).
Show a summary per file
File Description
src/vs/sessions/contrib/remoteAgentHost/electron-browser/tunnelAgentHostServiceImpl.ts Updates tunnel-backed connection registration to call addManagedConnection.
src/vs/sessions/contrib/remoteAgentHost/browser/webTunnelAgentHostService.ts Updates web tunnel-backed connection registration to call addManagedConnection.
src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostActions.ts Updates inline comment to reflect the renamed API (addManagedConnection).
src/vs/platform/agentHost/test/electron-browser/sshRemoteAgentHostService.test.ts Adds renderer-side SSH service tests covering teardown and reconnect behavior.
src/vs/platform/agentHost/test/electron-browser/remoteAgentHostService.test.ts Extends tests to verify transportDisposable disposal on remove/replace/service dispose.
src/vs/platform/agentHost/electron-browser/sshRemoteAgentHostServiceImpl.ts Implements SSH transport teardown via transportDisposable and switches to addManagedConnection.
src/vs/platform/agentHost/common/remoteAgentHostService.ts Updates service contract: rename to addManagedConnection and document transportDisposable semantics.
src/vs/platform/agentHost/browser/remoteAgentHostServiceImpl.ts Implements addManagedConnection and ensures transport disposables are owned by per-entry stores.

Copilot's findings

  • Files reviewed: 8/8 changed files
  • Comments generated: 3

Comment thread src/vs/platform/agentHost/test/electron-browser/sshRemoteAgentHostService.test.ts Outdated
Comment thread src/vs/platform/agentHost/electron-browser/sshRemoteAgentHostServiceImpl.ts Outdated
Comment thread src/vs/platform/agentHost/electron-browser/sshRemoteAgentHostServiceImpl.ts Outdated
- Share a _setupConnection helper between connect() and reconnect() that
  wraps the full setup (handshake, handle creation, registration with
  IRemoteAgentHostService) in a single try/catch. On any failure after the
  shared-process tunnel is established, drop the renderer-side handle, fire
  the change event, dispose the protocol client, and best-effort tell main
  to  so we don't leak the SSH tunnel if registration throws.disconnect
  Previously reconnect() had no error handling at all.
- Replace the polling-based awaitClientThenResolve helper in the renderer
  test with a deterministic DeferredPromise that resolves the moment the
  Nth protocol client is created, removing potential CI flakiness on slow
  machines.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@roblourens roblourens marked this pull request as ready for review April 22, 2026 20:55
@roblourens roblourens enabled auto-merge (squash) April 22, 2026 20:55
@roblourens roblourens merged commit 1f9cd94 into main Apr 22, 2026
26 checks passed
@roblourens roblourens deleted the roblou/agents/ssh-remote-disconnect-issue branch April 22, 2026 21:44
@vs-code-engineering vs-code-engineering Bot added this to the 1.118.0 milestone Apr 22, 2026
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