Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts
13401 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 { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
7
import { Event } from '../../../../base/common/event.js';
8
import { observableValue } from '../../../../base/common/observable.js';
9
import { URI } from '../../../../base/common/uri.js';
10
import * as nls from '../../../../nls.js';
11
import { agentHostAuthority } from '../../../../platform/agentHost/common/agentHostUri.js';
12
import { type AgentProvider, type IAgentConnection } from '../../../../platform/agentHost/common/agentService.js';
13
import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostEntry, IRemoteAgentHostService, RemoteAgentHostAutoConnectSettingId, RemoteAgentHostConnectionStatus, RemoteAgentHostEntryType, RemoteAgentHostsEnabledSettingId, RemoteAgentHostsSettingId, getEntryAddress } from '../../../../platform/agentHost/common/remoteAgentHostService.js';
14
import { TunnelAgentHostsSettingId } from '../../../../platform/agentHost/common/tunnelAgentHost.js';
15
import { type ProtectedResourceMetadata } from '../../../../platform/agentHost/common/state/protocol/state.js';
16
import { type AgentInfo, type CustomizationRef, type RootState } from '../../../../platform/agentHost/common/state/sessionState.js';
17
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
18
import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
19
import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';
20
import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';
21
import { IFileService } from '../../../../platform/files/common/files.js';
22
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
23
import { ILogService } from '../../../../platform/log/common/log.js';
24
import { INotificationService } from '../../../../platform/notification/common/notification.js';
25
import product from '../../../../platform/product/common/product.js';
26
import { Registry } from '../../../../platform/registry/common/platform.js';
27
import { IStorageService } from '../../../../platform/storage/common/storage.js';
28
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
29
import { AgentCustomizationSyncProvider } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationSyncProvider.js';
30
import { authenticateProtectedResources, AgentHostAuthTokenCache, resolveAuthenticationInteractively } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostAuth.js';
31
import { AgentHostLanguageModelProvider } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLanguageModelProvider.js';
32
import { AgentHostSessionHandler } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.js';
33
import { LoggingAgentConnection } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.js';
34
import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';
35
import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';
36
import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';
37
import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js';
38
import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js';
39
import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';
40
import { resolveCustomizationRefs } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLocalCustomizations.js';
41
import { IAgentHostFileSystemService } from '../../../../workbench/services/agentHost/common/agentHostFileSystemService.js';
42
import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';
43
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
44
import { remoteAgentHostSessionTypeId } from '../common/remoteAgentHostSessionType.js';
45
import { createRemoteAgentHarnessDescriptor, RemoteAgentCustomizationItemProvider, RemoteAgentPluginController } from './remoteAgentHostCustomizationHarness.js';
46
import { RemoteAgentHostSessionsProvider } from './remoteAgentHostSessionsProvider.js';
47
import { SyncedCustomizationBundler } from './syncedCustomizationBundler.js';
48
import { ISSHRemoteAgentHostService } from '../../../../platform/agentHost/common/sshRemoteAgentHost.js';
49
import { IAgentHostTerminalService } from '../../../../workbench/contrib/terminal/browser/agentHostTerminalService.js';
50
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
51
import { logTerminalRecovery } from '../../../common/sessionsTelemetry.js';
52
53
/** Per-connection state bundle, disposed when a connection is removed. */
54
class ConnectionState extends Disposable {
55
readonly store = this._register(new DisposableStore());
56
readonly agents = this._register(new DisposableMap<AgentProvider, DisposableStore>());
57
readonly modelProviders = new Map<AgentProvider, AgentHostLanguageModelProvider>();
58
readonly loggedConnection: LoggingAgentConnection;
59
/** Dedupes redundant `authenticate` RPCs when the resolved token hasn't changed. */
60
readonly authTokenCache = new AgentHostAuthTokenCache();
61
62
constructor(
63
readonly name: string | undefined,
64
connection: IAgentConnection,
65
channelId: string,
66
channelLabel: string,
67
@IInstantiationService instantiationService: IInstantiationService,
68
) {
69
super();
70
this.loggedConnection = this._register(instantiationService.createInstance(LoggingAgentConnection, connection, channelId, channelLabel));
71
}
72
}
73
74
/**
75
* Discovers available agents from each connected remote agent host and
76
* dynamically registers each one as a chat session type with its own
77
* session handler and language model provider.
78
*
79
* Uses the same unified {@link AgentHostSessionHandler} as the local
80
* agent host, obtaining per-connection {@link IAgentConnection}
81
* instances from {@link IRemoteAgentHostService.getConnection}.
82
*/
83
export class RemoteAgentHostContribution extends Disposable implements IWorkbenchContribution {
84
85
static readonly ID = 'sessions.contrib.remoteAgentHostContribution';
86
87
/** Per-connection state: client state + per-agent registrations. */
88
private readonly _connections = this._register(new DisposableMap<string, ConnectionState>());
89
90
/** Per-address sessions provider, registered for all configured entries. */
91
private readonly _providerStores = this._register(new DisposableMap<string, DisposableStore>());
92
private readonly _providerInstances = new Map<string, RemoteAgentHostSessionsProvider>();
93
private readonly _pendingSSHReconnects = new Set<string>();
94
95
constructor(
96
@IRemoteAgentHostService private readonly _remoteAgentHostService: IRemoteAgentHostService,
97
@IChatSessionsService private readonly _chatSessionsService: IChatSessionsService,
98
@ILanguageModelsService private readonly _languageModelsService: ILanguageModelsService,
99
@ILogService private readonly _logService: ILogService,
100
@IInstantiationService private readonly _instantiationService: IInstantiationService,
101
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
102
@IDefaultAccountService private readonly _defaultAccountService: IDefaultAccountService,
103
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
104
@IFileService private readonly _fileService: IFileService,
105
@INotificationService private readonly _notificationService: INotificationService,
106
@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,
107
@IConfigurationService private readonly _configurationService: IConfigurationService,
108
@IAgentHostFileSystemService private readonly _agentHostFileSystemService: IAgentHostFileSystemService,
109
@ISSHRemoteAgentHostService private readonly _sshService: ISSHRemoteAgentHostService,
110
@IAICustomizationWorkspaceService private readonly _customizationWorkspaceService: IAICustomizationWorkspaceService,
111
@ICustomizationHarnessService private readonly _customizationHarnessService: ICustomizationHarnessService,
112
@IStorageService private readonly _storageService: IStorageService,
113
@IAgentPluginService private readonly _agentPluginService: IAgentPluginService,
114
@IAgentHostTerminalService private readonly _agentHostTerminalService: IAgentHostTerminalService,
115
@ITelemetryService private readonly _telemetryService: ITelemetryService,
116
@IPromptsService private readonly _promptsService: IPromptsService,
117
) {
118
super();
119
120
// Reconcile providers when configured entries change
121
this._register(this._configurationService.onDidChangeConfiguration(e => {
122
if (e.affectsConfiguration(RemoteAgentHostsSettingId) || e.affectsConfiguration(RemoteAgentHostsEnabledSettingId) || e.affectsConfiguration(RemoteAgentHostAutoConnectSettingId)) {
123
this._reconcile();
124
}
125
}));
126
127
// Reconcile when connections change (added/removed/reconnected)
128
this._register(this._remoteAgentHostService.onDidChangeConnections(() => {
129
this._reconcile();
130
}));
131
132
// Push auth token whenever the default account or sessions change
133
this._register(this._defaultAccountService.onDidChangeDefaultAccount(() => this._authenticateAllConnections()));
134
this._register(this._authenticationService.onDidChangeSessions(() => this._authenticateAllConnections()));
135
136
// Initial setup for configured entries and connected remotes
137
this._reconcile();
138
}
139
140
private _reconcile(): void {
141
this._reconcileProviders();
142
this._reconcileConnections();
143
this._reconnectSSHEntries();
144
145
// Ensure every live connection is wired to its provider.
146
// This covers the case where a provider was recreated (e.g. name
147
// change) while a connection for that address already existed —
148
// we need to re-expose both the connection and the output channel,
149
// otherwise `Show Output` on the recreated provider would break.
150
for (const [address, connState] of this._connections) {
151
const connectionInfo = this._remoteAgentHostService.connections.find(c => c.address === address);
152
const provider = this._providerInstances.get(address);
153
if (provider) {
154
provider.setConnection(connState.loggedConnection, connectionInfo?.defaultDirectory);
155
provider.setOutputChannelId(connState.loggedConnection.channelId);
156
}
157
}
158
159
// Update connection status on all providers (including those
160
// that are reconnecting and don't have an active connection).
161
for (const [address, provider] of this._providerInstances) {
162
const connectionInfo = this._remoteAgentHostService.connections.find(c => c.address === address);
163
if (connectionInfo) {
164
provider.setConnectionStatus(connectionInfo.status);
165
} else {
166
provider.setConnectionStatus(RemoteAgentHostConnectionStatus.Disconnected);
167
}
168
}
169
}
170
171
private _reconcileProviders(): void {
172
const enabled = this._configurationService.getValue<boolean>(RemoteAgentHostsEnabledSettingId);
173
const entries = enabled ? this._remoteAgentHostService.configuredEntries : [];
174
const desiredAddresses = new Set(entries.map(e => getEntryAddress(e)));
175
176
// Remove providers no longer configured
177
for (const [address] of this._providerStores) {
178
if (!desiredAddresses.has(address)) {
179
this._providerStores.deleteAndDispose(address);
180
}
181
}
182
183
// Add or recreate providers for configured entries
184
for (const entry of entries) {
185
const address = getEntryAddress(entry);
186
const existing = this._providerInstances.get(address);
187
if (existing && existing.label !== (entry.name || address)) {
188
// Name changed — recreate since ISessionsProvider.label is readonly
189
this._providerStores.deleteAndDispose(address);
190
}
191
if (!this._providerStores.has(address)) {
192
this._createProvider(entry);
193
}
194
}
195
}
196
197
private _createProvider(entry: IRemoteAgentHostEntry): void {
198
const address = getEntryAddress(entry);
199
const store = new DisposableStore();
200
const provider = this._instantiationService.createInstance(
201
RemoteAgentHostSessionsProvider, { address, name: entry.name });
202
store.add(provider);
203
store.add(this._sessionsProvidersService.registerProvider(provider));
204
this._providerInstances.set(address, provider);
205
store.add(toDisposable(() => this._providerInstances.delete(address)));
206
this._providerStores.set(address, store);
207
}
208
209
/**
210
* Re-establish SSH connections for configured entries that have an
211
* sshConfigHost but no active connection.
212
*/
213
private _reconnectSSHEntries(): void {
214
const autoConnect = this._configurationService.getValue<boolean>(RemoteAgentHostAutoConnectSettingId);
215
const entries = this._remoteAgentHostService.configuredEntries;
216
for (const entry of entries) {
217
if (entry.connection.type !== RemoteAgentHostEntryType.SSH || !entry.connection.sshConfigHost) {
218
continue;
219
}
220
const address = getEntryAddress(entry);
221
const sshConfigHost = entry.connection.sshConfigHost;
222
// Skip if already connected or reconnecting
223
const hasConnection = this._remoteAgentHostService.connections.some(
224
c => c.address === address && c.status === RemoteAgentHostConnectionStatus.Connected
225
);
226
if (hasConnection || this._pendingSSHReconnects.has(sshConfigHost)) {
227
continue;
228
}
229
if (!autoConnect) {
230
continue;
231
}
232
this._pendingSSHReconnects.add(sshConfigHost);
233
this._logService.info(`[RemoteAgentHost] Re-establishing SSH tunnel for ${sshConfigHost}`);
234
this._sshService.reconnect(sshConfigHost, entry.name).then(() => {
235
this._pendingSSHReconnects.delete(sshConfigHost);
236
this._logService.info(`[RemoteAgentHost] SSH tunnel re-established for ${sshConfigHost}`);
237
}).catch(err => {
238
this._pendingSSHReconnects.delete(sshConfigHost);
239
this._logService.error(`[RemoteAgentHost] SSH reconnect failed for ${sshConfigHost}`, err);
240
// Host is unreachable — unpublish any cached sessions we
241
// were showing so the UI doesn't list stale entries for a
242
// host we cannot currently reach.
243
this._providerInstances.get(address)?.unpublishCachedSessions();
244
});
245
}
246
}
247
248
private _reconcileConnections(): void {
249
const currentConnections = this._remoteAgentHostService.connections;
250
const connectedAddresses = new Set(
251
currentConnections
252
.filter(c => c.status === RemoteAgentHostConnectionStatus.Connected)
253
.map(c => c.address)
254
);
255
const allAddresses = new Set(currentConnections.map(c => c.address));
256
257
// Remove contribution state for connections that are no longer present at all
258
for (const [address] of this._connections) {
259
if (!allAddresses.has(address)) {
260
this._logService.info(`[RemoteAgentHost] Removing contribution for ${address}`);
261
this._providerInstances.get(address)?.clearConnection();
262
this._connections.deleteAndDispose(address);
263
} else if (!connectedAddresses.has(address)) {
264
// Connection exists but is not connected (reconnecting or disconnected).
265
// Keep the contribution state but don't clear the provider —
266
// the session cache is preserved during reconnect.
267
}
268
}
269
270
// Add or update connections
271
for (const connectionInfo of currentConnections) {
272
// Only set up contribution state for connected entries
273
if (connectionInfo.status !== RemoteAgentHostConnectionStatus.Connected) {
274
continue;
275
}
276
const existing = this._connections.get(connectionInfo.address);
277
if (existing) {
278
const nameChanged = existing.name !== connectionInfo.name;
279
const clientIdChanged = existing.loggedConnection.clientId !== connectionInfo.clientId;
280
281
// If the name or clientId changed, tear down and re-register
282
if (nameChanged || clientIdChanged) {
283
this._logService.info(`[RemoteAgentHost] Reconnecting contribution for ${connectionInfo.address}: oldClientId=${existing.loggedConnection.clientId}, newClientId=${connectionInfo.clientId}, nameChanged=${nameChanged}`);
284
const oldClientId = existing.loggedConnection.clientId;
285
this._connections.deleteAndDispose(connectionInfo.address);
286
this._setupConnection(connectionInfo);
287
288
// Reconnect active terminals only when the backing
289
// client changed. Name-only updates don't invalidate
290
// subscriptions and would cause unnecessary buffer
291
// clear/replay flicker.
292
if (clientIdChanged) {
293
const newConnection = this._remoteAgentHostService.getConnection(connectionInfo.address);
294
if (newConnection) {
295
this._agentHostTerminalService.reconnectTerminals(newConnection, oldClientId).then(
296
({ recovered, total }) => {
297
if (total > 0) {
298
this._logService.info(`[RemoteAgentHost] Terminal reconnection: ${recovered}/${total} recovered`);
299
logTerminalRecovery(this._telemetryService, { recoveredCount: recovered, totalCount: total });
300
}
301
},
302
err => this._logService.warn('[RemoteAgentHost] Terminal reconnection failed', err)
303
);
304
}
305
}
306
}
307
} else {
308
this._setupConnection(connectionInfo);
309
}
310
}
311
}
312
313
private _setupConnection(connectionInfo: IRemoteAgentHostConnectionInfo): void {
314
const connection = this._remoteAgentHostService.getConnection(connectionInfo.address);
315
if (!connection) {
316
return;
317
}
318
319
const { address, name } = connectionInfo;
320
const channelLabel = `Agent Host (${name || address})`;
321
const connState = this._instantiationService.createInstance(ConnectionState, name, connection, `agenthost.${connection.clientId}`, channelLabel);
322
const loggedConnection = connState.loggedConnection;
323
this._connections.set(address, connState);
324
const store = connState.store;
325
326
// Track authority -> connection mapping for FS provider routing
327
const authority = agentHostAuthority(address);
328
store.add(this._agentHostFileSystemService.registerAuthority(authority, connection));
329
330
// React to root state changes (agent discovery)
331
store.add(loggedConnection.rootState.onDidChange(rootState => {
332
this._handleRootStateChange(address, loggedConnection, rootState);
333
}));
334
335
// If root state is already available, process it immediately
336
const initialRootState = loggedConnection.rootState.value;
337
if (initialRootState && !(initialRootState instanceof Error)) {
338
this._handleRootStateChange(address, loggedConnection, initialRootState);
339
}
340
341
// Wire connection to existing sessions provider
342
const provider = this._providerInstances.get(address);
343
if (provider) {
344
provider.setConnection(loggedConnection, connectionInfo.defaultDirectory);
345
// Expose the output channel ID so the workspace picker can offer "Show Output"
346
provider.setOutputChannelId(loggedConnection.channelId);
347
}
348
}
349
350
private _handleRootStateChange(address: string, loggedConnection: LoggingAgentConnection, rootState: RootState): void {
351
const connState = this._connections.get(address);
352
if (!connState) {
353
return;
354
}
355
356
const incoming = new Set(rootState.agents.map(a => a.provider));
357
358
// Remove agents no longer present
359
for (const [provider] of connState.agents) {
360
if (!incoming.has(provider)) {
361
connState.agents.deleteAndDispose(provider);
362
connState.modelProviders.delete(provider);
363
}
364
}
365
366
// Authenticate using protectedResources from agent info
367
this._authenticateWithConnection(address, loggedConnection, rootState.agents)
368
.catch(() => { /* best-effort */ });
369
370
// Register new agents, push model updates to existing ones
371
for (const agent of rootState.agents) {
372
if (!connState.agents.has(agent.provider)) {
373
this._registerAgent(address, loggedConnection, agent, connState.name);
374
} else {
375
const modelProvider = connState.modelProviders.get(agent.provider);
376
modelProvider?.updateModels(agent.models);
377
}
378
}
379
}
380
381
private _registerAgent(address: string, loggedConnection: LoggingAgentConnection, agent: AgentInfo, configuredName: string | undefined): void {
382
const connState = this._connections.get(address);
383
if (!connState) {
384
return;
385
}
386
387
const agentStore = new DisposableStore();
388
connState.agents.set(agent.provider, agentStore);
389
connState.store.add(agentStore);
390
391
const sanitized = agentHostAuthority(address);
392
const providerId = `agenthost-${sanitized}`;
393
const sessionType = remoteAgentHostSessionTypeId(sanitized, agent.provider);
394
const agentId = sessionType;
395
const vendor = sessionType;
396
397
// User-facing display name for this agent. We always include the
398
// agent's own name so that a host exposing multiple agents (e.g.
399
// `copilot` + `openai` from the same machine) produces distinct
400
// labels instead of collapsing to a single `configuredName`.
401
const hostLabel = configuredName || address;
402
const agentLabel = agent.displayName?.trim() || agent.provider;
403
const displayName = `${agentLabel} [${hostLabel}]`;
404
405
// Per-agent working directory cache, scoped to the agent store lifetime
406
const sessionWorkingDirs = new Map<string, URI>();
407
agentStore.add(toDisposable(() => sessionWorkingDirs.clear()));
408
409
// Capture the working directory from the session that is being created.
410
const resolveWorkingDirectory = (sessionResource: URI): URI | undefined => {
411
const resourceKey = sessionResource.toString();
412
const cached = sessionWorkingDirs.get(resourceKey);
413
if (cached) {
414
return cached;
415
}
416
const provider = this._sessionsProvidersService.getProvider<RemoteAgentHostSessionsProvider>(providerId);
417
const session = provider?.getSessionByResource(sessionResource);
418
const repository = session?.workspace.get()?.repositories[0];
419
const workingDirectory = repository?.workingDirectory ?? repository?.uri;
420
if (workingDirectory) {
421
sessionWorkingDirs.set(resourceKey, workingDirectory);
422
return workingDirectory;
423
}
424
return undefined;
425
};
426
427
// Chat session contribution
428
agentStore.add(this._chatSessionsService.registerChatSessionContribution({
429
type: sessionType,
430
name: agentId,
431
displayName,
432
description: agent.description,
433
canDelegate: true,
434
requiresCustomModels: true,
435
supportsDelegation: false,
436
capabilities: {
437
supportsCheckpoints: true,
438
supportsPromptAttachments: true,
439
},
440
}));
441
442
// Customization harness for this remote agent
443
const pluginController = agentStore.add(new RemoteAgentPluginController(
444
hostLabel,
445
sanitized,
446
loggedConnection,
447
this._fileDialogService,
448
this._notificationService,
449
this._customizationWorkspaceService,
450
));
451
const itemProvider = agentStore.add(new RemoteAgentCustomizationItemProvider(agent, loggedConnection, sanitized, pluginController, this._fileService, this._logService));
452
const syncProvider = agentStore.add(new AgentCustomizationSyncProvider(sessionType, this._storageService));
453
const harnessDescriptor = createRemoteAgentHarnessDescriptor(sessionType, displayName, pluginController, itemProvider, syncProvider);
454
agentStore.add(this._customizationHarnessService.registerExternalHarness(harnessDescriptor));
455
456
// Bundler for packaging individual files into a virtual Open Plugin
457
const bundler = agentStore.add(this._instantiationService.createInstance(SyncedCustomizationBundler, sessionType));
458
459
// Agent-level customizations observable
460
const customizations = observableValue<CustomizationRef[]>('agentCustomizations', []);
461
const updateCustomizations = async () => {
462
const refs = await resolveCustomizationRefs(this._promptsService, syncProvider, this._agentPluginService, bundler);
463
customizations.set(refs, undefined);
464
};
465
agentStore.add(syncProvider.onDidChange(() => updateCustomizations()));
466
agentStore.add(Event.any(
467
this._promptsService.onDidChangeCustomAgents,
468
this._promptsService.onDidChangeSlashCommands,
469
this._promptsService.onDidChangeSkills,
470
this._promptsService.onDidChangeInstructions,
471
)(() => updateCustomizations()));
472
updateCustomizations(); // resolve initial state
473
474
// Session handler (unified)
475
const sessionHandler = agentStore.add(this._instantiationService.createInstance(
476
AgentHostSessionHandler, {
477
provider: agent.provider,
478
agentId,
479
sessionType,
480
fullName: displayName,
481
description: agent.description,
482
connection: loggedConnection,
483
connectionAuthority: sanitized,
484
extensionId: 'vscode.remote-agent-host',
485
extensionDisplayName: 'Remote Agent Host',
486
resolveWorkingDirectory,
487
resolveAuthentication: (resources) => this._resolveAuthenticationInteractively(address, loggedConnection, resources),
488
customizations,
489
}));
490
agentStore.add(this._chatSessionsService.registerChatSessionContentProvider(sessionType, sessionHandler));
491
492
// Language model provider.
493
// Order matters: `updateModels` must be called after
494
// `registerLanguageModelProvider` so the initial `onDidChange` is observed.
495
const vendorDescriptor = { vendor, displayName, configuration: undefined, managementCommand: undefined, when: undefined };
496
this._languageModelsService.deltaLanguageModelChatProviderDescriptors([vendorDescriptor], []);
497
agentStore.add(toDisposable(() => this._languageModelsService.deltaLanguageModelChatProviderDescriptors([], [vendorDescriptor])));
498
const modelProvider = agentStore.add(new AgentHostLanguageModelProvider(sessionType, vendor));
499
connState.modelProviders.set(agent.provider, modelProvider);
500
agentStore.add(toDisposable(() => connState.modelProviders.delete(agent.provider)));
501
agentStore.add(this._languageModelsService.registerLanguageModelProvider(vendor, modelProvider));
502
modelProvider.updateModels(agent.models);
503
504
this._logService.info(`[RemoteAgentHost] Registered agent ${agent.provider} from ${address} as ${sessionType}`);
505
}
506
507
private _authenticateAllConnections(): void {
508
for (const [address, connState] of this._connections) {
509
const rootState = connState.loggedConnection.rootState.value;
510
if (rootState && !(rootState instanceof Error)) {
511
this._authenticateWithConnection(address, connState.loggedConnection, rootState.agents).catch(() => { /* best-effort */ });
512
}
513
}
514
}
515
516
/**
517
* Authenticate using protectedResources from agent info in root state.
518
* Resolves tokens via the standard VS Code authentication service.
519
*
520
* Marks the matching provider's `authenticationPending` observable while
521
* the auth pass is in flight so that sessions surface as still loading.
522
*/
523
private async _authenticateWithConnection(address: string, loggedConnection: LoggingAgentConnection, agents: readonly AgentInfo[]): Promise<void> {
524
const providerId = `agenthost-${agentHostAuthority(address)}`;
525
const provider = this._sessionsProvidersService.getProvider<RemoteAgentHostSessionsProvider>(providerId);
526
const authTokenCache = this._connections.get(address)?.authTokenCache;
527
provider?.setAuthenticationPending(true);
528
try {
529
await authenticateProtectedResources(agents, {
530
authTokenCache,
531
authenticationService: this._authenticationService,
532
logPrefix: '[RemoteAgentHost]',
533
logService: this._logService,
534
authenticate: request => loggedConnection.authenticate(request),
535
});
536
} catch (err) {
537
this._logService.error('[RemoteAgentHost] Failed to authenticate with connection', err);
538
loggedConnection.logError('authenticateWithConnection', err);
539
} finally {
540
provider?.setAuthenticationPending(false);
541
}
542
}
543
544
/**
545
* Interactively prompt the user to authenticate when the server requires it.
546
* Returns true if authentication succeeded.
547
*/
548
private async _resolveAuthenticationInteractively(address: string, loggedConnection: LoggingAgentConnection, protectedResources: readonly ProtectedResourceMetadata[]): Promise<boolean> {
549
const authTokenCache = this._connections.get(address)?.authTokenCache;
550
try {
551
return await resolveAuthenticationInteractively(protectedResources, {
552
authTokenCache,
553
authenticationService: this._authenticationService,
554
logPrefix: '[RemoteAgentHost]',
555
logService: this._logService,
556
authenticate: request => loggedConnection.authenticate(request),
557
});
558
} catch (err) {
559
this._logService.error('[RemoteAgentHost] Interactive authentication failed', err);
560
loggedConnection.logError('resolveAuthenticationInteractively', err);
561
}
562
return false;
563
}
564
}
565
566
registerWorkbenchContribution2(RemoteAgentHostContribution.ID, RemoteAgentHostContribution, WorkbenchPhase.AfterRestored);
567
568
Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({
569
properties: {
570
[RemoteAgentHostsEnabledSettingId]: {
571
type: 'boolean',
572
description: nls.localize('chat.remoteAgentHosts.enabled', "Enable connecting to remote agent hosts."),
573
default: product.quality !== 'stable',
574
scope: ConfigurationScope.APPLICATION,
575
tags: ['experimental', 'advanced'],
576
},
577
[RemoteAgentHostAutoConnectSettingId]: {
578
type: 'boolean',
579
description: nls.localize('chat.remoteAgentHosts.autoConnect', "Automatically connect to online dev tunnel and SSH-configured remote agent hosts on startup. When disabled, cached sessions are still shown but connections are established only on demand."),
580
default: true,
581
scope: ConfigurationScope.APPLICATION,
582
tags: ['experimental', 'advanced'],
583
},
584
'chat.sshRemoteAgentHostCommand': {
585
type: 'string',
586
description: nls.localize('chat.sshRemoteAgentHostCommand', "For development: Override the command used to start the remote agent host over SSH. When set, skips automatic CLI installation and runs this command instead. The command must print a WebSocket URL matching ws://127.0.0.1:PORT (optionally with ?tkn=TOKEN) to stdout or stderr./"),
587
default: '',
588
scope: ConfigurationScope.APPLICATION,
589
tags: ['experimental', 'advanced'],
590
},
591
'chat.agentHost.forwardSSHAgent': {
592
type: 'boolean',
593
description: nls.localize('chat.agentHost.forwardSSHAgent', "When enabled, forwards the local SSH agent to the remote machine during SSH agent host connections to hosts whose SSH config has `ForwardAgent yes`. Only enable this for trusted hosts. The remote agent host process must be restarted for this setting to take effect."),
594
default: false,
595
scope: ConfigurationScope.APPLICATION,
596
tags: ['experimental', 'advanced'],
597
},
598
[RemoteAgentHostsSettingId]: {
599
type: 'array',
600
items: {
601
type: 'object',
602
properties: {
603
address: { type: 'string', description: nls.localize('chat.remoteAgentHosts.address', "The address of the remote agent host (e.g. \"localhost:3000\").") },
604
name: { type: 'string', description: nls.localize('chat.remoteAgentHosts.name', "A display name for this remote agent host.") },
605
connectionToken: { type: 'string', description: nls.localize('chat.remoteAgentHosts.connectionToken', "An optional connection token for authenticating with the remote agent host.") },
606
sshConfigHost: { type: 'string', description: nls.localize('chat.remoteAgentHosts.sshConfigHost', "SSH config host alias for automatic reconnection via SSH tunnel.") },
607
},
608
required: ['address', 'name'],
609
},
610
description: nls.localize('chat.remoteAgentHosts', "A list of remote agent host addresses to connect to (e.g. \"localhost:3000\")."),
611
default: [],
612
scope: ConfigurationScope.APPLICATION,
613
tags: ['experimental', 'advanced'],
614
},
615
[TunnelAgentHostsSettingId]: {
616
type: 'array',
617
items: { type: 'string' },
618
description: nls.localize('chat.remoteAgentTunnels', "Additional dev tunnel names to look for when connecting to remote agent hosts. These are looked up in addition to tunnels automatically enumerated from your account."),
619
default: [],
620
scope: ConfigurationScope.APPLICATION,
621
tags: ['experimental', 'advanced'],
622
},
623
},
624
});
625
626
// Side-effect registrations for the remote agent host feature
627
import './remoteAgentHostActions.js';
628
import './manageRemoteAgentHosts.js';
629
import '../../chat/browser/agentHost/agentHostModelPicker.js';
630
631