Path: blob/main/src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHost.contribution.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Disposable, DisposableMap, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';6import { Event } from '../../../../base/common/event.js';7import { observableValue } from '../../../../base/common/observable.js';8import { URI } from '../../../../base/common/uri.js';9import * as nls from '../../../../nls.js';10import { agentHostAuthority } from '../../../../platform/agentHost/common/agentHostUri.js';11import { type AgentProvider, type IAgentConnection } from '../../../../platform/agentHost/common/agentService.js';12import { IRemoteAgentHostConnectionInfo, IRemoteAgentHostEntry, IRemoteAgentHostService, RemoteAgentHostAutoConnectSettingId, RemoteAgentHostConnectionStatus, RemoteAgentHostEntryType, RemoteAgentHostsEnabledSettingId, RemoteAgentHostsSettingId, getEntryAddress } from '../../../../platform/agentHost/common/remoteAgentHostService.js';13import { TunnelAgentHostsSettingId } from '../../../../platform/agentHost/common/tunnelAgentHost.js';14import { type ProtectedResourceMetadata } from '../../../../platform/agentHost/common/state/protocol/state.js';15import { type AgentInfo, type CustomizationRef, type RootState } from '../../../../platform/agentHost/common/state/sessionState.js';16import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';17import { ConfigurationScope, Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';18import { IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';19import { IFileDialogService } from '../../../../platform/dialogs/common/dialogs.js';20import { IFileService } from '../../../../platform/files/common/files.js';21import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';22import { ILogService } from '../../../../platform/log/common/log.js';23import { INotificationService } from '../../../../platform/notification/common/notification.js';24import product from '../../../../platform/product/common/product.js';25import { Registry } from '../../../../platform/registry/common/platform.js';26import { IStorageService } from '../../../../platform/storage/common/storage.js';27import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';28import { AgentCustomizationSyncProvider } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationSyncProvider.js';29import { authenticateProtectedResources, AgentHostAuthTokenCache, resolveAuthenticationInteractively } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostAuth.js';30import { AgentHostLanguageModelProvider } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLanguageModelProvider.js';31import { AgentHostSessionHandler } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.js';32import { LoggingAgentConnection } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/loggingAgentConnection.js';33import { IChatSessionsService } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';34import { IAICustomizationWorkspaceService } from '../../../../workbench/contrib/chat/common/aiCustomizationWorkspaceService.js';35import { ICustomizationHarnessService } from '../../../../workbench/contrib/chat/common/customizationHarnessService.js';36import { ILanguageModelsService } from '../../../../workbench/contrib/chat/common/languageModels.js';37import { IAgentPluginService } from '../../../../workbench/contrib/chat/common/plugins/agentPluginService.js';38import { IPromptsService } from '../../../../workbench/contrib/chat/common/promptSyntax/service/promptsService.js';39import { resolveCustomizationRefs } from '../../../../workbench/contrib/chat/browser/agentSessions/agentHost/agentHostLocalCustomizations.js';40import { IAgentHostFileSystemService } from '../../../../workbench/services/agentHost/common/agentHostFileSystemService.js';41import { IAuthenticationService } from '../../../../workbench/services/authentication/common/authentication.js';42import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';43import { remoteAgentHostSessionTypeId } from '../common/remoteAgentHostSessionType.js';44import { createRemoteAgentHarnessDescriptor, RemoteAgentCustomizationItemProvider, RemoteAgentPluginController } from './remoteAgentHostCustomizationHarness.js';45import { RemoteAgentHostSessionsProvider } from './remoteAgentHostSessionsProvider.js';46import { SyncedCustomizationBundler } from './syncedCustomizationBundler.js';47import { ISSHRemoteAgentHostService } from '../../../../platform/agentHost/common/sshRemoteAgentHost.js';48import { IAgentHostTerminalService } from '../../../../workbench/contrib/terminal/browser/agentHostTerminalService.js';49import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';50import { logTerminalRecovery } from '../../../common/sessionsTelemetry.js';5152/** Per-connection state bundle, disposed when a connection is removed. */53class ConnectionState extends Disposable {54readonly store = this._register(new DisposableStore());55readonly agents = this._register(new DisposableMap<AgentProvider, DisposableStore>());56readonly modelProviders = new Map<AgentProvider, AgentHostLanguageModelProvider>();57readonly loggedConnection: LoggingAgentConnection;58/** Dedupes redundant `authenticate` RPCs when the resolved token hasn't changed. */59readonly authTokenCache = new AgentHostAuthTokenCache();6061constructor(62readonly name: string | undefined,63connection: IAgentConnection,64channelId: string,65channelLabel: string,66@IInstantiationService instantiationService: IInstantiationService,67) {68super();69this.loggedConnection = this._register(instantiationService.createInstance(LoggingAgentConnection, connection, channelId, channelLabel));70}71}7273/**74* Discovers available agents from each connected remote agent host and75* dynamically registers each one as a chat session type with its own76* session handler and language model provider.77*78* Uses the same unified {@link AgentHostSessionHandler} as the local79* agent host, obtaining per-connection {@link IAgentConnection}80* instances from {@link IRemoteAgentHostService.getConnection}.81*/82export class RemoteAgentHostContribution extends Disposable implements IWorkbenchContribution {8384static readonly ID = 'sessions.contrib.remoteAgentHostContribution';8586/** Per-connection state: client state + per-agent registrations. */87private readonly _connections = this._register(new DisposableMap<string, ConnectionState>());8889/** Per-address sessions provider, registered for all configured entries. */90private readonly _providerStores = this._register(new DisposableMap<string, DisposableStore>());91private readonly _providerInstances = new Map<string, RemoteAgentHostSessionsProvider>();92private readonly _pendingSSHReconnects = new Set<string>();9394constructor(95@IRemoteAgentHostService private readonly _remoteAgentHostService: IRemoteAgentHostService,96@IChatSessionsService private readonly _chatSessionsService: IChatSessionsService,97@ILanguageModelsService private readonly _languageModelsService: ILanguageModelsService,98@ILogService private readonly _logService: ILogService,99@IInstantiationService private readonly _instantiationService: IInstantiationService,100@IAuthenticationService private readonly _authenticationService: IAuthenticationService,101@IDefaultAccountService private readonly _defaultAccountService: IDefaultAccountService,102@IFileDialogService private readonly _fileDialogService: IFileDialogService,103@IFileService private readonly _fileService: IFileService,104@INotificationService private readonly _notificationService: INotificationService,105@ISessionsProvidersService private readonly _sessionsProvidersService: ISessionsProvidersService,106@IConfigurationService private readonly _configurationService: IConfigurationService,107@IAgentHostFileSystemService private readonly _agentHostFileSystemService: IAgentHostFileSystemService,108@ISSHRemoteAgentHostService private readonly _sshService: ISSHRemoteAgentHostService,109@IAICustomizationWorkspaceService private readonly _customizationWorkspaceService: IAICustomizationWorkspaceService,110@ICustomizationHarnessService private readonly _customizationHarnessService: ICustomizationHarnessService,111@IStorageService private readonly _storageService: IStorageService,112@IAgentPluginService private readonly _agentPluginService: IAgentPluginService,113@IAgentHostTerminalService private readonly _agentHostTerminalService: IAgentHostTerminalService,114@ITelemetryService private readonly _telemetryService: ITelemetryService,115@IPromptsService private readonly _promptsService: IPromptsService,116) {117super();118119// Reconcile providers when configured entries change120this._register(this._configurationService.onDidChangeConfiguration(e => {121if (e.affectsConfiguration(RemoteAgentHostsSettingId) || e.affectsConfiguration(RemoteAgentHostsEnabledSettingId) || e.affectsConfiguration(RemoteAgentHostAutoConnectSettingId)) {122this._reconcile();123}124}));125126// Reconcile when connections change (added/removed/reconnected)127this._register(this._remoteAgentHostService.onDidChangeConnections(() => {128this._reconcile();129}));130131// Push auth token whenever the default account or sessions change132this._register(this._defaultAccountService.onDidChangeDefaultAccount(() => this._authenticateAllConnections()));133this._register(this._authenticationService.onDidChangeSessions(() => this._authenticateAllConnections()));134135// Initial setup for configured entries and connected remotes136this._reconcile();137}138139private _reconcile(): void {140this._reconcileProviders();141this._reconcileConnections();142this._reconnectSSHEntries();143144// Ensure every live connection is wired to its provider.145// This covers the case where a provider was recreated (e.g. name146// change) while a connection for that address already existed —147// we need to re-expose both the connection and the output channel,148// otherwise `Show Output` on the recreated provider would break.149for (const [address, connState] of this._connections) {150const connectionInfo = this._remoteAgentHostService.connections.find(c => c.address === address);151const provider = this._providerInstances.get(address);152if (provider) {153provider.setConnection(connState.loggedConnection, connectionInfo?.defaultDirectory);154provider.setOutputChannelId(connState.loggedConnection.channelId);155}156}157158// Update connection status on all providers (including those159// that are reconnecting and don't have an active connection).160for (const [address, provider] of this._providerInstances) {161const connectionInfo = this._remoteAgentHostService.connections.find(c => c.address === address);162if (connectionInfo) {163provider.setConnectionStatus(connectionInfo.status);164} else {165provider.setConnectionStatus(RemoteAgentHostConnectionStatus.Disconnected);166}167}168}169170private _reconcileProviders(): void {171const enabled = this._configurationService.getValue<boolean>(RemoteAgentHostsEnabledSettingId);172const entries = enabled ? this._remoteAgentHostService.configuredEntries : [];173const desiredAddresses = new Set(entries.map(e => getEntryAddress(e)));174175// Remove providers no longer configured176for (const [address] of this._providerStores) {177if (!desiredAddresses.has(address)) {178this._providerStores.deleteAndDispose(address);179}180}181182// Add or recreate providers for configured entries183for (const entry of entries) {184const address = getEntryAddress(entry);185const existing = this._providerInstances.get(address);186if (existing && existing.label !== (entry.name || address)) {187// Name changed — recreate since ISessionsProvider.label is readonly188this._providerStores.deleteAndDispose(address);189}190if (!this._providerStores.has(address)) {191this._createProvider(entry);192}193}194}195196private _createProvider(entry: IRemoteAgentHostEntry): void {197const address = getEntryAddress(entry);198const store = new DisposableStore();199const provider = this._instantiationService.createInstance(200RemoteAgentHostSessionsProvider, { address, name: entry.name });201store.add(provider);202store.add(this._sessionsProvidersService.registerProvider(provider));203this._providerInstances.set(address, provider);204store.add(toDisposable(() => this._providerInstances.delete(address)));205this._providerStores.set(address, store);206}207208/**209* Re-establish SSH connections for configured entries that have an210* sshConfigHost but no active connection.211*/212private _reconnectSSHEntries(): void {213const autoConnect = this._configurationService.getValue<boolean>(RemoteAgentHostAutoConnectSettingId);214const entries = this._remoteAgentHostService.configuredEntries;215for (const entry of entries) {216if (entry.connection.type !== RemoteAgentHostEntryType.SSH || !entry.connection.sshConfigHost) {217continue;218}219const address = getEntryAddress(entry);220const sshConfigHost = entry.connection.sshConfigHost;221// Skip if already connected or reconnecting222const hasConnection = this._remoteAgentHostService.connections.some(223c => c.address === address && c.status === RemoteAgentHostConnectionStatus.Connected224);225if (hasConnection || this._pendingSSHReconnects.has(sshConfigHost)) {226continue;227}228if (!autoConnect) {229continue;230}231this._pendingSSHReconnects.add(sshConfigHost);232this._logService.info(`[RemoteAgentHost] Re-establishing SSH tunnel for ${sshConfigHost}`);233this._sshService.reconnect(sshConfigHost, entry.name).then(() => {234this._pendingSSHReconnects.delete(sshConfigHost);235this._logService.info(`[RemoteAgentHost] SSH tunnel re-established for ${sshConfigHost}`);236}).catch(err => {237this._pendingSSHReconnects.delete(sshConfigHost);238this._logService.error(`[RemoteAgentHost] SSH reconnect failed for ${sshConfigHost}`, err);239// Host is unreachable — unpublish any cached sessions we240// were showing so the UI doesn't list stale entries for a241// host we cannot currently reach.242this._providerInstances.get(address)?.unpublishCachedSessions();243});244}245}246247private _reconcileConnections(): void {248const currentConnections = this._remoteAgentHostService.connections;249const connectedAddresses = new Set(250currentConnections251.filter(c => c.status === RemoteAgentHostConnectionStatus.Connected)252.map(c => c.address)253);254const allAddresses = new Set(currentConnections.map(c => c.address));255256// Remove contribution state for connections that are no longer present at all257for (const [address] of this._connections) {258if (!allAddresses.has(address)) {259this._logService.info(`[RemoteAgentHost] Removing contribution for ${address}`);260this._providerInstances.get(address)?.clearConnection();261this._connections.deleteAndDispose(address);262} else if (!connectedAddresses.has(address)) {263// Connection exists but is not connected (reconnecting or disconnected).264// Keep the contribution state but don't clear the provider —265// the session cache is preserved during reconnect.266}267}268269// Add or update connections270for (const connectionInfo of currentConnections) {271// Only set up contribution state for connected entries272if (connectionInfo.status !== RemoteAgentHostConnectionStatus.Connected) {273continue;274}275const existing = this._connections.get(connectionInfo.address);276if (existing) {277const nameChanged = existing.name !== connectionInfo.name;278const clientIdChanged = existing.loggedConnection.clientId !== connectionInfo.clientId;279280// If the name or clientId changed, tear down and re-register281if (nameChanged || clientIdChanged) {282this._logService.info(`[RemoteAgentHost] Reconnecting contribution for ${connectionInfo.address}: oldClientId=${existing.loggedConnection.clientId}, newClientId=${connectionInfo.clientId}, nameChanged=${nameChanged}`);283const oldClientId = existing.loggedConnection.clientId;284this._connections.deleteAndDispose(connectionInfo.address);285this._setupConnection(connectionInfo);286287// Reconnect active terminals only when the backing288// client changed. Name-only updates don't invalidate289// subscriptions and would cause unnecessary buffer290// clear/replay flicker.291if (clientIdChanged) {292const newConnection = this._remoteAgentHostService.getConnection(connectionInfo.address);293if (newConnection) {294this._agentHostTerminalService.reconnectTerminals(newConnection, oldClientId).then(295({ recovered, total }) => {296if (total > 0) {297this._logService.info(`[RemoteAgentHost] Terminal reconnection: ${recovered}/${total} recovered`);298logTerminalRecovery(this._telemetryService, { recoveredCount: recovered, totalCount: total });299}300},301err => this._logService.warn('[RemoteAgentHost] Terminal reconnection failed', err)302);303}304}305}306} else {307this._setupConnection(connectionInfo);308}309}310}311312private _setupConnection(connectionInfo: IRemoteAgentHostConnectionInfo): void {313const connection = this._remoteAgentHostService.getConnection(connectionInfo.address);314if (!connection) {315return;316}317318const { address, name } = connectionInfo;319const channelLabel = `Agent Host (${name || address})`;320const connState = this._instantiationService.createInstance(ConnectionState, name, connection, `agenthost.${connection.clientId}`, channelLabel);321const loggedConnection = connState.loggedConnection;322this._connections.set(address, connState);323const store = connState.store;324325// Track authority -> connection mapping for FS provider routing326const authority = agentHostAuthority(address);327store.add(this._agentHostFileSystemService.registerAuthority(authority, connection));328329// React to root state changes (agent discovery)330store.add(loggedConnection.rootState.onDidChange(rootState => {331this._handleRootStateChange(address, loggedConnection, rootState);332}));333334// If root state is already available, process it immediately335const initialRootState = loggedConnection.rootState.value;336if (initialRootState && !(initialRootState instanceof Error)) {337this._handleRootStateChange(address, loggedConnection, initialRootState);338}339340// Wire connection to existing sessions provider341const provider = this._providerInstances.get(address);342if (provider) {343provider.setConnection(loggedConnection, connectionInfo.defaultDirectory);344// Expose the output channel ID so the workspace picker can offer "Show Output"345provider.setOutputChannelId(loggedConnection.channelId);346}347}348349private _handleRootStateChange(address: string, loggedConnection: LoggingAgentConnection, rootState: RootState): void {350const connState = this._connections.get(address);351if (!connState) {352return;353}354355const incoming = new Set(rootState.agents.map(a => a.provider));356357// Remove agents no longer present358for (const [provider] of connState.agents) {359if (!incoming.has(provider)) {360connState.agents.deleteAndDispose(provider);361connState.modelProviders.delete(provider);362}363}364365// Authenticate using protectedResources from agent info366this._authenticateWithConnection(address, loggedConnection, rootState.agents)367.catch(() => { /* best-effort */ });368369// Register new agents, push model updates to existing ones370for (const agent of rootState.agents) {371if (!connState.agents.has(agent.provider)) {372this._registerAgent(address, loggedConnection, agent, connState.name);373} else {374const modelProvider = connState.modelProviders.get(agent.provider);375modelProvider?.updateModels(agent.models);376}377}378}379380private _registerAgent(address: string, loggedConnection: LoggingAgentConnection, agent: AgentInfo, configuredName: string | undefined): void {381const connState = this._connections.get(address);382if (!connState) {383return;384}385386const agentStore = new DisposableStore();387connState.agents.set(agent.provider, agentStore);388connState.store.add(agentStore);389390const sanitized = agentHostAuthority(address);391const providerId = `agenthost-${sanitized}`;392const sessionType = remoteAgentHostSessionTypeId(sanitized, agent.provider);393const agentId = sessionType;394const vendor = sessionType;395396// User-facing display name for this agent. We always include the397// agent's own name so that a host exposing multiple agents (e.g.398// `copilot` + `openai` from the same machine) produces distinct399// labels instead of collapsing to a single `configuredName`.400const hostLabel = configuredName || address;401const agentLabel = agent.displayName?.trim() || agent.provider;402const displayName = `${agentLabel} [${hostLabel}]`;403404// Per-agent working directory cache, scoped to the agent store lifetime405const sessionWorkingDirs = new Map<string, URI>();406agentStore.add(toDisposable(() => sessionWorkingDirs.clear()));407408// Capture the working directory from the session that is being created.409const resolveWorkingDirectory = (sessionResource: URI): URI | undefined => {410const resourceKey = sessionResource.toString();411const cached = sessionWorkingDirs.get(resourceKey);412if (cached) {413return cached;414}415const provider = this._sessionsProvidersService.getProvider<RemoteAgentHostSessionsProvider>(providerId);416const session = provider?.getSessionByResource(sessionResource);417const repository = session?.workspace.get()?.repositories[0];418const workingDirectory = repository?.workingDirectory ?? repository?.uri;419if (workingDirectory) {420sessionWorkingDirs.set(resourceKey, workingDirectory);421return workingDirectory;422}423return undefined;424};425426// Chat session contribution427agentStore.add(this._chatSessionsService.registerChatSessionContribution({428type: sessionType,429name: agentId,430displayName,431description: agent.description,432canDelegate: true,433requiresCustomModels: true,434supportsDelegation: false,435capabilities: {436supportsCheckpoints: true,437supportsPromptAttachments: true,438},439}));440441// Customization harness for this remote agent442const pluginController = agentStore.add(new RemoteAgentPluginController(443hostLabel,444sanitized,445loggedConnection,446this._fileDialogService,447this._notificationService,448this._customizationWorkspaceService,449));450const itemProvider = agentStore.add(new RemoteAgentCustomizationItemProvider(agent, loggedConnection, sanitized, pluginController, this._fileService, this._logService));451const syncProvider = agentStore.add(new AgentCustomizationSyncProvider(sessionType, this._storageService));452const harnessDescriptor = createRemoteAgentHarnessDescriptor(sessionType, displayName, pluginController, itemProvider, syncProvider);453agentStore.add(this._customizationHarnessService.registerExternalHarness(harnessDescriptor));454455// Bundler for packaging individual files into a virtual Open Plugin456const bundler = agentStore.add(this._instantiationService.createInstance(SyncedCustomizationBundler, sessionType));457458// Agent-level customizations observable459const customizations = observableValue<CustomizationRef[]>('agentCustomizations', []);460const updateCustomizations = async () => {461const refs = await resolveCustomizationRefs(this._promptsService, syncProvider, this._agentPluginService, bundler);462customizations.set(refs, undefined);463};464agentStore.add(syncProvider.onDidChange(() => updateCustomizations()));465agentStore.add(Event.any(466this._promptsService.onDidChangeCustomAgents,467this._promptsService.onDidChangeSlashCommands,468this._promptsService.onDidChangeSkills,469this._promptsService.onDidChangeInstructions,470)(() => updateCustomizations()));471updateCustomizations(); // resolve initial state472473// Session handler (unified)474const sessionHandler = agentStore.add(this._instantiationService.createInstance(475AgentHostSessionHandler, {476provider: agent.provider,477agentId,478sessionType,479fullName: displayName,480description: agent.description,481connection: loggedConnection,482connectionAuthority: sanitized,483extensionId: 'vscode.remote-agent-host',484extensionDisplayName: 'Remote Agent Host',485resolveWorkingDirectory,486resolveAuthentication: (resources) => this._resolveAuthenticationInteractively(address, loggedConnection, resources),487customizations,488}));489agentStore.add(this._chatSessionsService.registerChatSessionContentProvider(sessionType, sessionHandler));490491// Language model provider.492// Order matters: `updateModels` must be called after493// `registerLanguageModelProvider` so the initial `onDidChange` is observed.494const vendorDescriptor = { vendor, displayName, configuration: undefined, managementCommand: undefined, when: undefined };495this._languageModelsService.deltaLanguageModelChatProviderDescriptors([vendorDescriptor], []);496agentStore.add(toDisposable(() => this._languageModelsService.deltaLanguageModelChatProviderDescriptors([], [vendorDescriptor])));497const modelProvider = agentStore.add(new AgentHostLanguageModelProvider(sessionType, vendor));498connState.modelProviders.set(agent.provider, modelProvider);499agentStore.add(toDisposable(() => connState.modelProviders.delete(agent.provider)));500agentStore.add(this._languageModelsService.registerLanguageModelProvider(vendor, modelProvider));501modelProvider.updateModels(agent.models);502503this._logService.info(`[RemoteAgentHost] Registered agent ${agent.provider} from ${address} as ${sessionType}`);504}505506private _authenticateAllConnections(): void {507for (const [address, connState] of this._connections) {508const rootState = connState.loggedConnection.rootState.value;509if (rootState && !(rootState instanceof Error)) {510this._authenticateWithConnection(address, connState.loggedConnection, rootState.agents).catch(() => { /* best-effort */ });511}512}513}514515/**516* Authenticate using protectedResources from agent info in root state.517* Resolves tokens via the standard VS Code authentication service.518*519* Marks the matching provider's `authenticationPending` observable while520* the auth pass is in flight so that sessions surface as still loading.521*/522private async _authenticateWithConnection(address: string, loggedConnection: LoggingAgentConnection, agents: readonly AgentInfo[]): Promise<void> {523const providerId = `agenthost-${agentHostAuthority(address)}`;524const provider = this._sessionsProvidersService.getProvider<RemoteAgentHostSessionsProvider>(providerId);525const authTokenCache = this._connections.get(address)?.authTokenCache;526provider?.setAuthenticationPending(true);527try {528await authenticateProtectedResources(agents, {529authTokenCache,530authenticationService: this._authenticationService,531logPrefix: '[RemoteAgentHost]',532logService: this._logService,533authenticate: request => loggedConnection.authenticate(request),534});535} catch (err) {536this._logService.error('[RemoteAgentHost] Failed to authenticate with connection', err);537loggedConnection.logError('authenticateWithConnection', err);538} finally {539provider?.setAuthenticationPending(false);540}541}542543/**544* Interactively prompt the user to authenticate when the server requires it.545* Returns true if authentication succeeded.546*/547private async _resolveAuthenticationInteractively(address: string, loggedConnection: LoggingAgentConnection, protectedResources: readonly ProtectedResourceMetadata[]): Promise<boolean> {548const authTokenCache = this._connections.get(address)?.authTokenCache;549try {550return await resolveAuthenticationInteractively(protectedResources, {551authTokenCache,552authenticationService: this._authenticationService,553logPrefix: '[RemoteAgentHost]',554logService: this._logService,555authenticate: request => loggedConnection.authenticate(request),556});557} catch (err) {558this._logService.error('[RemoteAgentHost] Interactive authentication failed', err);559loggedConnection.logError('resolveAuthenticationInteractively', err);560}561return false;562}563}564565registerWorkbenchContribution2(RemoteAgentHostContribution.ID, RemoteAgentHostContribution, WorkbenchPhase.AfterRestored);566567Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).registerConfiguration({568properties: {569[RemoteAgentHostsEnabledSettingId]: {570type: 'boolean',571description: nls.localize('chat.remoteAgentHosts.enabled', "Enable connecting to remote agent hosts."),572default: product.quality !== 'stable',573scope: ConfigurationScope.APPLICATION,574tags: ['experimental', 'advanced'],575},576[RemoteAgentHostAutoConnectSettingId]: {577type: 'boolean',578description: 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."),579default: true,580scope: ConfigurationScope.APPLICATION,581tags: ['experimental', 'advanced'],582},583'chat.sshRemoteAgentHostCommand': {584type: 'string',585description: 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./"),586default: '',587scope: ConfigurationScope.APPLICATION,588tags: ['experimental', 'advanced'],589},590'chat.agentHost.forwardSSHAgent': {591type: 'boolean',592description: 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."),593default: false,594scope: ConfigurationScope.APPLICATION,595tags: ['experimental', 'advanced'],596},597[RemoteAgentHostsSettingId]: {598type: 'array',599items: {600type: 'object',601properties: {602address: { type: 'string', description: nls.localize('chat.remoteAgentHosts.address', "The address of the remote agent host (e.g. \"localhost:3000\").") },603name: { type: 'string', description: nls.localize('chat.remoteAgentHosts.name', "A display name for this remote agent host.") },604connectionToken: { type: 'string', description: nls.localize('chat.remoteAgentHosts.connectionToken', "An optional connection token for authenticating with the remote agent host.") },605sshConfigHost: { type: 'string', description: nls.localize('chat.remoteAgentHosts.sshConfigHost', "SSH config host alias for automatic reconnection via SSH tunnel.") },606},607required: ['address', 'name'],608},609description: nls.localize('chat.remoteAgentHosts', "A list of remote agent host addresses to connect to (e.g. \"localhost:3000\")."),610default: [],611scope: ConfigurationScope.APPLICATION,612tags: ['experimental', 'advanced'],613},614[TunnelAgentHostsSettingId]: {615type: 'array',616items: { type: 'string' },617description: 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."),618default: [],619scope: ConfigurationScope.APPLICATION,620tags: ['experimental', 'advanced'],621},622},623});624625// Side-effect registrations for the remote agent host feature626import './remoteAgentHostActions.js';627import './manageRemoteAgentHosts.js';628import '../../chat/browser/agentHost/agentHostModelPicker.js';629630631