Path: blob/main/src/vs/platform/agentHost/node/agentHostMain.ts
13394 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 { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';6import { Server as ChildProcessServer } from '../../../base/parts/ipc/node/ipc.cp.js';7import { Server as UtilityProcessServer } from '../../../base/parts/ipc/node/ipc.mp.js';8import { isUtilityProcess } from '../../../base/parts/sandbox/node/electronTypes.js';9import { Emitter } from '../../../base/common/event.js';10import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';11import { joinPath } from '../../../base/common/resources.js';12import { isWindows } from '../../../base/common/platform.js';13import { URI } from '../../../base/common/uri.js';14import { generateUuid } from '../../../base/common/uuid.js';15import * as os from 'os';16import * as inspector from 'inspector';17import { AgentHostIpcChannels, IAgentHostInspectInfo, IAgentHostSocketInfo, IConnectionTrackerService } from '../common/agentService.js';18import { AgentService } from './agentService.js';19import { IAgentConfigurationService } from './agentConfigurationService.js';20import { IAgentHostTerminalManager } from './agentHostTerminalManager.js';21import { CopilotAgent } from './copilot/copilotAgent.js';22import { CopilotApiService, ICopilotApiService } from './shared/copilotApiService.js';23import { ProtocolServerHandler } from './protocolServerHandler.js';24import { WebSocketProtocolServer } from './webSocketTransport.js';25import { INativeEnvironmentService } from '../../environment/common/environment.js';26import { NativeEnvironmentService } from '../../environment/node/environmentService.js';27import { parseArgs, OPTIONS } from '../../environment/node/argv.js';28import { getLogLevel, ILogService } from '../../log/common/log.js';29import { LogService } from '../../log/common/logService.js';30import { LoggerService } from '../../log/node/loggerService.js';31import { LoggerChannel } from '../../log/common/logIpc.js';32import { DefaultURITransformer } from '../../../base/common/uriIpc.js';33import product from '../../product/common/product.js';34import { IProductService } from '../../product/common/productService.js';35import { localize } from '../../../nls.js';36import { FileService } from '../../files/common/fileService.js';37import { IFileService } from '../../files/common/files.js';38import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.js';39import { Schemas } from '../../../base/common/network.js';40import { InstantiationService } from '../../instantiation/common/instantiationService.js';41import { ServiceCollection } from '../../instantiation/common/serviceCollection.js';42import { SessionDataService } from './sessionDataService.js';43import { ISessionDataService } from '../common/sessionDataService.js';44import { IDiffComputeService } from '../common/diffComputeService.js';45import { NodeWorkerDiffComputeService } from './diffComputeService.js';46import { AgentHostClientFileSystemProvider } from '../common/agentHostClientFileSystemProvider.js';47import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js';48import { AGENT_HOST_CLIENT_RESOURCE_CHANNEL, createAgentHostClientResourceConnection } from '../common/agentHostClientResourceChannel.js';49import { IAgentPluginManager } from '../common/agentPluginManager.js';50import { AgentPluginManager } from './agentPluginManager.js';51import { AgentHostGitService, IAgentHostGitService } from './agentHostGitService.js';52import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js';53import { join } from '../../../base/common/path.js';5455// Entry point for the agent host utility process.56// Sets up IPC, logging, and registers agent providers (Copilot).57// When VSCODE_AGENT_HOST_PORT or VSCODE_AGENT_HOST_SOCKET_PATH env vars58// are set, also starts a WebSocket server for external clients.5960startAgentHost();6162function startAgentHost(): void {63// Setup RPC - supports both Electron utility process and Node child process64let server: ChildProcessServer<string> | UtilityProcessServer;65if (isUtilityProcess(process)) {66server = new UtilityProcessServer();67} else {68server = new ChildProcessServer(AgentHostIpcChannels.AgentHost);69}7071const disposables = new DisposableStore();7273// Services74const productService: IProductService = { _serviceBrand: undefined, ...product };75const environmentService = new NativeEnvironmentService(parseArgs(process.argv, OPTIONS), productService);76const loggerService = new LoggerService(getLogLevel(environmentService), environmentService.logsHome);77server.registerChannel(AgentHostIpcChannels.Logger, new LoggerChannel(loggerService, () => DefaultURITransformer));78const logger = loggerService.createLogger('agenthost', { name: localize('agentHost', "Agent Host") });79const logService = new LogService(logger);80logService.info('Agent Host process started successfully');8182// File service83const fileService = disposables.add(new FileService(logService));84disposables.add(fileService.registerProvider(Schemas.file, disposables.add(new DiskFileSystemProvider(logService))));85// In-memory filesystem backing transient file-edit previews shown during86// tool-call confirmations.87disposables.add(registerPendingEditContentProvider(fileService));8889// Session data service90const sessionDataService = new SessionDataService(URI.file(environmentService.userDataPath), fileService, logService);91const rootConfigResource = joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-config.json');9293// Create the real service implementation that lives in this process94let agentService: AgentService;95try {96// Build the DI container early so the git service can be created via97// `createInstance` (it needs IFileService + INativeEnvironmentService).98const diServices = new ServiceCollection();99diServices.set(INativeEnvironmentService, environmentService);100diServices.set(ILogService, logService);101diServices.set(IFileService, fileService);102diServices.set(ISessionDataService, sessionDataService);103diServices.set(IProductService, productService);104const instantiationService = new InstantiationService(diServices);105const gitService = instantiationService.createInstance(AgentHostGitService);106diServices.set(IAgentHostGitService, gitService);107const copilotApiService = instantiationService.createInstance(CopilotApiService, undefined);108diServices.set(ICopilotApiService, copilotApiService);109agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, rootConfigResource);110const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService);111diServices.set(IAgentPluginManager, pluginManager);112const diffComputeService = disposables.add(new NodeWorkerDiffComputeService(logService));113diServices.set(IDiffComputeService, diffComputeService);114115diServices.set(IAgentHostTerminalManager, agentService.terminalManager);116diServices.set(IAgentConfigurationService, agentService.configurationService);117agentService.registerProvider(instantiationService.createInstance(CopilotAgent));118} catch (err) {119logService.error('Failed to create AgentService', err);120throw err;121}122const agentChannel = ProxyChannel.fromService(agentService, disposables);123server.registerChannel(AgentHostIpcChannels.AgentHost, agentChannel);124125// Single shared `vscode-agent-client` filesystem provider. Per-client126// authorities are added either by ProtocolServerHandler (for WebSocket127// transports) or by the IPC connection lifecycle below (for the local128// in-process renderer-to-utility-process MessagePort transport).129const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider());130disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, clientFileSystemProvider));131132// Wire reverse-RPC for in-process renderer connections. The renderer's133// `MessagePortClient` ctx is its `clientId`, and it exposes134// `AGENT_HOST_CLIENT_RESOURCE_CHANNEL` for filesystem reads.135if (server instanceof UtilityProcessServer) {136const authorityRegistrations = new Map<unknown, IDisposable>();137disposables.add(server.onDidAddConnection(connection => {138const clientId = connection.ctx;139if (typeof clientId !== 'string' || !clientId) {140return;141}142const channel = server.getChannel(AGENT_HOST_CLIENT_RESOURCE_CHANNEL, c => c.ctx === clientId);143const fsConnection = createAgentHostClientResourceConnection(channel);144authorityRegistrations.set(connection, clientFileSystemProvider.registerAuthority(clientId, fsConnection));145}));146disposables.add(server.onDidRemoveConnection(connection => {147const reg = authorityRegistrations.get(connection);148if (reg) {149reg.dispose();150authorityRegistrations.delete(connection);151}152}));153}154155// Expose the WebSocket client connection count to the parent process via IPC.156// This is NOT part of the agent host protocol -- it is only used by the157// server process to manage the agent host process lifetime.158const connectionCountEmitter = disposables.add(new Emitter<number>());159let dynamicSocketInfo: IAgentHostSocketInfo | undefined;160const connectionTrackerService: IConnectionTrackerService = {161onDidChangeConnectionCount: connectionCountEmitter.event,162async startWebSocketServer(): Promise<IAgentHostSocketInfo> {163if (dynamicSocketInfo) {164return dynamicSocketInfo;165}166167const socketPath = isWindows168? `\\\\.\\pipe\\vscode-agent-host-${generateUuid().replace(/-/g, '')}`169: join(os.tmpdir(), `vscode-agent-host-${generateUuid().replace(/-/g, '')}.sock`);170171const wsServer = disposables.add(await WebSocketProtocolServer.create(172{ socketPath },173logService,174));175176const protocolHandler = disposables.add(new ProtocolServerHandler(177agentService,178agentService.stateManager,179wsServer,180{ defaultDirectory: URI.file(os.homedir()).toString() },181clientFileSystemProvider,182logService,183));184disposables.add(protocolHandler.onDidChangeConnectionCount(count => connectionCountEmitter.fire(count)));185186logService.info(`[AgentHost] Dynamic WebSocket server listening on ${socketPath}`);187dynamicSocketInfo = { socketPath };188return dynamicSocketInfo;189},190async getInspectInfo(tryEnable: boolean): Promise<IAgentHostInspectInfo | undefined> {191let url = inspector.url();192if (!url && tryEnable) {193try {194inspector.open(0, '127.0.0.1', false);195} catch (err) {196logService.error('[AgentHost] Failed to open inspector', err);197return undefined;198}199url = inspector.url();200}201if (!url) {202return undefined;203}204// Inspector URL looks like: ws://host:port/uuid (host may be IPv6 in brackets)205try {206const parsedUrl = new URL(url);207if (parsedUrl.protocol !== 'ws:') {208logService.warn(`[AgentHost] Unexpected inspector URL: ${url}`);209return undefined;210}211212const port = Number(parsedUrl.port);213const auth = parsedUrl.pathname.replace(/^\/+/, '');214if (!Number.isInteger(port) || !auth) {215logService.warn(`[AgentHost] Unexpected inspector URL: ${url}`);216return undefined;217}218219const host = parsedUrl.hostname === '0.0.0.0'220? '127.0.0.1'221: parsedUrl.hostname === '::'222? '::1'223: parsedUrl.hostname;224const devtoolsHost = host.includes(':') ? `[${host}]` : host;225226return {227host,228port,229devtoolsUrl: `devtools://devtools/bundled/js_app.html?v8only=true&ws=${devtoolsHost}:${parsedUrl.port}/${auth}`,230};231} catch {232logService.warn(`[AgentHost] Unexpected inspector URL: ${url}`);233return undefined;234}235},236};237const connectionTrackerChannel = ProxyChannel.fromService(connectionTrackerService, disposables);238server.registerChannel(AgentHostIpcChannels.ConnectionTracker, connectionTrackerChannel);239240// Start WebSocket server for external clients if configured (env-var flow for CLI/server)241startWebSocketServer(agentService, clientFileSystemProvider, logService, disposables, count => connectionCountEmitter.fire(count)).catch(err => {242logService.error('Failed to start WebSocket server', err);243});244245process.once('exit', () => {246agentService.dispose();247logService.dispose();248disposables.dispose();249});250}251252/**253* When the parent process passes WebSocket configuration via environment254* variables, start a protocol server that external clients can connect to.255* This reuses the same {@link AgentService} and {@link AgentHostStateManager}256* that the IPC channel uses, so both IPC and WebSocket clients share state.257*/258async function startWebSocketServer(agentService: AgentService, clientFileSystemProvider: AgentHostClientFileSystemProvider, logService: ILogService, disposables: DisposableStore, onConnectionCountChanged: (count: number) => void): Promise<void> {259const port = process.env['VSCODE_AGENT_HOST_PORT'];260const socketPath = process.env['VSCODE_AGENT_HOST_SOCKET_PATH'];261262if (!port && !socketPath) {263return;264}265266const connectionToken = process.env['VSCODE_AGENT_HOST_CONNECTION_TOKEN'];267const host = process.env['VSCODE_AGENT_HOST_HOST'] || 'localhost';268269const wsServer = disposables.add(await WebSocketProtocolServer.create(270socketPath271? {272socketPath,273connectionTokenValidate: connectionToken274? (token) => token === connectionToken275: undefined,276}277: {278port: parseInt(port!, 10),279host,280connectionTokenValidate: connectionToken281? (token) => token === connectionToken282: undefined,283},284logService,285));286287const protocolHandler = disposables.add(new ProtocolServerHandler(288agentService,289agentService.stateManager,290wsServer,291{ defaultDirectory: URI.file(os.homedir()).toString() },292clientFileSystemProvider,293logService,294));295disposables.add(protocolHandler.onDidChangeConnectionCount(onConnectionCountChanged));296297const listenTarget = socketPath ?? `${host}:${port}`;298logService.info(`[AgentHost] WebSocket server listening on ${listenTarget}`);299// Do not change this line. The CLI looks for this in the output.300console.log(`Agent host server listening on ${listenTarget}`);301}302303304