Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/common/remoteAgentHostService.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
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 { connectionTokenQueryName } from '../../../base/common/network.js';
9
import { createDecorator } from '../../instantiation/common/instantiation.js';
10
import type { IAgentConnection } from './agentService.js';
11
import { TUNNEL_ADDRESS_PREFIX } from './tunnelAgentHost.js';
12
13
/** Connection status for a remote agent host. */
14
export const enum RemoteAgentHostConnectionStatus {
15
Connected = 'connected',
16
Connecting = 'connecting',
17
Disconnected = 'disconnected',
18
}
19
20
/** Configuration key for the list of remote agent host addresses. */
21
export const RemoteAgentHostsSettingId = 'chat.remoteAgentHosts';
22
23
/** Configuration key to enable remote agent host connections. */
24
export const RemoteAgentHostsEnabledSettingId = 'chat.remoteAgentHostsEnabled';
25
26
/**
27
* Configuration key that controls whether online dev tunnels and
28
* configured SSH remote agent hosts are auto-connected at startup.
29
*/
30
export const RemoteAgentHostAutoConnectSettingId = 'chat.remoteAgentHostsAutoConnect';
31
32
export const enum RemoteAgentHostEntryType {
33
WebSocket = 'websocket',
34
SSH = 'ssh',
35
Tunnel = 'tunnel',
36
}
37
38
export interface IRemoteAgentHostWebSocketConnection {
39
readonly type: RemoteAgentHostEntryType.WebSocket;
40
readonly address: string;
41
}
42
43
export interface IRemoteAgentHostSSHConnection {
44
readonly type: RemoteAgentHostEntryType.SSH;
45
/**
46
* The WebSocket address used by the agent host protocol client to
47
* communicate with the remote agent host process. This is typically a
48
* forwarded local port (e.g. `localhost:4321`) established by the SSH
49
* tunnel — it is NOT the SSH hostname itself.
50
*/
51
readonly address: string;
52
/**
53
* SSH config host alias (e.g. `myserver`). When set, the SSH tunnel is
54
* automatically re-established on startup using the user's SSH config.
55
* This takes precedence over {@link hostName} when constructing the
56
* VS Code Remote SSH authority.
57
*/
58
readonly sshConfigHost?: string;
59
/**
60
* The actual SSH hostname or IP address of the remote machine
61
* (e.g. `myserver.example.com`). This is the host that the SSH
62
* client connects to, and is used to construct the VS Code Remote
63
* SSH authority when {@link sshConfigHost} is not available.
64
*/
65
readonly hostName: string;
66
/** SSH username for the remote machine. */
67
readonly user?: string;
68
/** SSH port on the remote machine (default 22). */
69
readonly port?: number;
70
}
71
72
export interface IRemoteAgentHostTunnelConnection {
73
readonly type: RemoteAgentHostEntryType.Tunnel;
74
/** Dev tunnel ID. */
75
readonly tunnelId: string;
76
/** Dev tunnel cluster region. */
77
readonly clusterId: string;
78
/**
79
* User-defined display name for this tunnel (derived from tunnel tags).
80
* Used as the tunnel name in the VS Code Remote Tunnels authority
81
* (e.g. `tunnel+<label>`). Falls back to {@link tunnelId} if not set.
82
*/
83
readonly label?: string;
84
/** Auth provider used to connect to this tunnel. */
85
readonly authProvider?: 'github' | 'microsoft';
86
}
87
88
export type RemoteAgentHostConnection = IRemoteAgentHostWebSocketConnection | IRemoteAgentHostSSHConnection | IRemoteAgentHostTunnelConnection;
89
90
/** An entry in the {@link RemoteAgentHostsSettingId} setting. */
91
export interface IRemoteAgentHostEntry {
92
readonly name: string;
93
readonly connectionToken?: string;
94
readonly connection: RemoteAgentHostConnection;
95
}
96
97
export function getEntryAddress(entry: IRemoteAgentHostEntry): string {
98
switch (entry.connection.type) {
99
case RemoteAgentHostEntryType.WebSocket:
100
case RemoteAgentHostEntryType.SSH:
101
return entry.connection.address;
102
case RemoteAgentHostEntryType.Tunnel:
103
return `${TUNNEL_ADDRESS_PREFIX}${entry.connection.tunnelId}`;
104
}
105
}
106
107
export const enum RemoteAgentHostInputValidationError {
108
Empty = 'empty',
109
Invalid = 'invalid',
110
}
111
112
export interface IParsedRemoteAgentHostInput {
113
readonly address: string;
114
readonly connectionToken?: string;
115
readonly suggestedName: string;
116
}
117
118
export type RemoteAgentHostInputParseResult =
119
| { readonly parsed: IParsedRemoteAgentHostInput; readonly error?: undefined }
120
| { readonly parsed?: undefined; readonly error: RemoteAgentHostInputValidationError };
121
122
export const IRemoteAgentHostService = createDecorator<IRemoteAgentHostService>('remoteAgentHostService');
123
124
/**
125
* Manages connections to one or more remote agent host processes over
126
* WebSocket. Each connection is identified by its address string and
127
* exposed as an {@link IAgentConnection}, the same interface used for
128
* the local agent host.
129
*/
130
export interface IRemoteAgentHostService {
131
readonly _serviceBrand: undefined;
132
133
/** Fires when a remote connection is established or lost. */
134
readonly onDidChangeConnections: Event<void>;
135
136
/** Currently connected remote addresses with metadata. */
137
readonly connections: readonly IRemoteAgentHostConnectionInfo[];
138
139
/** All configured remote agent host entries from settings, regardless of connection status. */
140
readonly configuredEntries: readonly IRemoteAgentHostEntry[];
141
142
/**
143
* Get a per-connection {@link IAgentConnection} for subscribing to
144
* state, dispatching actions, creating sessions, etc.
145
*
146
* Returns `undefined` if no active connection exists for the address.
147
*/
148
getConnection(address: string): IAgentConnection | undefined;
149
150
/**
151
* Adds or updates a configured remote host and resolves once a connection
152
* to that host is available.
153
*/
154
addRemoteAgentHost(entry: IRemoteAgentHostEntry): Promise<IRemoteAgentHostConnectionInfo>;
155
156
/**
157
* Removes a configured remote host entry by address.
158
* Disconnects any active connection and removes the entry from settings.
159
*/
160
removeRemoteAgentHost(address: string): Promise<void>;
161
162
/**
163
* Forcefully reconnect to a configured remote host.
164
* Tears down any existing connection and starts a fresh connect attempt
165
* with reset backoff.
166
*/
167
reconnect(address: string): void;
168
169
/**
170
* Register a pre-connected agent connection.
171
* Used by the SSH and tunnel services to inject relay-backed connections
172
* without going through the WebSocket connect flow.
173
*
174
* The optional `transportDisposable` represents the underlying transport
175
* (e.g. an SSH tunnel relay or tunnel-relay session) and is owned by this
176
* service for the lifetime of the entry. It will be disposed when:
177
* - the entry is removed via {@link removeRemoteAgentHost}
178
* - the entry is reconciled away (config-driven removal)
179
* - this service itself is disposed
180
* Callers should put any teardown that needs to happen on entry removal
181
* (e.g. closing the shared-process tunnel, dropping renderer-side handles)
182
* into this disposable, so a single removal path tears down the whole stack.
183
*/
184
addManagedConnection(entry: IRemoteAgentHostEntry, connection: IAgentConnection, transportDisposable?: IDisposable): Promise<IRemoteAgentHostConnectionInfo>;
185
186
/**
187
* Look up the {@link IRemoteAgentHostEntry} for a given address.
188
* Checks both configured entries from settings and dynamically
189
* registered entries (e.g. tunnel connections).
190
*/
191
getEntryByAddress(address: string): IRemoteAgentHostEntry | undefined;
192
}
193
194
/** Metadata about a single remote connection. */
195
export interface IRemoteAgentHostConnectionInfo {
196
readonly address: string;
197
readonly name: string;
198
readonly clientId: string;
199
readonly defaultDirectory?: string;
200
readonly status: RemoteAgentHostConnectionStatus;
201
}
202
203
export class NullRemoteAgentHostService implements IRemoteAgentHostService {
204
declare readonly _serviceBrand: undefined;
205
readonly onDidChangeConnections = Event.None;
206
readonly connections: readonly IRemoteAgentHostConnectionInfo[] = [];
207
readonly configuredEntries: readonly IRemoteAgentHostEntry[] = [];
208
getConnection(): IAgentConnection | undefined { return undefined; }
209
async addRemoteAgentHost(): Promise<IRemoteAgentHostConnectionInfo> {
210
throw new Error('Remote agent host connections are not supported in this environment.');
211
}
212
async removeRemoteAgentHost(_address: string): Promise<void> { }
213
reconnect(_address: string): void { }
214
async addManagedConnection(): Promise<IRemoteAgentHostConnectionInfo> {
215
throw new Error('Remote agent host connections are not supported in this environment.');
216
}
217
getEntryByAddress(): IRemoteAgentHostEntry | undefined { return undefined; }
218
}
219
220
export function parseRemoteAgentHostInput(input: string): RemoteAgentHostInputParseResult {
221
const trimmedInput = input.trim();
222
if (!trimmedInput) {
223
return { error: RemoteAgentHostInputValidationError.Empty };
224
}
225
226
const candidate = extractRemoteAgentHostCandidate(trimmedInput);
227
if (!candidate) {
228
return { error: RemoteAgentHostInputValidationError.Invalid };
229
}
230
231
const hasExplicitScheme = /^[a-zA-Z][a-zA-Z\d+.-]*:\/\//.test(candidate);
232
try {
233
const url = new URL(hasExplicitScheme ? candidate : `ws://${candidate}`);
234
const normalizedProtocol = normalizeRemoteAgentHostProtocol(url.protocol);
235
if (!normalizedProtocol || !url.host) {
236
return { error: RemoteAgentHostInputValidationError.Invalid };
237
}
238
239
const connectionToken = url.searchParams.get(connectionTokenQueryName) ?? undefined;
240
url.searchParams.delete(connectionTokenQueryName);
241
242
// Only preserve wss: in the address - the transport defaults to ws:
243
const address = formatRemoteAgentHostAddress(url, normalizedProtocol === 'wss:' ? normalizedProtocol : undefined);
244
if (!address) {
245
return { error: RemoteAgentHostInputValidationError.Invalid };
246
}
247
248
return {
249
parsed: {
250
address,
251
connectionToken,
252
suggestedName: url.host,
253
},
254
};
255
} catch {
256
return { error: RemoteAgentHostInputValidationError.Invalid };
257
}
258
}
259
260
function extractRemoteAgentHostCandidate(input: string): string | undefined {
261
const urlMatch = input.match(/(?<url>(?:https?|wss?):\/\/\S+)/i);
262
const candidate = urlMatch?.groups?.url ?? input;
263
const trimmedCandidate = candidate.trim().replace(/[),.;\]]+$/, '');
264
return trimmedCandidate || undefined;
265
}
266
267
function normalizeRemoteAgentHostProtocol(protocol: string): 'ws:' | 'wss:' | undefined {
268
switch (protocol.toLowerCase()) {
269
case 'ws:':
270
case 'http:':
271
return 'ws:';
272
case 'wss:':
273
case 'https:':
274
return 'wss:';
275
default:
276
return undefined;
277
}
278
}
279
280
function formatRemoteAgentHostAddress(url: URL, protocol: 'ws:' | 'wss:' | undefined): string | undefined {
281
if (!url.host) {
282
return undefined;
283
}
284
285
const path = url.pathname !== '/' ? url.pathname : '';
286
const query = url.search;
287
const base = protocol ? `${protocol}//${url.host}` : url.host;
288
return `${base}${path}${query}`;
289
}
290
291
/** Raw shape of entries persisted in the {@link RemoteAgentHostsSettingId} setting. */
292
export interface IRawRemoteAgentHostEntry {
293
readonly address: string;
294
readonly name: string;
295
readonly connectionToken?: string;
296
readonly sshConfigHost?: string;
297
readonly sshHostName?: string;
298
readonly sshUser?: string;
299
readonly sshPort?: number;
300
}
301
302
export function rawEntryToEntry(raw: IRawRemoteAgentHostEntry): IRemoteAgentHostEntry | undefined {
303
if (raw.sshConfigHost || raw.sshHostName || raw.sshUser || raw.sshPort) {
304
return {
305
name: raw.name,
306
connectionToken: raw.connectionToken,
307
connection: {
308
type: RemoteAgentHostEntryType.SSH,
309
address: raw.address,
310
sshConfigHost: raw.sshConfigHost,
311
hostName: raw.sshHostName ?? raw.address,
312
user: raw.sshUser,
313
port: raw.sshPort,
314
},
315
};
316
}
317
return {
318
name: raw.name,
319
connectionToken: raw.connectionToken,
320
connection: {
321
type: RemoteAgentHostEntryType.WebSocket,
322
address: raw.address,
323
},
324
};
325
}
326
327
export function entryToRawEntry(entry: IRemoteAgentHostEntry): IRawRemoteAgentHostEntry | undefined {
328
switch (entry.connection.type) {
329
case RemoteAgentHostEntryType.SSH:
330
return {
331
address: entry.connection.address,
332
name: entry.name,
333
connectionToken: entry.connectionToken,
334
sshConfigHost: entry.connection.sshConfigHost,
335
sshHostName: entry.connection.hostName,
336
sshUser: entry.connection.user,
337
sshPort: entry.connection.port,
338
};
339
case RemoteAgentHostEntryType.WebSocket:
340
return {
341
address: entry.connection.address,
342
name: entry.name,
343
connectionToken: entry.connectionToken,
344
};
345
case RemoteAgentHostEntryType.Tunnel:
346
return undefined;
347
}
348
}
349
350