Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/node/agentHostServerMain.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
// Standalone agent host server with WebSocket protocol transport.
7
// 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>]
8
9
import { fileURLToPath } from 'url';
10
11
// This standalone process isn't bootstrapped via bootstrap-esm.ts, so we must
12
// set _VSCODE_FILE_ROOT ourselves so that FileAccess can resolve module paths.
13
// This file lives at out/vs/platform/agentHost/node/ - the root is `out/`.
14
globalThis._VSCODE_FILE_ROOT = fileURLToPath(new URL('../../../..', import.meta.url));
15
16
import * as fs from 'fs';
17
import * as os from 'os';
18
import { DisposableStore } from '../../../base/common/lifecycle.js';
19
import { raceTimeout } from '../../../base/common/async.js';
20
import { joinPath } from '../../../base/common/resources.js';
21
import { URI } from '../../../base/common/uri.js';
22
import { generateUuid } from '../../../base/common/uuid.js';
23
import { localize } from '../../../nls.js';
24
import { NativeEnvironmentService } from '../../environment/node/environmentService.js';
25
import { INativeEnvironmentService } from '../../environment/common/environment.js';
26
import { parseArgs, OPTIONS } from '../../environment/node/argv.js';
27
import { getLogLevel, ILogService, NullLogService } from '../../log/common/log.js';
28
import { LogService } from '../../log/common/logService.js';
29
import { LoggerService } from '../../log/node/loggerService.js';
30
import product from '../../product/common/product.js';
31
import { IProductService } from '../../product/common/productService.js';
32
import { InstantiationService } from '../../instantiation/common/instantiationService.js';
33
import { ServiceCollection } from '../../instantiation/common/serviceCollection.js';
34
import { CopilotAgent } from './copilot/copilotAgent.js';
35
import { AgentService } from './agentService.js';
36
import { IAgentConfigurationService } from './agentConfigurationService.js';
37
import { IAgentHostTerminalManager } from './agentHostTerminalManager.js';
38
import { WebSocketProtocolServer } from './webSocketTransport.js';
39
import { ProtocolServerHandler } from './protocolServerHandler.js';
40
import { FileService } from '../../files/common/fileService.js';
41
import { IFileService } from '../../files/common/files.js';
42
import { DiskFileSystemProvider } from '../../files/node/diskFileSystemProvider.js';
43
import { Schemas } from '../../../base/common/network.js';
44
import { ISessionDataService } from '../common/sessionDataService.js';
45
import { IDiffComputeService } from '../common/diffComputeService.js';
46
import { NodeWorkerDiffComputeService } from './diffComputeService.js';
47
import { SessionDataService } from './sessionDataService.js';
48
import { AgentHostClientFileSystemProvider } from '../common/agentHostClientFileSystemProvider.js';
49
import { AGENT_CLIENT_SCHEME } from '../common/agentClientUri.js';
50
import { resolveServerUrls } from './serverUrls.js';
51
import { AgentPluginManager } from './agentPluginManager.js';
52
import { IAgentPluginManager } from '../common/agentPluginManager.js';
53
import { registerPendingEditContentProvider } from './copilot/pendingEditContentStore.js';
54
import { AgentHostGitService, IAgentHostGitService } from './agentHostGitService.js';
55
56
/** Log to stderr so messages appear in the terminal alongside the process. */
57
function log(msg: string): void {
58
process.stderr.write(`[AgentHostServer] ${msg}\n`);
59
}
60
61
// ---- Options ----------------------------------------------------------------
62
63
const connectionTokenRegex = /^[0-9A-Za-z_-]+$/;
64
65
interface IServerOptions {
66
readonly port: number;
67
readonly host: string | undefined;
68
readonly enableMockAgent: boolean;
69
readonly quiet: boolean;
70
/** Connection token string, or `undefined` when `--without-connection-token`. */
71
readonly connectionToken: string | undefined;
72
}
73
74
function parseServerOptions(): IServerOptions {
75
const argv = process.argv.slice(2);
76
const envPort = parseInt(process.env['VSCODE_AGENT_HOST_PORT'] ?? '8081', 10);
77
const portIdx = argv.indexOf('--port');
78
const port = portIdx >= 0 ? parseInt(argv[portIdx + 1], 10) : envPort;
79
const hostIdx = argv.indexOf('--host');
80
const host = hostIdx >= 0 ? argv[hostIdx + 1] : undefined;
81
const enableMockAgent = argv.includes('--enable-mock-agent');
82
const quiet = argv.includes('--quiet');
83
84
// Connection token
85
const withoutConnectionToken = argv.includes('--without-connection-token');
86
const connectionTokenIdx = argv.indexOf('--connection-token');
87
const connectionTokenFileIdx = argv.indexOf('--connection-token-file');
88
const rawToken = connectionTokenIdx >= 0 ? argv[connectionTokenIdx + 1] : undefined;
89
const tokenFilePath = connectionTokenFileIdx >= 0 ? argv[connectionTokenFileIdx + 1] : undefined;
90
91
let connectionToken: string | undefined;
92
if (withoutConnectionToken) {
93
if (rawToken !== undefined || tokenFilePath !== undefined) {
94
log('Error: --without-connection-token cannot be used with --connection-token or --connection-token-file');
95
process.exit(1);
96
}
97
connectionToken = undefined;
98
} else if (tokenFilePath !== undefined) {
99
if (rawToken !== undefined) {
100
log('Error: --connection-token cannot be used with --connection-token-file');
101
process.exit(1);
102
}
103
try {
104
connectionToken = fs.readFileSync(tokenFilePath).toString().replace(/\r?\n$/, '');
105
} catch {
106
log(`Error: Unable to read connection token file at '${tokenFilePath}'`);
107
process.exit(1);
108
}
109
if (!connectionTokenRegex.test(connectionToken!)) {
110
log(`Error: The connection token in '${tokenFilePath}' does not adhere to the characters 0-9, a-z, A-Z, _, or -.`);
111
process.exit(1);
112
}
113
} else if (rawToken !== undefined) {
114
if (!connectionTokenRegex.test(rawToken)) {
115
log(`Error: The connection token '${rawToken}' does not adhere to the characters 0-9, a-z, A-Z, _, or -.`);
116
process.exit(1);
117
}
118
connectionToken = rawToken;
119
} else {
120
// Default: generate a random token (secure by default)
121
connectionToken = generateUuid();
122
}
123
124
return { port, host, enableMockAgent, quiet, connectionToken };
125
}
126
127
// ---- Main -------------------------------------------------------------------
128
129
async function main(): Promise<void> {
130
const options = parseServerOptions();
131
const disposables = new DisposableStore();
132
133
// Services
134
const productService: IProductService = { _serviceBrand: undefined, ...product };
135
const args = parseArgs(process.argv.slice(2), OPTIONS);
136
const environmentService = new NativeEnvironmentService(args, productService);
137
138
// Logging — production logging unless --quiet
139
let logService: ILogService;
140
let loggerService: LoggerService | undefined;
141
142
if (options.quiet) {
143
logService = new NullLogService();
144
} else {
145
const services = new ServiceCollection();
146
services.set(IProductService, productService);
147
services.set(INativeEnvironmentService, environmentService);
148
loggerService = new LoggerService(getLogLevel(environmentService), environmentService.logsHome);
149
const logger = loggerService.createLogger('agenthost-server', { name: localize('agentHostServer', "Agent Host Server") });
150
logService = disposables.add(new LogService(logger));
151
services.set(ILogService, logService);
152
log('Starting standalone agent host server');
153
}
154
155
logService.info('[AgentHostServer] Starting standalone agent host server');
156
157
// File service
158
const fileService = disposables.add(new FileService(logService));
159
disposables.add(fileService.registerProvider(Schemas.file, disposables.add(new DiskFileSystemProvider(logService))));
160
// In-memory filesystem backing transient file-edit previews shown during
161
// tool-call confirmations.
162
disposables.add(registerPendingEditContentProvider(fileService));
163
164
// Session data service
165
const sessionDataService = new SessionDataService(URI.file(environmentService.userDataPath), fileService, logService);
166
const rootConfigResource = joinPath(environmentService.appSettingsHome, 'globalStorage', 'agent-host-config.json');
167
168
// Build the DI container early so the git service can be created via
169
// `createInstance` (it needs IFileService + INativeEnvironmentService).
170
// The git service is shared by AgentService (for diff computation +
171
// showBlob) and the production agent registration path.
172
const diServices = new ServiceCollection();
173
diServices.set(IProductService, productService);
174
diServices.set(INativeEnvironmentService, environmentService);
175
diServices.set(ILogService, logService);
176
diServices.set(IFileService, fileService);
177
diServices.set(ISessionDataService, sessionDataService);
178
const instantiationService = new InstantiationService(diServices);
179
const gitService = instantiationService.createInstance(AgentHostGitService);
180
181
// Create the agent service (owns AgentHostStateManager + AgentSideEffects internally)
182
const agentService = new AgentService(logService, fileService, sessionDataService, productService, gitService, rootConfigResource);
183
disposables.add(agentService);
184
185
// Register agents
186
if (!options.quiet) {
187
// Production agents (require DI)
188
const pluginManager = new AgentPluginManager(URI.file(environmentService.userDataPath), fileService, logService);
189
diServices.set(IAgentPluginManager, pluginManager);
190
diServices.set(IDiffComputeService, disposables.add(new NodeWorkerDiffComputeService(logService)));
191
diServices.set(IAgentHostTerminalManager, agentService.terminalManager);
192
diServices.set(IAgentConfigurationService, agentService.configurationService);
193
diServices.set(IAgentHostGitService, gitService);
194
const copilotAgent = disposables.add(instantiationService.createInstance(CopilotAgent));
195
agentService.registerProvider(copilotAgent);
196
log('CopilotAgent registered');
197
}
198
199
if (options.enableMockAgent) {
200
// Dynamic import to avoid bundling test code in production
201
import('../test/node/mockAgent.js').then(({ ScriptedMockAgent }) => {
202
const mockAgent = disposables.add(new ScriptedMockAgent());
203
agentService.registerProvider(mockAgent);
204
}).catch(err => {
205
logService.error('[AgentHostServer] Failed to load mock agent', err);
206
});
207
}
208
209
// WebSocket server
210
const wsServer = disposables.add(await WebSocketProtocolServer.create({
211
port: options.port,
212
host: options.host,
213
connectionTokenValidate: options.connectionToken
214
? token => token === options.connectionToken
215
: undefined,
216
}, logService));
217
218
219
const clientFileSystemProvider = disposables.add(new AgentHostClientFileSystemProvider());
220
disposables.add(fileService.registerProvider(AGENT_CLIENT_SCHEME, clientFileSystemProvider));
221
222
// Wire up protocol handler
223
disposables.add(new ProtocolServerHandler(
224
agentService,
225
agentService.stateManager,
226
wsServer,
227
{ defaultDirectory: URI.file(os.homedir()).toString() },
228
clientFileSystemProvider,
229
logService,
230
));
231
232
// Report ready
233
function reportReady(addr: string): void {
234
const listeningPort = Number(addr.split(':').pop());
235
process.stdout.write(`READY:${listeningPort}\n`);
236
237
const urls = resolveServerUrls(options.host, listeningPort);
238
for (const url of urls.local) {
239
log(` Local: ${url}`);
240
logService.info(`[AgentHostServer] Local: ${url}`);
241
}
242
for (const url of urls.network) {
243
log(` Network: ${url}`);
244
logService.info(`[AgentHostServer] Network: ${url}`);
245
}
246
if (urls.network.length === 0 && options.host === undefined) {
247
log(' Network: use --host to expose');
248
logService.info('[AgentHostServer] Network: use --host to expose');
249
}
250
}
251
252
const address = wsServer.address;
253
if (address) {
254
reportReady(address);
255
} else {
256
const interval = setInterval(() => {
257
const addr = wsServer.address;
258
if (addr) {
259
clearInterval(interval);
260
reportReady(addr);
261
}
262
}, 10);
263
}
264
265
// Keep alive until stdin closes or signal
266
process.stdin.resume();
267
process.stdin.on('end', () => { void shutdown(); });
268
process.on('SIGTERM', () => { void shutdown(); });
269
process.on('SIGINT', () => { void shutdown(); });
270
271
let shuttingDown = false;
272
async function shutdown(): Promise<void> {
273
if (shuttingDown) {
274
return;
275
}
276
shuttingDown = true;
277
logService.info('[AgentHostServer] Shutting down...');
278
// Close the WebSocket server first so no further actions can be
279
// dispatched while we wait for in-flight writes to flush — otherwise
280
// a late-arriving action could keep queuing DB writes and either
281
// undermine the flush or push us past the timeout.
282
wsServer.dispose();
283
// Wait for in-flight persistence writes to flush to the per-session
284
// SQLite databases. Without this, a SIGTERM arriving while a
285
// `setMetadata` write (configValues, customTitle, isRead, isDone,
286
// diffs) is in flight can drop the latest value — see the
287
// "Session Config persistence across restarts" integration test.
288
// Capped so a stuck write cannot hang shutdown indefinitely.
289
await raceTimeout(sessionDataService.whenIdle(), 3000, () => {
290
logService.warn('[AgentHostServer] Timed out waiting for session database writes to flush; exiting anyway.');
291
});
292
disposables.dispose();
293
loggerService?.dispose();
294
process.exit(0);
295
}
296
}
297
298
main();
299
300