1
>
/*---------------------------------------------------------------------------------------------
sshRemoteAgentHost.ts
2
>
* Copyright (c) Microsoft Corporation. All rights reserved.
3
>
* Licensed under the MIT License. See License.txt in the project root for license information.
4
>
*--------------------------------------------------------------------------------------------*/
5
>
6
>
import { Event } from '../../../base/common/event.js';
7
>
import { IDisposable } from '../../../base/common/lifecycle.js';
8
>
import { URI } from '../../../base/common/uri.js';
9
>
import { createDecorator } from '../../instantiation/common/instantiation.js';
10
>
import type { IRelayMessage } from './relayTransport.js';
11
>
12
>
export type { IRelayMessage } from './relayTransport.js';
13
>
14
>
export const ISSHRemoteAgentHostService = createDecorator<ISSHRemoteAgentHostService>('sshRemoteAgentHostService');
15
>
16
>
/**
17
>
* IPC channel name for the main-process SSH service.
18
>
*/
19
>
export const SSH_REMOTE_AGENT_HOST_CHANNEL = 'sshRemoteAgentHost';
20
>
21
>
export const enum SSHAuthMethod {
22
>
/** Use the local SSH agent for key-based auth. */
23
>
Agent = 'agent',
24
>
/** Authenticate with an explicit private key file. */
25
>
KeyFile = 'keyFile',
26
>
/** Authenticate with a password. */
27
>
Password = 'password',
28
>
}
29
>
30
>
export interface ISSHAgentHostConfig {
31
>
/** Remote hostname or IP. */
32
>
readonly host: string;
33
>
/** SSH port (default 22). */
34
>
readonly port?: number;
35
>
/** Username on the remote machine. */
36
>
readonly username: string;
37
>
/** Authentication method. */
38
>
readonly authMethod: SSHAuthMethod;
39
>
/** Path to the private key file (when {@link authMethod} is KeyFile). */
40
>
readonly privateKeyPath?: string;
41
>
/** Raw IdentityAgent value from resolved SSH config; may be a socket path, `none`, `SSH_AUTH_SOCK`, or an environment reference. */
42
>
readonly identityAgent?: string;
43
>
/** Password string (when {@link authMethod} is Password). */
44
>
readonly password?: string;
45
>
/** Display name for this connection. */
46
>
readonly name: string;
47
>
/** SSH config host alias (e.g. "robfast2") for reconnection on restart. */
48
>
readonly sshConfigHost?: string;
49
>
/** Dev override: custom command to start the remote agent host instead of the default CLI. */
50
>
readonly remoteAgentHostCommand?: string;
51
>
/** When true, enables OpenSSH agent forwarding ([email protected]) for this connection. Requires {@link authMethod} to be Agent. */
52
>
readonly agentForward?: boolean;
53
>
}
54
>
55
>
/**
56
>
* A sanitized view of the SSH config that omits secret material
57
>
* (password, private key path). Exposed on active connections so
58
>
* consumers can inspect connection metadata without accessing credentials.
59
>
*/
60
>
export type ISSHAgentHostConfigSanitized = Omit<ISSHAgentHostConfig, 'password' | 'privateKeyPath'>;
61
>
62
>
export interface ISSHAgentHostConnection extends IDisposable {
63
>
/** The SSH config used to establish this connection (secrets stripped). */
64
>
readonly config: ISSHAgentHostConfigSanitized;
65
>
/** The connection address (e.g. `ssh:myhost` or `user@host:22`) registered with IRemoteAgentHostService. */
66
>
readonly localAddress: string;
67
>
/** The display name. */
68
>
readonly name: string;
69
>
/** Fires when this SSH connection is closed or lost. */
70
>
readonly onDidClose: Event<void>;
71
>
}
72
>
73
>
/**
74
>
* Manages SSH connections that bootstrap a remote agent host process.
75
>
*
76
>
* Each connection SSHs into a remote machine, ensures the VS Code CLI
77
>
* is installed, starts `code agent-host`, and creates a WebSocket relay
78
>
* over the SSH channel. Messages are forwarded between the renderer and
79
>
* the remote agent host via IPC through the shared process.
80
>
*/
81
>
export interface ISSHRemoteAgentHostService {
82
>
readonly _serviceBrand: undefined;
83
>
84
>
/** Fires when the set of active SSH connections changes. */
85
>
readonly onDidChangeConnections: Event<void>;
86
>
87
>
/** Progress messages during connect. */
88
>
readonly onDidReportConnectProgress: Event<ISSHConnectProgress>;
89
>
90
>
/** Currently active SSH-bootstrapped connections. */
91
>
readonly connections: readonly ISSHAgentHostConnection[];
92
>
93
>
/**
94
>
* Bootstrap a remote agent host over SSH.
95
>
*
96
>
* 1. Opens an SSH connection to the remote host
97
>
* 2. Downloads and installs the VS Code CLI if needed
98
>
* 3. Starts `code agent-host`
99
>
* 4. Creates a WebSocket relay over the SSH channel
100
>
* 5. Registers the connection with {@link IRemoteAgentHostService}
101
>
*
102
>
* Resolves with the connection handle once the agent host is reachable.
103
>
*/
104
>
connect(config: ISSHAgentHostConfig): Promise<ISSHAgentHostConnection>;
105
>
106
>
/**
107
>
* Disconnect an SSH-bootstrapped connection by host address.
108
>
* Tears down the SSH tunnel, stops the remote agent host, and
109
>
* removes the entry from {@link IRemoteAgentHostService}.
110
>
*/
111
>
disconnect(host: string): Promise<void>;
112
>
113
>
/** List SSH config host aliases (excluding wildcards). */
114
>
listSSHConfigHosts(): Promise<string[]>;
115
>
116
>
/**
117
>
* Ensure `~/.ssh/config` exists (creating it with the right permissions if
118
>
* missing) and return its URI. The parent `~/.ssh` directory is created
119
>
* with mode 0700 and the config file with mode 0600 on POSIX systems.
120
>
*/
121
>
ensureUserSSHConfig(): Promise<URI>;
122
>
123
>
/**
124
>
* List the known SSH configuration file URIs in priority order — typically the
125
>
* per-user `~/.ssh/config` (always returned, even if it does not yet exist) and
126
>
* the system-wide `/etc/ssh/ssh_config` (only when present on disk).
127
>
*/
128
>
listSSHConfigFiles(): Promise<URI[]>;
129
>
130
>
/** Resolve full SSH config for a host via `ssh -G`. */
131
>
resolveSSHConfig(host: string): Promise<ISSHResolvedConfig>;
132
>
133
>
/**
134
>
* Re-establish an SSH tunnel on startup for a previously connected host.
135
>
* Returns the new local forwarded address and registers it.
136
>
*/
137
>
reconnect(sshConfigHost: string, name: string): Promise<ISSHAgentHostConnection>;
138
>
}
139
>
/**
140
>
* Serializable result from a successful SSH connect operation.
141
>
* Returned over IPC from the main process.
142
>
*/
143
>
export interface ISSHConnectResult {
144
>
/** Unique identifier for this connection's relay channel. */
145
>
readonly connectionId: string;
146
>
/** Display-friendly address (e.g. "ssh:robfast2"). */
147
>
readonly address: string;
148
>
readonly name: string;
149
>
readonly connectionToken: string | undefined;
150
>
readonly config: ISSHAgentHostConfigSanitized;
151
>
/** SSH config host alias for reconnection on restart. */
152
>
readonly sshConfigHost?: string;
153
>
}
154
>
155
>
/**
156
>
* Resolved SSH configuration for a host, obtained from `ssh -G`.
157
>
*/
158
>
export interface ISSHResolvedConfig {
159
>
readonly hostname: string;
160
>
readonly user: string | undefined;
161
>
readonly port: number;
162
>
readonly identityFile: string[];
163
>
readonly identityAgent: string | undefined;
164
>
readonly forwardAgent: boolean;
165
>
}
166
>
167
>
export interface ISSHConnectProgress {
168
>
readonly connectionKey: string;
169
>
readonly message: string;
170
>
}
171
>
172
>
/**
173
>
* A single prompt within a keyboard-interactive authentication request.
174
>
* Mirrors the shape ssh2 hands us — `echo: false` means the user input
175
>
* should be hidden (typically a password).
176
>
*/
177
>
export interface ISSHKeyboardInteractivePrompt {
178
>
readonly prompt: string;
179
>
readonly echo: boolean;
180
>
}
181
>
182
>
/**
183
>
* Request from the main process for the renderer to gather responses to
184
>
* a keyboard-interactive auth challenge from the SSH server. The renderer
185
>
* is expected to respond with {@link ISSHRemoteAgentHostMainService.respondKeyboardInteractive}
186
>
* within a reasonable time, or the underlying SSH connect attempt will time out.
187
>
*/
188
>
export interface ISSHKeyboardInteractiveRequest {
189
>
readonly requestId: string;
190
>
readonly connectionKey: string;
191
>
/** Display-friendly host (e.g. SSH config alias or `user@host`). */
192
>
readonly displayHost: string;
193
>
readonly username: string;
194
>
/** Optional name field from the server (often empty). */
195
>
readonly name: string;
196
>
/** Optional instructions field from the server (often empty). */
197
>
readonly instructions: string;
198
>
readonly prompts: readonly ISSHKeyboardInteractivePrompt[];
199
>
}
200
>
201
>
/**
202
>
* Main-process service that performs the actual SSH work.
203
>
* The renderer calls this over IPC and handles registration
204
>
* with {@link IRemoteAgentHostService} locally.
205
>
*/
206
>
export const ISSHRemoteAgentHostMainService = createDecorator<ISSHRemoteAgentHostMainService>('sshRemoteAgentHostMainService');
207
>
208
>
export interface ISSHRemoteAgentHostMainService {
209
>
readonly _serviceBrand: undefined;
210
>
211
>
/** Fires when the set of active SSH connections changes. */
212
>
readonly onDidChangeConnections: Event<void>;
213
>
214
>
/** Fires when a connection is closed from the shared process side. */
215
>
readonly onDidCloseConnection: Event<string /* connectionId */>;
216
>
217
>
/** Progress messages during connect (e.g. "Installing CLI..."). */
218
>
readonly onDidReportConnectProgress: Event<ISSHConnectProgress>;
219
>
220
>
/** Fires when a message is received from a remote agent host via the SSH relay. */
221
>
readonly onDidRelayMessage: Event<IRelayMessage>;
222
>
223
>
/** Fires when a relay connection to a remote agent host closes. */
224
>
readonly onDidRelayClose: Event<string /* connectionId */>;
225
>
226
>
/**
227
>
* Fires when the SSH server requests keyboard-interactive auth (typically
228
>
* a password prompt). The renderer must answer via {@link respondKeyboardInteractive}
229
>
* with the same `requestId`, otherwise the auth attempt will hang until the
230
>
* SSH `readyTimeout` elapses.
231
>
*/
232
>
readonly onDidRequestKeyboardInteractive: Event<ISSHKeyboardInteractiveRequest>;
233
>
234
>
/**
235
>
* Fires when a previously requested keyboard-interactive prompt is no
236
>
* longer needed (e.g. the underlying SSH connect attempt failed or was
237
>
* aborted). The renderer should dismiss any UI it opened for `requestId`.
238
>
*/
239
>
readonly onDidCancelKeyboardInteractive: Event<string /* requestId */>;
240
>
241
>
/**
242
>
* Provide responses for a previously fired keyboard-interactive request.
243
>
* Pass `undefined` when the user cancels the prompt; this aborts the
244
>
* owning SSH connection attempt.
245
>
*/
246
>
respondKeyboardInteractive(requestId: string, responses: readonly string[] | undefined): Promise<void>;
247
>
248
>
/**
249
>
* Bootstrap a remote agent host over SSH. Returns serializable
250
>
* connection info for the renderer to register.
251
>
*/
252
>
connect(config: ISSHAgentHostConfig): Promise<ISSHConnectResult>;
253
>
254
>
/**
255
>
* Send a message to a remote agent host through the SSH relay.
256
>
*/
257
>
relaySend(connectionId: string, message: string): Promise<void>;
258
>
259
>
/**
260
>
* Disconnect an SSH-bootstrapped connection by host address.
261
>
*/
262
>
disconnect(host: string): Promise<void>;
263
>
264
>
/** List SSH config host aliases (excluding wildcards). */
265
>
listSSHConfigHosts(): Promise<string[]>;
266
>
267
>
/**
268
>
* Ensure `~/.ssh/config` exists (creating it with the right permissions if
269
>
* missing) and return its URI.
270
>
*/
271
>
ensureUserSSHConfig(): Promise<URI>;
272
>
273
>
/** List the known SSH configuration file URIs (user config always included). */
274
>
listSSHConfigFiles(): Promise<URI[]>;
275
>
276
>
/** Resolve full SSH config for a host via `ssh -G`. */
277
>
resolveSSHConfig(host: string): Promise<ISSHResolvedConfig>;
278
>
279
>
/**
280
>
* Re-establish an SSH tunnel for a previously connected host.
281
>
* Resolves the SSH config alias, connects, and returns fresh
282
>
* connection info with a new local forwarded port.
283
>
*/
284
>
reconnect(sshConfigHost: string, name: string, remoteAgentHostCommand?: string, agentForward?: boolean): Promise<ISSHConnectResult>;
285
>
}