Path: blob/main/src/vs/platform/agentHost/node/agentHostServerMain.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*--------------------------------------------------------------------------------------------*/45// Standalone agent host server with WebSocket protocol transport.6// Start with: node out/vs/platform/agentHost/node/agentHostServerMain.js [--port <port>] [--host <host>] [--connection-token <token>] [--connection-token-file <path>] [--without-connection-token] [--enable-mock-agent] [--quiet] [--log <level>]78import { fileURLToPath } from 'url';910// This standalone process isn't bootstrapped via bootstrap-esm.ts, so we must11// set _VSCODE_FILE_ROOT ourselves so that FileAccess can resolve module paths.12// This file lives at out/vs/platform/agentHost/node/ - the root is `out/`.13globalThis._VSCODE_FILE_ROOT = fileURLToPath(new URL('../../../..', import.meta.url));1415import * as fs from 'fs';16import * as os from 'os';17import { DisposableStore } from '../../../base/common/lifecycle.js';18import { raceTimeout } from '../../../base/common/async.js';19import { joinPath } from '../../../base/common/resources.js';20import { URI } from '../../../base/common/uri.js';21import { generateUuid } from '../../../base/common/uuid.js';22import { localize } from '../../../nls.js';23import { NativeEnvironmentService } from '../../environment/node/environmentService.js';24import { INativeEnvironmentService } from '../../environment/common/environment.js';25import { parseArgs, OPTIONS } from '../../environment/node/argv.js';26import { getLogLevel, ILogService, NullLogService } from '../../log/common/log.js';27import { LogService } from '../../log/common/logService.js';28import { LoggerService } from '../../log/node/loggerService.js';29import product from '../../product/common/product.js';30import { IProductService } from '../../product/common/productService.js';31import { InstantiationService } from '../../instantiation/common/instantiationService.js';32import { ServiceCollection } from '../../instantiation/common/serviceCollection.js';33import { CopilotAgent } from './copilot/copilotAgent.js';34import { AgentService } from './agentService.js';35import { IAgentConfigurationService } from './agentConfigurationService.js';36import { IAgentHostTerminalManager } from './agentHostTerminalManager.js';37import { WebSocketProtocolServer } from './webSocketTransport.js';38import { ProtocolServerHandler } from './protocolServerHandler.js';39import { FileService } from '../../files/common/fileService.js';40import { IFileService } from '../../files/common/files.js';41import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.js';42import { Schemas } from '../../../base/common/network.js';43import { ISessionDataService } from '../common/sessionDataService.js';44import { IDiffComputeService } from '../common/diffComputeService.js';45import { NodeWorkerDiffComputeService } from './diffComputeService.js';46import { SessionDataService } from './sessionDataService.js';47import { AgentHostClientFileSystemProvider } from '../common/agentHostClientFileSystemProvider.js';48import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js';49import { resolveServerUrls } from './serverUrls.js';50import { AgentPluginManager } from './agentPluginManager.js';51import { IAgentPluginManager } from '../common/agentPluginManager.js';52import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js';53import { AgentHostGitService, IAgentHostGitService } from './agentHostGitService.js';5455/** Log to stderr so messages appear in the terminal alongside the process. */56function log(msg: string): void {57process.stderr.write(`[AgentHostServer] ${msg}\n`);58}5960// ---- Options ----------------------------------------------------------------6162const connectionTokenRegex = /^[0-9A-Za-z_-]+$/;6364interface IServerOptions {65readonly port: number;66readonly host: string | undefined;67readonly enableMockAgent: boolean;68readonly quiet: boolean;69/** Connection token string, or `undefined` when `--without-connection-token`. */70readonly connectionToken: string | undefined;71}7273function parseServerOptions(): IServerOptions {74const argv = process.argv.slice(2);75const envPort = parseInt(process.env['VSCODE_AGENT_HOST_PORT'] ?? '8081', 10);76const portIdx = argv.indexOf('--port');77const port = portIdx >= 0 ? parseInt(argv[portIdx + 1], 10) : envPort;78const hostIdx = argv.indexOf('--host');79const host = hostIdx >= 0 ? argv[hostIdx + 1] : undefined;80const enableMockAgent = argv.includes('--enable-mock-agent');81const quiet = argv.includes('--quiet');8283// Connection token84const withoutConnectionToken = argv.includes('--without-connection-token');85const connectionTokenIdx = argv.indexOf('--connection-token');86const connectionTokenFileIdx = argv.indexOf('--connection-token-file');87const rawToken = connectionTokenIdx >= 0 ? argv[connectionTokenIdx + 1] : undefined;88const tokenFilePath = connectionTokenFileIdx >= 0 ? argv[connectionTokenFileIdx + 1] : undefined;8990let connectionToken: string | undefined;91if (withoutConnectionToken) {92if (rawToken !== undefined || tokenFilePath !== undefined) {93log('Error: --without-connection-token cannot be used with --connection-token or --connection-token-file');94process.exit(1);95}96connectionToken = undefined;97} else if (tokenFilePath !== undefined) {98if (rawToken !== undefined) {99log('Error: --connection-token cannot be used with --connection-token-file');100process.exit(1);101}102try {103connectionToken = fs.readFileSync(tokenFilePath).toString().replace(/\r?\n$/, '');104} catch {105log(`Error: Unable to read connection token file at '${tokenFilePath}'`);106process.exit(1);107}108if (!connectionTokenRegex.test(connectionToken!)) {109log(`Error: The connection token in '${tokenFilePath}' does not adhere to the characters 0-9, a-z, A-Z, _, or -.`);110process.exit(1);111}112} else if (rawToken !== undefined) {113if (!connectionTokenRegex.test(rawToken)) {114log(`Error: The connection token '${rawToken}' does not adhere to the characters 0-9, a-z, A-Z, _, or -.`);115process.exit(1);116}117connectionToken = rawToken;118} else {119// Default: generate a random token (secure by default)120connectionToken = generateUuid();121}122123return { port, host, enableMockAgent, quiet, connectionToken };124}125126// ---- Main -------------------------------------------------------------------127128async function main(): Promise<void> {129const options = parseServerOptions();130const disposables = new DisposableStore();131132// Services133const productService: IProductService = { _serviceBrand: undefined, ...product };134const args = parseArgs(process.argv.slice(2), OPTIONS);135const environmentService = new NativeEnvironmentService(args, productService);136137// Logging — production logging unless --quiet138let logService: ILogService;139let loggerService: LoggerService | undefined;140141if (options.quiet) {142logService = new NullLogService();143} else {144const services = new ServiceCollection();145services.set(IProductService, productService);146services.set(INativeEnvironmentService, environmentService);147loggerService = new LoggerService(getLogLevel(environmentService), environmentService.logsHome);148const logger = loggerService.createLogger('agenthost-server', { name: localize('agentHostServer', "Agent Host Server") });149logService = disposables.add(new LogService(logger));150services.set(ILogService, logService);151log('Starting standalone agent host server');152}153154logService.info('[AgentHostServer] Starting standalone agent host server');155156// File service157const fileService = disposables.add(new FileService(logService));158disposables.add(fileService.registerProvider(Schemas.file, disposables.add(new DiskFileSystemProvider(logService))));159// In-memory filesystem backing transient file-edit previews shown during160// tool-call confirmations.161disposables.add(registerPendingEditContentProvider(fileService));162163// Session data service164const sessionDataService = new SessionDataService(URI.file(environmentService.userDataPath), fileService, logService);165const rootConfigResource = joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-config.json');166167// Build the DI container early so the git service can be created via168// `createInstance` (it needs IFileService + INativeEnvironmentService).169// The git service is shared by AgentService (for diff computation +170// showBlob) and the production agent registration path.171const diServices = new ServiceCollection();172diServices.set(IProductService, productService);173diServices.set(INativeEnvironmentService, environmentService);174diServices.set(ILogService, logService);175diServices.set(IFileService, fileService);176diServices.set(ISessionDataService, sessionDataService);177const instantiationService = new InstantiationService(diServices);178const gitService = instantiationService.createInstance(AgentHostGitService);179180// Create the agent service (owns AgentHostStateManager + AgentSideEffects internally)181const agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, rootConfigResource);182disposables.add(agentService);183184// Register agents185if (!options.quiet) {186// Production agents (require DI)187const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService);188diServices.set(IAgentPluginManager, pluginManager);189diServices.set(IDiffComputeService, disposables.add(new NodeWorkerDiffComputeService(logService)));190diServices.set(IAgentHostTerminalManager, agentService.terminalManager);191diServices.set(IAgentConfigurationService, agentService.configurationService);192diServices.set(IAgentHostGitService, gitService);193const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent));194agentService.registerProvider(copilotAgent);195log('CopilotAgent registered');196}197198if (options.enableMockAgent) {199// Dynamic import to avoid bundling test code in production200import('../test/node/mockAgent.js').then(({ ScriptedMockAgent }) => {201const mockAgent = disposables.add(new ScriptedMockAgent());202agentService.registerProvider(mockAgent);203}).catch(err => {204logService.error('[AgentHostServer] Failed to load mock agent', err);205});206}207208// WebSocket server209const wsServer = disposables.add(await WebSocketProtocolServer.create({210port: options.port,211host: options.host,212connectionTokenValidate: options.connectionToken213? token => token === options.connectionToken214: undefined,215}, logService));216217218const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider());219disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, clientFileSystemProvider));220221// Wire up protocol handler222disposables.add(new ProtocolServerHandler(223agentService,224agentService.stateManager,225wsServer,226{ defaultDirectory: URI.file(os.homedir()).toString() },227clientFileSystemProvider,228logService,229));230231// Report ready232function reportReady(addr: string): void {233const listeningPort = Number(addr.split(':').pop());234process.stdout.write(`READY:${listeningPort}\n`);235236const urls = resolveServerUrls(options.host, listeningPort);237for (const url of urls.local) {238log(` Local: ${url}`);239logService.info(`[AgentHostServer] Local: ${url}`);240}241for (const url of urls.network) {242log(` Network: ${url}`);243logService.info(`[AgentHostServer] Network: ${url}`);244}245if (urls.network.length === 0 && options.host === undefined) {246log(' Network: use --host to expose');247logService.info('[AgentHostServer] Network: use --host to expose');248}249}250251const address = wsServer.address;252if (address) {253reportReady(address);254} else {255const interval = setInterval(() => {256const addr = wsServer.address;257if (addr) {258clearInterval(interval);259reportReady(addr);260}261}, 10);262}263264// Keep alive until stdin closes or signal265process.stdin.resume();266process.stdin.on('end', () => { void shutdown(); });267process.on('SIGTERM', () => { void shutdown(); });268process.on('SIGINT', () => { void shutdown(); });269270let shuttingDown = false;271async function shutdown(): Promise<void> {272if (shuttingDown) {273return;274}275shuttingDown = true;276logService.info('[AgentHostServer] Shutting down...');277// Close the WebSocket server first so no further actions can be278// dispatched while we wait for in-flight writes to flush — otherwise279// a late-arriving action could keep queuing DB writes and either280// undermine the flush or push us past the timeout.281wsServer.dispose();282// Wait for in-flight persistence writes to flush to the per-session283// SQLite databases. Without this, a SIGTERM arriving while a284// `setMetadata` write (configValues, customTitle, isRead, isDone,285// diffs) is in flight can drop the latest value — see the286// "Session Config persistence across restarts" integration test.287// Capped so a stuck write cannot hang shutdown indefinitely.288await raceTimeout(sessionDataService.whenIdle(), 3000, () => {289logService.warn('[AgentHostServer] Timed out waiting for session database writes to flush; exiting anyway.');290});291disposables.dispose();292loggerService?.dispose();293process.exit(0);294}295}296297main();298299300