@rivet-dev/agentos-core 0.0.0-agentos-dylib-plugin.473700e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +204 -0
  2. package/dist/agent-os.d.ts +560 -0
  3. package/dist/agent-os.js +3624 -0
  4. package/dist/agent-session-types.d.ts +67 -0
  5. package/dist/agent-session-types.js +1 -0
  6. package/dist/agents.d.ts +63 -0
  7. package/dist/agents.js +42 -0
  8. package/dist/base-filesystem.d.ts +34 -0
  9. package/dist/base-filesystem.js +173 -0
  10. package/dist/cron/cron-manager.d.ts +22 -0
  11. package/dist/cron/cron-manager.js +160 -0
  12. package/dist/cron/index.d.ts +5 -0
  13. package/dist/cron/index.js +3 -0
  14. package/dist/cron/parse-schedule.d.ts +25 -0
  15. package/dist/cron/parse-schedule.js +66 -0
  16. package/dist/cron/schedule-driver.d.ts +19 -0
  17. package/dist/cron/schedule-driver.js +1 -0
  18. package/dist/cron/timer-driver.d.ts +17 -0
  19. package/dist/cron/timer-driver.js +61 -0
  20. package/dist/cron/types.d.ts +55 -0
  21. package/dist/cron/types.js +1 -0
  22. package/dist/filesystem-snapshot.d.ts +14 -0
  23. package/dist/filesystem-snapshot.js +105 -0
  24. package/dist/host-dir-mount.d.ts +38 -0
  25. package/dist/host-dir-mount.js +34 -0
  26. package/dist/host-tools-zod.d.ts +7 -0
  27. package/dist/host-tools-zod.js +243 -0
  28. package/dist/host-tools.d.ts +45 -0
  29. package/dist/host-tools.js +26 -0
  30. package/dist/index.d.ts +10 -0
  31. package/dist/index.js +10 -0
  32. package/dist/js-bridge.d.ts +3 -0
  33. package/dist/js-bridge.js +2 -0
  34. package/dist/json-rpc.d.ts +50 -0
  35. package/dist/json-rpc.js +22 -0
  36. package/dist/layers.d.ts +51 -0
  37. package/dist/layers.js +185 -0
  38. package/dist/overlay-filesystem.d.ts +19 -0
  39. package/dist/overlay-filesystem.js +645 -0
  40. package/dist/packages.d.ts +127 -0
  41. package/dist/packages.js +335 -0
  42. package/dist/runtime-compat.d.ts +471 -0
  43. package/dist/runtime-compat.js +2009 -0
  44. package/dist/runtime.d.ts +336 -0
  45. package/dist/runtime.js +1 -0
  46. package/dist/sidecar/agent-os-protocol.d.ts +219 -0
  47. package/dist/sidecar/agent-os-protocol.js +594 -0
  48. package/dist/sidecar/binary.d.ts +9 -0
  49. package/dist/sidecar/binary.js +30 -0
  50. package/dist/sidecar/cargo.d.ts +2 -0
  51. package/dist/sidecar/cargo.js +142 -0
  52. package/dist/sidecar/native-process-client.d.ts +2 -0
  53. package/dist/sidecar/native-process-client.js +1 -0
  54. package/dist/sidecar/permissions.d.ts +3 -0
  55. package/dist/sidecar/permissions.js +54 -0
  56. package/dist/sidecar/process.d.ts +6 -0
  57. package/dist/sidecar/process.js +8 -0
  58. package/dist/sidecar/rpc-client.d.ts +244 -0
  59. package/dist/sidecar/rpc-client.js +2028 -0
  60. package/dist/test/docker.d.ts +133 -0
  61. package/dist/test/docker.js +336 -0
  62. package/dist/test/file-system.d.ts +39 -0
  63. package/dist/test/file-system.js +562 -0
  64. package/dist/test/mock-s3.d.ts +42 -0
  65. package/dist/test/mock-s3.js +292 -0
  66. package/dist/test/runtime.d.ts +11 -0
  67. package/dist/test/runtime.js +9 -0
  68. package/dist/test/sandbox-agent.d.ts +9 -0
  69. package/dist/test/sandbox-agent.js +395 -0
  70. package/dist/test/terminal-harness.d.ts +27 -0
  71. package/dist/test/terminal-harness.js +122 -0
  72. package/dist/types.d.ts +10 -0
  73. package/dist/types.js +1 -0
  74. package/fixtures/AGENTOS_SYSTEM_PROMPT.md +21 -0
  75. package/package.json +111 -0
@@ -0,0 +1,3624 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, posix as posixPath, resolve as resolveHostPath, } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { validateToolkits } from "./host-tools.js";
8
+ import { zodToJsonSchema } from "./host-tools-zod.js";
9
+ import { resolvePublishedSidecarBinary } from "./sidecar/binary.js";
10
+ import { findCargoBinary, resolveCargoBinary } from "./sidecar/cargo.js";
11
+ export { isAcpTimeoutErrorData } from "./json-rpc.js";
12
+ const ACP_PROTOCOL_VERSION = 1;
13
+ const ACP_EXTENSION_NAMESPACE = "dev.rivet.agent-os.acp";
14
+ const SHELL_DISPOSE_TIMEOUT_MS = 5_000;
15
+ /**
16
+ * Reserved `env` key on `AcpResumeSessionRequest` carrying the resolved adapter
17
+ * bin entrypoint. The resume wire request omits a dedicated `adapterEntrypoint`
18
+ * field; the sidecar reads the entrypoint from this key and strips it before
19
+ * launching the adapter. Must stay in sync with the sidecar constant of the same
20
+ * name in `crates/agentos-sidecar/src/acp_extension.rs`.
21
+ */
22
+ const RESUME_ADAPTER_ENTRYPOINT_ENV = "AGENT_OS_RESUME_ADAPTER_ENTRYPOINT";
23
+ function defaultAcpClientCapabilities() {
24
+ return {
25
+ fs: {
26
+ readTextFile: true,
27
+ writeTextFile: true,
28
+ },
29
+ terminal: true,
30
+ };
31
+ }
32
+ async function waitForTrackedExitPromises(promises, timeoutMs) {
33
+ if (promises.length === 0) {
34
+ return;
35
+ }
36
+ await Promise.race([
37
+ Promise.allSettled(promises).then(() => undefined),
38
+ new Promise((resolve) => {
39
+ setTimeout(resolve, timeoutMs);
40
+ }),
41
+ ]);
42
+ }
43
+ import { AGENT_CONFIGS } from "./agents.js";
44
+ import { getBaseEnvironment, getBaseFilesystemEntries, } from "./base-filesystem.js";
45
+ import { CronManager } from "./cron/cron-manager.js";
46
+ import { TimerScheduleDriver } from "./cron/timer-driver.js";
47
+ import { snapshotVirtualFilesystem, sortFilesystemEntries, } from "./filesystem-snapshot.js";
48
+ import { createHostDirBackend } from "./host-dir-mount.js";
49
+ import { serializeMountConfigForSidecar, } from "./js-bridge.js";
50
+ import { createSnapshotExport, } from "./layers.js";
51
+ import { processSoftware, resolvePackageDir, } from "./packages.js";
52
+ import { allowAll } from "./runtime-compat.js";
53
+ import { AcpRuntimeKind, decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agent-os-protocol.js";
54
+ import { serializePermissionsForSidecar } from "./sidecar/permissions.js";
55
+ import { createAgentOsSidecarClient, NATIVE_SIDECAR_FRAME_TIMEOUT_MS, NativeSidecarKernelProxy, SidecarProcess, serializeRootFilesystemForSidecar, } from "./sidecar/rpc-client.js";
56
+ const LEGACY_PERMISSION_METHOD = "request/permission";
57
+ const ACP_PERMISSION_METHOD = "session/request_permission";
58
+ class AcpDispatchError extends Error {
59
+ code;
60
+ data;
61
+ constructor(code, message, data) {
62
+ super(message);
63
+ this.name = "AcpDispatchError";
64
+ this.code = code;
65
+ this.data = data;
66
+ }
67
+ }
68
+ function toJsonRpcNotification(value) {
69
+ if (!value ||
70
+ typeof value !== "object" ||
71
+ Array.isArray(value) ||
72
+ value.jsonrpc !== "2.0" ||
73
+ typeof value.method !== "string") {
74
+ throw new Error("Invalid JSON-RPC notification from sidecar");
75
+ }
76
+ return value;
77
+ }
78
+ function toJsonRpcResponse(value) {
79
+ if (!value ||
80
+ typeof value !== "object" ||
81
+ Array.isArray(value) ||
82
+ value.jsonrpc !== "2.0" ||
83
+ !(typeof value.id === "number" ||
84
+ typeof value.id === "string" ||
85
+ value.id === null)) {
86
+ throw new Error("Invalid JSON-RPC response from sidecar");
87
+ }
88
+ return value;
89
+ }
90
+ function toJsonRpcRequest(value) {
91
+ if (!value ||
92
+ typeof value !== "object" ||
93
+ Array.isArray(value) ||
94
+ value.jsonrpc !== "2.0" ||
95
+ !(typeof value.id === "number" ||
96
+ typeof value.id === "string" ||
97
+ value.id === null) ||
98
+ typeof value.method !== "string") {
99
+ throw new Error("Invalid JSON-RPC request from ACP callback");
100
+ }
101
+ return value;
102
+ }
103
+ function toRecord(value) {
104
+ return value && typeof value === "object" && !Array.isArray(value)
105
+ ? value
106
+ : {};
107
+ }
108
+ function parseAcpJson(value, context) {
109
+ if (value === null) {
110
+ return undefined;
111
+ }
112
+ try {
113
+ return JSON.parse(value);
114
+ }
115
+ catch (error) {
116
+ throw new Error(`invalid ACP ${context} JSON: ${error instanceof Error ? error.message : String(error)}`);
117
+ }
118
+ }
119
+ function parseAcpJsonList(values, context) {
120
+ return values.map((value, index) => parseAcpJson(value, `${context}[${index}]`));
121
+ }
122
+ function sidecarSessionCreatedFromAcp(response) {
123
+ return {
124
+ sessionId: response.sessionId,
125
+ ...(response.pid !== null ? { pid: response.pid } : {}),
126
+ modes: parseAcpJson(response.modes, "modes"),
127
+ configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
128
+ agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
129
+ agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
130
+ };
131
+ }
132
+ function sidecarSessionStateFromAcp(response) {
133
+ return {
134
+ sessionId: response.sessionId,
135
+ agentType: response.agentType,
136
+ processId: response.processId,
137
+ ...(response.pid !== null ? { pid: response.pid } : {}),
138
+ closed: response.closed,
139
+ modes: parseAcpJson(response.modes, "modes"),
140
+ configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
141
+ agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
142
+ agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
143
+ };
144
+ }
145
+ function isLocalCancelledPromptResponse(method, response) {
146
+ const result = toRecord(response.result);
147
+ return (method === "session/prompt" &&
148
+ response.id === null &&
149
+ response.error === undefined &&
150
+ result.stopReason === "cancelled");
151
+ }
152
+ const CLOSED_SESSION_ID_RETENTION_LIMIT = 2048;
153
+ const CLOSED_SHELL_ID_RETENTION_LIMIT = 2048;
154
+ class BoundedSet {
155
+ limit;
156
+ #entries = new Map();
157
+ constructor(limit) {
158
+ if (!Number.isInteger(limit) || limit <= 0) {
159
+ throw new Error(`BoundedSet limit must be a positive integer: ${limit}`);
160
+ }
161
+ this.limit = limit;
162
+ }
163
+ add(value) {
164
+ if (this.#entries.has(value)) {
165
+ this.#entries.delete(value);
166
+ }
167
+ this.#entries.set(value, undefined);
168
+ if (this.#entries.size <= this.limit) {
169
+ return;
170
+ }
171
+ const oldest = this.#entries.keys().next();
172
+ if (!oldest.done) {
173
+ this.#entries.delete(oldest.value);
174
+ }
175
+ }
176
+ has(value) {
177
+ return this.#entries.has(value);
178
+ }
179
+ delete(value) {
180
+ return this.#entries.delete(value);
181
+ }
182
+ get size() {
183
+ return this.#entries.size;
184
+ }
185
+ }
186
+ function shouldDispatchToSessionEventHandlers(notification) {
187
+ return notification.method === "session/update";
188
+ }
189
+ function toSessionModes(value) {
190
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
191
+ return null;
192
+ }
193
+ return value;
194
+ }
195
+ function toSessionConfigOptions(value) {
196
+ return Array.isArray(value) ? value : [];
197
+ }
198
+ function toAgentCapabilities(value) {
199
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
200
+ return {};
201
+ }
202
+ return value;
203
+ }
204
+ function toAgentInfo(value) {
205
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
206
+ return null;
207
+ }
208
+ if (typeof value.name !== "string") {
209
+ return null;
210
+ }
211
+ return value;
212
+ }
213
+ function sessionEntryFromInit(sessionId, agentType, initData) {
214
+ return {
215
+ sessionId,
216
+ agentType,
217
+ processId: "",
218
+ pid: null,
219
+ closed: false,
220
+ modes: initData.modes ?? null,
221
+ configOptions: initData.configOptions ?? [],
222
+ capabilities: initData.capabilities ?? {},
223
+ agentInfo: initData.agentInfo ?? null,
224
+ eventHandlers: new Set(),
225
+ permissionHandlers: new Set(),
226
+ configOverrides: new Map(),
227
+ pendingPermissionReplies: new Map(),
228
+ };
229
+ }
230
+ function isOverlayMountConfig(config) {
231
+ return "filesystem" in config;
232
+ }
233
+ function isNativeMountConfig(config) {
234
+ return "plugin" in config;
235
+ }
236
+ function asMountConfigJsonObject(value) {
237
+ if (value && typeof value === "object" && !Array.isArray(value)) {
238
+ return value;
239
+ }
240
+ return {};
241
+ }
242
+ function getHostDirMountPluginConfig(config) {
243
+ const object = asMountConfigJsonObject(config);
244
+ if (typeof object.hostPath !== "string") {
245
+ return null;
246
+ }
247
+ const hostPathConfig = {
248
+ hostPath: object.hostPath,
249
+ };
250
+ if (typeof object.readOnly === "boolean") {
251
+ hostPathConfig.readOnly = object.readOnly;
252
+ }
253
+ return hostPathConfig;
254
+ }
255
+ function getSandboxAgentMountPluginConfig(config) {
256
+ const object = asMountConfigJsonObject(config);
257
+ if (typeof object.baseUrl !== "string") {
258
+ return null;
259
+ }
260
+ const sandboxConfig = {
261
+ baseUrl: object.baseUrl,
262
+ };
263
+ if (typeof object.token === "string") {
264
+ sandboxConfig.token = object.token;
265
+ }
266
+ if (typeof object.basePath === "string") {
267
+ sandboxConfig.basePath = object.basePath;
268
+ }
269
+ if (typeof object.timeoutMs === "number") {
270
+ sandboxConfig.timeoutMs = object.timeoutMs;
271
+ }
272
+ if (typeof object.maxFullReadBytes === "number") {
273
+ sandboxConfig.maxFullReadBytes = object.maxFullReadBytes;
274
+ }
275
+ if (object.headers &&
276
+ typeof object.headers === "object" &&
277
+ !Array.isArray(object.headers)) {
278
+ const headers = Object.entries(object.headers)
279
+ .filter(([, value]) => typeof value === "string")
280
+ .map(([name, value]) => [name, value]);
281
+ if (headers.length > 0) {
282
+ sandboxConfig.headers = Object.fromEntries(headers);
283
+ }
284
+ }
285
+ return sandboxConfig;
286
+ }
287
+ function getS3MountPluginConfig(config) {
288
+ const object = asMountConfigJsonObject(config);
289
+ if (typeof object.bucket !== "string") {
290
+ return null;
291
+ }
292
+ const s3Config = {
293
+ bucket: object.bucket,
294
+ };
295
+ if (typeof object.prefix === "string") {
296
+ s3Config.prefix = object.prefix;
297
+ }
298
+ if (typeof object.region === "string") {
299
+ s3Config.region = object.region;
300
+ }
301
+ if (typeof object.endpoint === "string") {
302
+ s3Config.endpoint = object.endpoint;
303
+ }
304
+ if (typeof object.chunkSize === "number") {
305
+ s3Config.chunkSize = object.chunkSize;
306
+ }
307
+ if (typeof object.inlineThreshold === "number") {
308
+ s3Config.inlineThreshold = object.inlineThreshold;
309
+ }
310
+ if (object.credentials &&
311
+ typeof object.credentials === "object" &&
312
+ !Array.isArray(object.credentials) &&
313
+ typeof object.credentials.accessKeyId === "string" &&
314
+ typeof object.credentials.secretAccessKey === "string") {
315
+ s3Config.credentials = {
316
+ accessKeyId: object.credentials.accessKeyId,
317
+ secretAccessKey: object.credentials.secretAccessKey,
318
+ };
319
+ }
320
+ return s3Config;
321
+ }
322
+ function getGoogleDriveMountPluginConfig(config) {
323
+ const object = asMountConfigJsonObject(config);
324
+ if (typeof object.folderId !== "string") {
325
+ return null;
326
+ }
327
+ if (!object.credentials ||
328
+ typeof object.credentials !== "object" ||
329
+ Array.isArray(object.credentials) ||
330
+ typeof object.credentials.clientEmail !== "string" ||
331
+ typeof object.credentials.privateKey !== "string") {
332
+ return null;
333
+ }
334
+ const googleDriveConfig = {
335
+ credentials: {
336
+ clientEmail: object.credentials.clientEmail,
337
+ privateKey: object.credentials.privateKey,
338
+ },
339
+ folderId: object.folderId,
340
+ };
341
+ if (typeof object.keyPrefix === "string") {
342
+ googleDriveConfig.keyPrefix = object.keyPrefix;
343
+ }
344
+ if (typeof object.chunkSize === "number") {
345
+ googleDriveConfig.chunkSize = object.chunkSize;
346
+ }
347
+ if (typeof object.inlineThreshold === "number") {
348
+ googleDriveConfig.inlineThreshold = object.inlineThreshold;
349
+ }
350
+ return googleDriveConfig;
351
+ }
352
+ const KERNEL_POSIX_BOOTSTRAP_DIRS = [
353
+ "/dev",
354
+ "/proc",
355
+ "/tmp",
356
+ "/bin",
357
+ "/lib",
358
+ "/sbin",
359
+ "/boot",
360
+ "/etc",
361
+ "/root",
362
+ "/run",
363
+ "/srv",
364
+ "/sys",
365
+ "/opt",
366
+ "/mnt",
367
+ "/media",
368
+ "/home",
369
+ "/home/user",
370
+ "/usr",
371
+ "/usr/bin",
372
+ "/usr/games",
373
+ "/usr/include",
374
+ "/usr/lib",
375
+ "/usr/libexec",
376
+ "/usr/man",
377
+ "/usr/local",
378
+ "/usr/local/bin",
379
+ "/usr/sbin",
380
+ "/usr/share",
381
+ "/usr/share/man",
382
+ "/var",
383
+ "/var/cache",
384
+ "/var/empty",
385
+ "/var/lib",
386
+ "/var/lock",
387
+ "/var/log",
388
+ "/var/run",
389
+ "/var/spool",
390
+ "/var/tmp",
391
+ "/etc/agentos",
392
+ ];
393
+ const NODE_RUNTIME_BOOTSTRAP_COMMANDS = ["node", "npm", "npx"];
394
+ const KERNEL_COMMAND_STUB = "#!/bin/sh\n# kernel command stub\n";
395
+ const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url));
396
+ const SIDECAR_BINARY = join(REPO_ROOT, "target/debug/agentos-sidecar");
397
+ const SIDECAR_BUILD_INPUTS = [
398
+ join(REPO_ROOT, "Cargo.toml"),
399
+ join(REPO_ROOT, "Cargo.lock"),
400
+ join(REPO_ROOT, "crates/bridge"),
401
+ join(REPO_ROOT, "crates/execution"),
402
+ join(REPO_ROOT, "crates/kernel"),
403
+ join(REPO_ROOT, "crates/sidecar"),
404
+ ];
405
+ let ensuredSidecarBinary = null;
406
+ function isWasmBinaryFile(path) {
407
+ try {
408
+ const header = readFileSync(path);
409
+ return (header.length >= 4 &&
410
+ header[0] === 0x00 &&
411
+ header[1] === 0x61 &&
412
+ header[2] === 0x73 &&
413
+ header[3] === 0x6d);
414
+ }
415
+ catch {
416
+ return false;
417
+ }
418
+ }
419
+ function collectBootstrapWasmCommands(commandDirs) {
420
+ const commands = [];
421
+ const seen = new Set();
422
+ for (const dir of commandDirs) {
423
+ let entries;
424
+ try {
425
+ entries = readdirSync(dir).sort((a, b) => a.localeCompare(b));
426
+ }
427
+ catch {
428
+ continue;
429
+ }
430
+ for (const entry of entries) {
431
+ if (entry.startsWith(".")) {
432
+ continue;
433
+ }
434
+ const fullPath = join(dir, entry);
435
+ try {
436
+ if (statSync(fullPath).isDirectory()) {
437
+ continue;
438
+ }
439
+ }
440
+ catch {
441
+ continue;
442
+ }
443
+ if (!isWasmBinaryFile(fullPath) || seen.has(entry)) {
444
+ continue;
445
+ }
446
+ seen.add(entry);
447
+ commands.push(entry);
448
+ }
449
+ }
450
+ return commands;
451
+ }
452
+ function resolveDeclaredCommandSource(commandDir, commandName, aliases) {
453
+ let current = commandName;
454
+ const visited = new Set();
455
+ while (!visited.has(current)) {
456
+ visited.add(current);
457
+ const candidatePath = join(commandDir, current);
458
+ if (isWasmBinaryFile(candidatePath)) {
459
+ return candidatePath;
460
+ }
461
+ const next = aliases[current];
462
+ if (!next) {
463
+ return null;
464
+ }
465
+ current = next;
466
+ }
467
+ return null;
468
+ }
469
+ function prepareCommandDirs(commandPackages) {
470
+ const commandDirs = [];
471
+ const tempDirs = [];
472
+ try {
473
+ for (const commandPackage of commandPackages) {
474
+ commandDirs.push(commandPackage.commandDir);
475
+ const aliasEntries = Object.entries(commandPackage.aliases)
476
+ .sort(([leftAlias], [rightAlias]) => leftAlias.localeCompare(rightAlias))
477
+ .flatMap(([aliasName]) => {
478
+ const aliasPath = join(commandPackage.commandDir, aliasName);
479
+ if (isWasmBinaryFile(aliasPath)) {
480
+ return [];
481
+ }
482
+ const sourcePath = resolveDeclaredCommandSource(commandPackage.commandDir, aliasName, commandPackage.aliases);
483
+ if (!sourcePath) {
484
+ return [];
485
+ }
486
+ return [[aliasName, sourcePath]];
487
+ });
488
+ if (aliasEntries.length === 0) {
489
+ continue;
490
+ }
491
+ const aliasDir = mkdtempSync(join(tmpdir(), "agent-os-command-aliases-"));
492
+ for (const [aliasName, sourcePath] of aliasEntries) {
493
+ writeFileSync(join(aliasDir, aliasName), readFileSync(sourcePath));
494
+ }
495
+ tempDirs.push(aliasDir);
496
+ commandDirs.push(aliasDir);
497
+ }
498
+ }
499
+ catch (error) {
500
+ for (const tempDir of tempDirs) {
501
+ rmSync(tempDir, { recursive: true, force: true });
502
+ }
503
+ throw error;
504
+ }
505
+ return {
506
+ commandDirs,
507
+ dispose() {
508
+ for (const tempDir of tempDirs) {
509
+ rmSync(tempDir, { recursive: true, force: true });
510
+ }
511
+ },
512
+ };
513
+ }
514
+ function collectConfiguredLowerPaths(config) {
515
+ const paths = new Set();
516
+ for (const lower of config?.lowers ?? []) {
517
+ if (lower.kind !== "snapshot-export") {
518
+ continue;
519
+ }
520
+ for (const entry of lower.source.filesystem.entries) {
521
+ paths.add(entry.path);
522
+ }
523
+ }
524
+ return paths;
525
+ }
526
+ function findBootstrapSeedEntry(config, path) {
527
+ for (const lower of config?.lowers ?? []) {
528
+ if (lower.kind !== "snapshot-export") {
529
+ continue;
530
+ }
531
+ const entry = lower.source.filesystem.entries.find((candidate) => candidate.path === path);
532
+ if (entry) {
533
+ return entry;
534
+ }
535
+ }
536
+ return getBaseFilesystemEntries().find((entry) => entry.path === path);
537
+ }
538
+ function createKernelBootstrapLower(config, commandNames, extraEntries = []) {
539
+ const includesBundledBaseLayer = !(config?.disableDefaultBaseLayer ?? false);
540
+ const existingPaths = collectConfiguredLowerPaths(config);
541
+ if (includesBundledBaseLayer) {
542
+ for (const entry of getBaseFilesystemEntries()) {
543
+ existingPaths.add(entry.path);
544
+ }
545
+ }
546
+ const entries = [
547
+ {
548
+ path: "/",
549
+ type: "directory",
550
+ mode: "755",
551
+ uid: 0,
552
+ gid: 0,
553
+ },
554
+ ];
555
+ for (const dir of KERNEL_POSIX_BOOTSTRAP_DIRS) {
556
+ if (existingPaths.has(dir)) {
557
+ continue;
558
+ }
559
+ const seed = findBootstrapSeedEntry(config, dir);
560
+ entries.push({
561
+ path: dir,
562
+ type: "directory",
563
+ mode: seed?.type === "directory" ? seed.mode : "755",
564
+ uid: seed?.uid ?? 0,
565
+ gid: seed?.gid ?? 0,
566
+ });
567
+ }
568
+ if (!includesBundledBaseLayer && !existingPaths.has("/usr/bin/env")) {
569
+ entries.push({
570
+ path: "/usr/bin/env",
571
+ type: "file",
572
+ mode: "644",
573
+ uid: 0,
574
+ gid: 0,
575
+ content: "AA==",
576
+ encoding: "base64",
577
+ });
578
+ }
579
+ const uniqueCommands = [...new Set(commandNames)].sort((a, b) => a.localeCompare(b));
580
+ for (const command of uniqueCommands) {
581
+ const stubPath = `/bin/${command}`;
582
+ if (existingPaths.has(stubPath)) {
583
+ continue;
584
+ }
585
+ entries.push({
586
+ path: stubPath,
587
+ type: "file",
588
+ mode: "755",
589
+ uid: 0,
590
+ gid: 0,
591
+ content: KERNEL_COMMAND_STUB,
592
+ encoding: "utf8",
593
+ });
594
+ }
595
+ for (const entry of sortFilesystemEntries(extraEntries)) {
596
+ if (existingPaths.has(entry.path)) {
597
+ continue;
598
+ }
599
+ entries.push(entry);
600
+ }
601
+ return entries.length > 1 ? createSnapshotExport(entries) : null;
602
+ }
603
+ function buildLiveBootstrapDirectoryEntries(existingPaths, config) {
604
+ const entries = [];
605
+ for (const dir of KERNEL_POSIX_BOOTSTRAP_DIRS) {
606
+ if (existingPaths.has(dir)) {
607
+ continue;
608
+ }
609
+ const seed = findBootstrapSeedEntry(config, dir);
610
+ entries.push({
611
+ path: dir,
612
+ kind: "directory",
613
+ mode: Number.parseInt(seed?.type === "directory" ? seed.mode : "755", 8),
614
+ uid: seed?.uid ?? 0,
615
+ gid: seed?.gid ?? 0,
616
+ executable: true,
617
+ });
618
+ }
619
+ return entries;
620
+ }
621
+ async function bootstrapLiveBootstrapDirectories(client, session, vm, config) {
622
+ const existingPaths = new Set((await client.snapshotRootFilesystem(session, vm)).map((entry) => entry.path));
623
+ const entries = buildLiveBootstrapDirectoryEntries(existingPaths, config);
624
+ if (entries.length === 0) {
625
+ return;
626
+ }
627
+ await client.bootstrapRootFilesystem(session, vm, entries);
628
+ }
629
+ function toSnapshotModeString(mode, kind) {
630
+ const fallback = kind === "directory" ? 0o755 : kind === "symlink" ? 0o777 : 0o644;
631
+ return `0${((mode ?? fallback) & 0o7777).toString(8)}`;
632
+ }
633
+ function convertSidecarRootSnapshotEntries(entries) {
634
+ return entries.map((entry) => {
635
+ const baseEntry = {
636
+ path: entry.path,
637
+ type: entry.kind,
638
+ mode: toSnapshotModeString(entry.mode, entry.kind),
639
+ uid: entry.uid ?? 0,
640
+ gid: entry.gid ?? 0,
641
+ };
642
+ if (entry.kind === "file") {
643
+ return {
644
+ ...baseEntry,
645
+ content: entry.content ?? "",
646
+ encoding: entry.encoding ?? "utf8",
647
+ };
648
+ }
649
+ if (entry.kind === "symlink") {
650
+ if (entry.target === undefined) {
651
+ throw new Error(`sidecar root snapshot for ${entry.path} is missing a symlink target`);
652
+ }
653
+ return {
654
+ ...baseEntry,
655
+ target: entry.target,
656
+ };
657
+ }
658
+ return baseEntry;
659
+ });
660
+ }
661
+ function ensureNativeSidecarBinary() {
662
+ // A published install has no in-repo Cargo workspace to build from: resolve
663
+ // the prebuilt platform binary (or the AGENTOS_SIDECAR_BIN override).
664
+ if (process.env.AGENTOS_SIDECAR_BIN ||
665
+ !existsSync(join(REPO_ROOT, "Cargo.toml"))) {
666
+ return resolvePublishedSidecarBinary();
667
+ }
668
+ if (ensuredSidecarBinary &&
669
+ existsSync(ensuredSidecarBinary) &&
670
+ !sidecarBinaryNeedsBuild()) {
671
+ return ensuredSidecarBinary;
672
+ }
673
+ if (sidecarBinaryNeedsBuild()) {
674
+ const cargoBinary = findCargoBinary();
675
+ if (cargoBinary) {
676
+ execFileSync(cargoBinary, ["build", "-q", "-p", "agentos-sidecar"], {
677
+ cwd: REPO_ROOT,
678
+ stdio: "pipe",
679
+ });
680
+ }
681
+ else if (!existsSync(SIDECAR_BINARY)) {
682
+ execFileSync(resolveCargoBinary(), ["build", "-q", "-p", "agentos-sidecar"], {
683
+ cwd: REPO_ROOT,
684
+ stdio: "pipe",
685
+ });
686
+ }
687
+ }
688
+ ensuredSidecarBinary = SIDECAR_BINARY;
689
+ return ensuredSidecarBinary;
690
+ }
691
+ function sidecarBinaryNeedsBuild() {
692
+ if (!existsSync(SIDECAR_BINARY)) {
693
+ return true;
694
+ }
695
+ const binaryMtimeMs = statSync(SIDECAR_BINARY).mtimeMs;
696
+ return SIDECAR_BUILD_INPUTS.some((path) => existsSync(path) && latestMtimeMs(path) > binaryMtimeMs);
697
+ }
698
+ function latestMtimeMs(path) {
699
+ const stats = statSync(path);
700
+ if (!stats.isDirectory()) {
701
+ return stats.mtimeMs;
702
+ }
703
+ let latest = stats.mtimeMs;
704
+ for (const entry of readdirSync(path)) {
705
+ latest = Math.max(latest, latestMtimeMs(join(path, entry)));
706
+ }
707
+ return latest;
708
+ }
709
+ function collectGuestCommandPaths(commandDirs) {
710
+ const guestPaths = new Map();
711
+ for (const [index, commandDir] of commandDirs.entries()) {
712
+ let entries;
713
+ try {
714
+ entries = readdirSync(commandDir).sort((left, right) => left.localeCompare(right));
715
+ }
716
+ catch {
717
+ continue;
718
+ }
719
+ for (const entry of entries) {
720
+ if (entry.startsWith(".")) {
721
+ continue;
722
+ }
723
+ if (!isWasmBinaryFile(join(commandDir, entry)) || guestPaths.has(entry)) {
724
+ continue;
725
+ }
726
+ guestPaths.set(entry, `/__secure_exec/commands/${index}/${entry}`);
727
+ }
728
+ }
729
+ return guestPaths;
730
+ }
731
+ async function resolveCompatLocalMounts(mounts) {
732
+ if (!mounts) {
733
+ return [];
734
+ }
735
+ const resolved = [];
736
+ for (const mount of mounts) {
737
+ if (isNativeMountConfig(mount)) {
738
+ continue;
739
+ }
740
+ if (!isOverlayMountConfig(mount)) {
741
+ resolved.push({
742
+ path: posixPath.normalize(mount.path),
743
+ fs: mount.driver,
744
+ readOnly: mount.readOnly ?? false,
745
+ });
746
+ continue;
747
+ }
748
+ const mode = mount.filesystem.mode ?? "ephemeral";
749
+ const fs = mode === "read-only"
750
+ ? mount.filesystem.store.createOverlayFilesystem({
751
+ mode: "read-only",
752
+ lowers: mount.filesystem.lowers,
753
+ })
754
+ : mount.filesystem.store.createOverlayFilesystem({
755
+ upper: await mount.filesystem.store.createWritableLayer(),
756
+ lowers: mount.filesystem.lowers,
757
+ });
758
+ resolved.push({
759
+ path: posixPath.normalize(mount.path),
760
+ fs,
761
+ readOnly: mode === "read-only",
762
+ });
763
+ }
764
+ return resolved;
765
+ }
766
+ function collectSidecarMountPlan(options) {
767
+ const sidecarMounts = [];
768
+ const hostMounts = [];
769
+ const hostPathMappings = [];
770
+ const seenMounts = new Set();
771
+ function pushMount(mount) {
772
+ const serialized = serializeMountConfigForSidecar(mount);
773
+ const key = `${serialized.guestPath}\0${serialized.plugin.id}\0${JSON.stringify(serialized.plugin.config)}`;
774
+ if (seenMounts.has(key)) {
775
+ return;
776
+ }
777
+ seenMounts.add(key);
778
+ sidecarMounts.push(serialized);
779
+ if (mount.plugin.id === "host_dir") {
780
+ const config = getHostDirMountPluginConfig(mount.plugin.config);
781
+ if (config) {
782
+ hostPathMappings.push({
783
+ vmPath: posixPath.normalize(mount.path),
784
+ hostPath: resolveHostPath(config.hostPath),
785
+ readOnly: mount.readOnly ?? config.readOnly ?? true,
786
+ });
787
+ }
788
+ if (config && options.mounts?.some((candidate) => candidate === mount)) {
789
+ hostMounts.push({
790
+ vmPath: posixPath.normalize(mount.path),
791
+ hostPath: resolveHostPath(config.hostPath),
792
+ readOnly: mount.readOnly ?? config.readOnly ?? true,
793
+ });
794
+ }
795
+ }
796
+ }
797
+ for (const mount of options.mounts ?? []) {
798
+ if (!isNativeMountConfig(mount)) {
799
+ continue;
800
+ }
801
+ pushMount(mount);
802
+ }
803
+ for (const root of options.softwareRoots) {
804
+ pushMount({
805
+ path: root.vmPath,
806
+ plugin: createHostDirBackend({
807
+ hostPath: root.hostPath,
808
+ readOnly: true,
809
+ }),
810
+ readOnly: true,
811
+ });
812
+ }
813
+ for (const [index, commandDir] of options.commandDirs.entries()) {
814
+ pushMount({
815
+ path: `/__secure_exec/commands/${index}`,
816
+ plugin: createHostDirBackend({
817
+ hostPath: commandDir,
818
+ readOnly: true,
819
+ }),
820
+ readOnly: true,
821
+ });
822
+ }
823
+ if (options.shimDir) {
824
+ pushMount({
825
+ path: "/usr/local/bin",
826
+ plugin: createHostDirBackend({
827
+ hostPath: options.shimDir,
828
+ readOnly: true,
829
+ }),
830
+ readOnly: true,
831
+ });
832
+ }
833
+ hostMounts.sort((left, right) => right.vmPath.length - left.vmPath.length);
834
+ hostPathMappings.sort((left, right) => right.vmPath.length - left.vmPath.length);
835
+ return { sidecarMounts, hostMounts, hostPathMappings };
836
+ }
837
+ function materializeToolShimDir(toolKits) {
838
+ const shimDir = mkdtempSync(join(tmpdir(), "agent-os-host-tools-shims-"));
839
+ writeFileSync(join(shimDir, "agentos"), KERNEL_COMMAND_STUB, { mode: 0o755 });
840
+ for (const toolKit of toolKits) {
841
+ writeFileSync(join(shimDir, `agentos-${toolKit.name}`), KERNEL_COMMAND_STUB, { mode: 0o755 });
842
+ }
843
+ return shimDir;
844
+ }
845
+ function collectToolkitBootstrapCommands(toolKits) {
846
+ if (toolKits.length === 0) {
847
+ return [];
848
+ }
849
+ return ["agentos", ...toolKits.map((toolKit) => `agentos-${toolKit.name}`)];
850
+ }
851
+ function validationMessage(error) {
852
+ if (typeof error === "object" &&
853
+ error !== null &&
854
+ "issues" in error &&
855
+ Array.isArray(error.issues)) {
856
+ return error.issues
857
+ .map((issue) => {
858
+ const path = Array.isArray(issue.path) && issue.path.length > 0
859
+ ? ` at "${issue.path.join(".")}"`
860
+ : "";
861
+ return `${issue.message}${path}`;
862
+ })
863
+ .join("; ");
864
+ }
865
+ return error instanceof Error ? error.message : String(error);
866
+ }
867
+ function toolToSidecarDefinition(tool) {
868
+ return {
869
+ description: tool.description,
870
+ inputSchema: zodToJsonSchema(tool.inputSchema),
871
+ ...(tool.timeout !== undefined ? { timeoutMs: tool.timeout } : {}),
872
+ ...(tool.examples && tool.examples.length > 0
873
+ ? {
874
+ examples: tool.examples.map((example) => ({
875
+ description: example.description,
876
+ input: example.input,
877
+ })),
878
+ }
879
+ : {}),
880
+ };
881
+ }
882
+ function combineInstructions(additionalInstructions, toolReference) {
883
+ const parts = [additionalInstructions, toolReference]
884
+ .map((part) => part?.trim())
885
+ .filter((part) => Boolean(part));
886
+ if (parts.length === 0) {
887
+ return null;
888
+ }
889
+ return parts.join("\n\n");
890
+ }
891
+ function buildHostToolReference(toolKits) {
892
+ if (toolKits.length === 0) {
893
+ return "";
894
+ }
895
+ const lines = [
896
+ "## Available Host Tools",
897
+ "",
898
+ "Run `agentos list-tools` to see all available tools.",
899
+ "",
900
+ ];
901
+ for (const toolKit of toolKits) {
902
+ lines.push(`### ${toolKit.name}`);
903
+ lines.push("");
904
+ lines.push(toolKit.description);
905
+ lines.push("");
906
+ for (const [toolName, tool] of Object.entries(toolKit.tools)) {
907
+ const sidecarTool = toolToSidecarDefinition(tool);
908
+ const signature = buildToolFlagSignature(sidecarTool.inputSchema);
909
+ const suffix = signature.length > 0 ? ` ${signature}` : "";
910
+ lines.push(`- \`agentos-${toolKit.name} ${toolName}${suffix}\` — ${tool.description}`);
911
+ }
912
+ lines.push("");
913
+ const toolsWithExamples = Object.entries(toolKit.tools).filter(([, tool]) => tool.examples && tool.examples.length > 0);
914
+ if (toolsWithExamples.length > 0) {
915
+ lines.push("**Examples:**");
916
+ lines.push("");
917
+ for (const [toolName, tool] of toolsWithExamples) {
918
+ for (const example of tool.examples ?? []) {
919
+ const args = inputToToolFlags(example.input);
920
+ const suffix = args.length > 0 ? ` ${args}` : "";
921
+ lines.push(`- ${example.description}: \`agentos-${toolKit.name} ${toolName}${suffix}\``);
922
+ }
923
+ }
924
+ lines.push("");
925
+ }
926
+ lines.push(`Run \`agentos-${toolKit.name} <tool> --help\` for details.`);
927
+ lines.push("");
928
+ }
929
+ return lines.join("\n");
930
+ }
931
+ function buildToolFlagSignature(schema) {
932
+ return describeToolFlags(schema)
933
+ .map((flag) => {
934
+ if (flag.required) {
935
+ return `${flag.name} <${flag.type}>`;
936
+ }
937
+ return `[${flag.name} <${flag.type}>]`;
938
+ })
939
+ .join(" ");
940
+ }
941
+ function describeToolFlags(schema) {
942
+ const schemaObject = asRecord(schema);
943
+ const properties = asRecord(schemaObject.properties);
944
+ const required = Array.isArray(schemaObject.required)
945
+ ? new Set(schemaObject.required.filter((item) => typeof item === "string"))
946
+ : new Set();
947
+ return Object.entries(properties).map(([fieldName, fieldSchema]) => ({
948
+ name: `--${camelToKebab(fieldName)}`,
949
+ type: describeToolFlagType(fieldSchema),
950
+ required: required.has(fieldName),
951
+ }));
952
+ }
953
+ function describeToolFlagType(schema) {
954
+ const schemaObject = asRecord(schema);
955
+ const type = typeof schemaObject.type === "string" ? schemaObject.type : undefined;
956
+ if (type === "array") {
957
+ const itemType = describeJsonSchemaScalarType(schemaObject.items);
958
+ return `${itemType}[]`;
959
+ }
960
+ if (type === "string") {
961
+ const enumValues = Array.isArray(schemaObject.enum)
962
+ ? schemaObject.enum.filter((item) => typeof item === "string")
963
+ : [];
964
+ return enumValues.length > 0 ? enumValues.join("|") : "string";
965
+ }
966
+ return type ?? "string";
967
+ }
968
+ function describeJsonSchemaScalarType(schema) {
969
+ const schemaObject = asRecord(schema);
970
+ return typeof schemaObject.type === "string" ? schemaObject.type : "string";
971
+ }
972
+ function inputToToolFlags(input) {
973
+ const inputObject = asRecord(input);
974
+ return Object.entries(inputObject)
975
+ .flatMap(([key, value]) => {
976
+ const flag = `--${camelToKebab(key)}`;
977
+ if (value === true) {
978
+ return [flag];
979
+ }
980
+ if (value === false) {
981
+ return [`--no-${camelToKebab(key)}`];
982
+ }
983
+ if (Array.isArray(value)) {
984
+ return value.map((item) => `${flag} ${toolCliString(item)}`);
985
+ }
986
+ return [`${flag} ${toolCliString(value)}`];
987
+ })
988
+ .join(" ");
989
+ }
990
+ function toolCliString(value) {
991
+ return typeof value === "string" ? value : (JSON.stringify(value) ?? "null");
992
+ }
993
+ function camelToKebab(value) {
994
+ return value.replace(/[A-Z]/g, (ch, index) => `${index > 0 ? "-" : ""}${ch.toLowerCase()}`);
995
+ }
996
+ function asRecord(value) {
997
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
998
+ return value;
999
+ }
1000
+ return {};
1001
+ }
1002
+ async function handleHostCallback(request, context) {
1003
+ const payload = request.payload;
1004
+ if (payload.type !== "host_callback") {
1005
+ return {
1006
+ type: "host_callback_result",
1007
+ invocation_id: "unknown",
1008
+ error: `unsupported sidecar request type: ${payload.type}`,
1009
+ };
1010
+ }
1011
+ const command = parseHostCommandCallbackInput(payload.input);
1012
+ if (command) {
1013
+ try {
1014
+ return {
1015
+ type: "host_callback_result",
1016
+ invocation_id: payload.invocation_id,
1017
+ result: await handleHostCommandCallback(command, context),
1018
+ };
1019
+ }
1020
+ catch (error) {
1021
+ return {
1022
+ type: "host_callback_result",
1023
+ invocation_id: payload.invocation_id,
1024
+ error: validationMessage(error),
1025
+ };
1026
+ }
1027
+ }
1028
+ const tool = context.toolMap.get(payload.callback_key);
1029
+ if (!tool) {
1030
+ return {
1031
+ type: "host_callback_result",
1032
+ invocation_id: payload.invocation_id,
1033
+ error: `Unknown tool "${payload.callback_key}"`,
1034
+ };
1035
+ }
1036
+ const permissionMode = toolPermissionMode(context.permissions, payload.callback_key);
1037
+ if (permissionMode !== "allow") {
1038
+ return {
1039
+ type: "host_callback_result",
1040
+ invocation_id: payload.invocation_id,
1041
+ error: `EACCES: blocked by tool.invoke policy for ${payload.callback_key}`,
1042
+ };
1043
+ }
1044
+ const parsed = tool.inputSchema.safeParse(payload.input);
1045
+ if (!parsed.success) {
1046
+ return {
1047
+ type: "host_callback_result",
1048
+ invocation_id: payload.invocation_id,
1049
+ error: validationMessage(parsed.error),
1050
+ };
1051
+ }
1052
+ try {
1053
+ return {
1054
+ type: "host_callback_result",
1055
+ invocation_id: payload.invocation_id,
1056
+ result: await executeHostTool(tool, payload.callback_key, parsed.data),
1057
+ };
1058
+ }
1059
+ catch (error) {
1060
+ return {
1061
+ type: "host_callback_result",
1062
+ invocation_id: payload.invocation_id,
1063
+ error: validationMessage(error),
1064
+ };
1065
+ }
1066
+ }
1067
+ function buildToolMap(toolKits) {
1068
+ const toolMap = new Map();
1069
+ for (const toolKit of toolKits) {
1070
+ for (const [toolName, tool] of Object.entries(toolKit.tools)) {
1071
+ toolMap.set(`${toolKit.name}:${toolName}`, tool);
1072
+ }
1073
+ }
1074
+ return toolMap;
1075
+ }
1076
+ function parseHostCommandCallbackInput(input) {
1077
+ const value = asRecord(input);
1078
+ if (value.type !== "command" ||
1079
+ typeof value.command !== "string" ||
1080
+ typeof value.cwd !== "string" ||
1081
+ !Array.isArray(value.args) ||
1082
+ !value.args.every((arg) => typeof arg === "string")) {
1083
+ return null;
1084
+ }
1085
+ return {
1086
+ type: "command",
1087
+ command: value.command,
1088
+ args: value.args,
1089
+ cwd: value.cwd,
1090
+ };
1091
+ }
1092
+ async function handleHostCommandCallback(command, context) {
1093
+ const directToolKit = context.toolKits.find((toolKit) => `agentos-${toolKit.name}` === command.command);
1094
+ if (command.command === "agentos") {
1095
+ return handleAgentOsRegistryCommand(command, context);
1096
+ }
1097
+ if (directToolKit) {
1098
+ return handleAgentOsToolkitCommand(command, context, directToolKit);
1099
+ }
1100
+ throw new Error(`Unknown host callback command "${command.command}"`);
1101
+ }
1102
+ async function handleAgentOsRegistryCommand(command, context) {
1103
+ const [subcommand, toolkitName, toolName, ...toolArgs] = command.args;
1104
+ if (!subcommand || isHelpFlag(subcommand)) {
1105
+ return {
1106
+ usage: "agentos <command>: list-tools [toolkit], <toolkit> --help, or <toolkit> <tool> ...",
1107
+ };
1108
+ }
1109
+ if (subcommand === "list-tools") {
1110
+ return toolkitName
1111
+ ? describeToolkitPayload(context.toolKits, toolkitName)
1112
+ : listToolkitsPayload(context.toolKits);
1113
+ }
1114
+ const toolKit = context.toolKits.find((kit) => kit.name === subcommand);
1115
+ if (!toolKit) {
1116
+ throw new Error(`No toolkit "${subcommand}". Available: ${toolkitNames(context.toolKits)}`);
1117
+ }
1118
+ if (!toolkitName || isHelpFlag(toolkitName)) {
1119
+ return describeToolkitPayload(context.toolKits, subcommand);
1120
+ }
1121
+ if (toolName && isHelpFlag(toolName)) {
1122
+ return describeToolPayload(toolKit, toolkitName);
1123
+ }
1124
+ return invokeHostTool({
1125
+ toolKit,
1126
+ toolName: toolkitName,
1127
+ args: [toolName, ...toolArgs].filter((value) => typeof value === "string"),
1128
+ cwd: command.cwd,
1129
+ context,
1130
+ });
1131
+ }
1132
+ async function handleAgentOsToolkitCommand(command, context, toolKit) {
1133
+ const [toolName, helpOrFirstArg, ...rest] = command.args;
1134
+ if (!toolName || isHelpFlag(toolName)) {
1135
+ return describeToolkitPayload(context.toolKits, toolKit.name);
1136
+ }
1137
+ if (helpOrFirstArg && isHelpFlag(helpOrFirstArg)) {
1138
+ return describeToolPayload(toolKit, toolName);
1139
+ }
1140
+ return invokeHostTool({
1141
+ toolKit,
1142
+ toolName,
1143
+ args: [helpOrFirstArg, ...rest].filter((value) => typeof value === "string"),
1144
+ cwd: command.cwd,
1145
+ context,
1146
+ });
1147
+ }
1148
+ async function invokeHostTool({ toolKit, toolName, args, cwd, context, }) {
1149
+ const tool = toolKit.tools[toolName];
1150
+ if (!tool) {
1151
+ throw new Error(`No tool "${toolName}" in toolkit "${toolKit.name}". Available: ${toolNames(toolKit)}`);
1152
+ }
1153
+ const callbackKey = `${toolKit.name}:${toolName}`;
1154
+ const permissionMode = toolPermissionMode(context.permissions, callbackKey);
1155
+ if (permissionMode !== "allow") {
1156
+ throw new Error(`EACCES: blocked by tool.invoke policy for ${callbackKey}`);
1157
+ }
1158
+ const input = await parseHostToolInput(tool, args, cwd, context.readFile);
1159
+ return executeHostTool(tool, callbackKey, input);
1160
+ }
1161
+ async function executeHostTool(tool, callbackKey, input) {
1162
+ const parsed = tool.inputSchema.safeParse(input);
1163
+ if (!parsed.success) {
1164
+ throw new Error(validationMessage(parsed.error));
1165
+ }
1166
+ return Promise.race([
1167
+ Promise.resolve(tool.execute(parsed.data)),
1168
+ new Promise((_, reject) => {
1169
+ if (tool.timeout === undefined) {
1170
+ return;
1171
+ }
1172
+ setTimeout(() => reject(new Error(`Tool "${callbackKey}" timed out after ${tool.timeout}ms`)), tool.timeout);
1173
+ }),
1174
+ ]);
1175
+ }
1176
+ async function parseHostToolInput(tool, args, cwd, readFile) {
1177
+ if (args[0] === "--json") {
1178
+ const value = args[1];
1179
+ if (value === undefined) {
1180
+ throw new Error("Flag --json requires a value");
1181
+ }
1182
+ return JSON.parse(value);
1183
+ }
1184
+ if (args[0] === "--json-file") {
1185
+ const value = args[1];
1186
+ if (value === undefined) {
1187
+ throw new Error("Flag --json-file requires a value");
1188
+ }
1189
+ const guestPath = value.startsWith("/")
1190
+ ? posixPath.normalize(value)
1191
+ : posixPath.normalize(`${cwd}/${value}`);
1192
+ const text = new TextDecoder().decode(await readFile(guestPath));
1193
+ return JSON.parse(text);
1194
+ }
1195
+ return parseToolArgv(toolToSidecarDefinition(tool).inputSchema, args);
1196
+ }
1197
+ function parseToolArgv(schema, argv) {
1198
+ const schemaObject = asRecord(schema);
1199
+ const properties = asRecord(schemaObject.properties);
1200
+ const required = Array.isArray(schemaObject.required)
1201
+ ? new Set(schemaObject.required.filter((value) => typeof value === "string"))
1202
+ : new Set();
1203
+ const flagToField = new Map();
1204
+ for (const [fieldName, fieldSchema] of Object.entries(properties)) {
1205
+ flagToField.set(camelToKebab(fieldName), [fieldName, fieldSchema]);
1206
+ }
1207
+ const input = {};
1208
+ for (let index = 0; index < argv.length;) {
1209
+ const arg = argv[index];
1210
+ if (!arg?.startsWith("--")) {
1211
+ throw new Error(`Unexpected positional argument: "${arg}"`);
1212
+ }
1213
+ const rawFlag = arg.slice(2);
1214
+ const negated = rawFlag.startsWith("no-");
1215
+ const flagName = negated ? rawFlag.slice(3) : rawFlag;
1216
+ const entry = flagToField.get(flagName);
1217
+ if (!entry) {
1218
+ throw new Error(`Unknown flag: --${rawFlag}`);
1219
+ }
1220
+ const [fieldName, fieldSchema] = entry;
1221
+ const fieldType = jsonSchemaType(fieldSchema);
1222
+ if (negated) {
1223
+ if (fieldType !== "boolean") {
1224
+ throw new Error(`Unknown flag: --${rawFlag}`);
1225
+ }
1226
+ input[fieldName] = false;
1227
+ index += 1;
1228
+ continue;
1229
+ }
1230
+ if (fieldType === "boolean") {
1231
+ input[fieldName] = true;
1232
+ index += 1;
1233
+ continue;
1234
+ }
1235
+ const value = argv[index + 1];
1236
+ if (value === undefined) {
1237
+ throw new Error(`Flag --${rawFlag} requires a value`);
1238
+ }
1239
+ if (fieldType === "number" || fieldType === "integer") {
1240
+ const number = Number(value);
1241
+ if (!Number.isFinite(number)) {
1242
+ throw new Error(`Flag --${rawFlag} expects a number, got "${value}"`);
1243
+ }
1244
+ input[fieldName] = number;
1245
+ index += 2;
1246
+ continue;
1247
+ }
1248
+ if (fieldType === "array") {
1249
+ const current = Array.isArray(input[fieldName])
1250
+ ? input[fieldName]
1251
+ : [];
1252
+ const itemSchema = asRecord(fieldSchema).items;
1253
+ const itemType = jsonSchemaType(itemSchema);
1254
+ current.push(itemType === "number" || itemType === "integer" ? Number(value) : value);
1255
+ input[fieldName] = current;
1256
+ index += 2;
1257
+ continue;
1258
+ }
1259
+ input[fieldName] = value;
1260
+ index += 2;
1261
+ }
1262
+ for (const fieldName of required) {
1263
+ if (!(fieldName in input)) {
1264
+ throw new Error(`Missing required flag: --${camelToKebab(fieldName)}`);
1265
+ }
1266
+ }
1267
+ return input;
1268
+ }
1269
+ function listToolkitsPayload(toolKits) {
1270
+ return {
1271
+ toolkits: toolKits.map((toolKit) => ({
1272
+ name: toolKit.name,
1273
+ description: toolKit.description,
1274
+ tools: Object.keys(toolKit.tools),
1275
+ })),
1276
+ };
1277
+ }
1278
+ function describeToolkitPayload(toolKits, toolkitName) {
1279
+ const toolKit = toolKits.find((kit) => kit.name === toolkitName);
1280
+ if (!toolKit) {
1281
+ throw new Error(`No toolkit "${toolkitName}". Available: ${toolkitNames(toolKits)}`);
1282
+ }
1283
+ return {
1284
+ name: toolKit.name,
1285
+ description: toolKit.description,
1286
+ tools: Object.fromEntries(Object.entries(toolKit.tools).map(([toolName, tool]) => [
1287
+ toolName,
1288
+ {
1289
+ description: tool.description,
1290
+ flags: describeToolFlags(toolToSidecarDefinition(tool).inputSchema),
1291
+ },
1292
+ ])),
1293
+ };
1294
+ }
1295
+ function describeToolPayload(toolKit, toolName) {
1296
+ const tool = toolKit.tools[toolName];
1297
+ if (!tool) {
1298
+ throw new Error(`No tool "${toolName}" in toolkit "${toolKit.name}". Available: ${toolNames(toolKit)}`);
1299
+ }
1300
+ return {
1301
+ toolkit: toolKit.name,
1302
+ tool: toolName,
1303
+ description: tool.description,
1304
+ flags: describeToolFlags(toolToSidecarDefinition(tool).inputSchema),
1305
+ examples: tool.examples?.map((example) => ({
1306
+ description: example.description,
1307
+ input: example.input,
1308
+ })) ?? [],
1309
+ };
1310
+ }
1311
+ function toolPermissionMode(permissions, callbackKey) {
1312
+ const scope = permissions.tool;
1313
+ if (!scope) {
1314
+ return "deny";
1315
+ }
1316
+ if (typeof scope === "string") {
1317
+ return scope;
1318
+ }
1319
+ let mode = scope.default ?? "deny";
1320
+ for (const rule of scope.rules) {
1321
+ const operations = rule.operations ?? ["*"];
1322
+ const patterns = rule.patterns ?? ["**"];
1323
+ if (operations.some((operation) => operation === "*" || operation === "invoke") &&
1324
+ patterns.some((pattern) => permissionPatternMatches(pattern, callbackKey))) {
1325
+ mode = rule.mode;
1326
+ }
1327
+ }
1328
+ return mode;
1329
+ }
1330
+ function permissionPatternMatches(pattern, value) {
1331
+ if (pattern === "*" || pattern === "**" || pattern === value) {
1332
+ return true;
1333
+ }
1334
+ const parts = pattern.split(/(\*\*|\*)/u);
1335
+ const source = parts
1336
+ .map((part) => {
1337
+ if (part === "**")
1338
+ return ".*";
1339
+ if (part === "*")
1340
+ return "[^:]*";
1341
+ return part.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
1342
+ })
1343
+ .join("");
1344
+ return new RegExp(`^${source}$`).test(value);
1345
+ }
1346
+ function toolkitNames(toolKits) {
1347
+ return toolKits.map((toolKit) => toolKit.name).join(", ");
1348
+ }
1349
+ function toolNames(toolKit) {
1350
+ return Object.keys(toolKit.tools).join(", ");
1351
+ }
1352
+ function isHelpFlag(value) {
1353
+ return value === "--help" || value === "-h";
1354
+ }
1355
+ function jsonSchemaType(schema) {
1356
+ const schemaObject = asRecord(schema);
1357
+ return typeof schemaObject.type === "string" ? schemaObject.type : undefined;
1358
+ }
1359
+ async function registerToolkitsOnSidecar(client, session, vm, toolKits) {
1360
+ if (toolKits.length === 0) {
1361
+ return "";
1362
+ }
1363
+ for (const toolKit of toolKits) {
1364
+ await client.registerHostCallbacks(session, vm, {
1365
+ name: toolKit.name,
1366
+ description: toolKit.description,
1367
+ commandAliases: [`agentos-${toolKit.name}`],
1368
+ registryCommandAliases: ["agentos"],
1369
+ callbacks: Object.fromEntries(Object.entries(toolKit.tools).map(([toolName, tool]) => [
1370
+ toolName,
1371
+ toolToSidecarDefinition(tool),
1372
+ ])),
1373
+ });
1374
+ }
1375
+ return buildHostToolReference(toolKits);
1376
+ }
1377
+ export class AgentOs {
1378
+ #kernel;
1379
+ sidecar;
1380
+ _sessions = new Map();
1381
+ _closedSessionIds = new BoundedSet(CLOSED_SESSION_ID_RETENTION_LIMIT);
1382
+ _sessionClosePromises = new Map();
1383
+ _pendingSessionRequestResolvers = new Map();
1384
+ _processes = new Map();
1385
+ _shells = new Map();
1386
+ _closedShellIds = new BoundedSet(CLOSED_SHELL_ID_RETENTION_LIMIT);
1387
+ _pendingShellExitPromises = new Set();
1388
+ _shellCounter = 0;
1389
+ _acpTerminals = new Map();
1390
+ _acpTerminalCounter = 0;
1391
+ _softwareRoots;
1392
+ _softwareAgentConfigs;
1393
+ _cronManager;
1394
+ _toolKits = [];
1395
+ _toolReference = "";
1396
+ _permissions = allowAll;
1397
+ _hostMounts;
1398
+ _env;
1399
+ _rootFilesystem;
1400
+ _sidecarLease = null;
1401
+ _sidecarClient;
1402
+ _sidecarSession;
1403
+ _sidecarVm;
1404
+ _disposeSidecarEventListener;
1405
+ constructor(kernel, sidecar, softwareRoots, softwareAgentConfigs, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm) {
1406
+ this.#kernel = kernel;
1407
+ this.sidecar = sidecar;
1408
+ this._softwareRoots = softwareRoots;
1409
+ this._softwareAgentConfigs = softwareAgentConfigs;
1410
+ this._hostMounts = hostMounts;
1411
+ this._env = env;
1412
+ this._rootFilesystem = rootFilesystem;
1413
+ this._sidecarClient = sidecarClient;
1414
+ this._sidecarSession = sidecarSession;
1415
+ this._sidecarVm = sidecarVm;
1416
+ this._disposeSidecarEventListener = this._sidecarClient.onEvent((event) => {
1417
+ this._handleSidecarEvent(event);
1418
+ });
1419
+ agentOsRuntimeAdmins.set(this, {
1420
+ kernel,
1421
+ rootView: rootFilesystem,
1422
+ env,
1423
+ sidecar,
1424
+ });
1425
+ }
1426
+ static async createSidecar(options = {}) {
1427
+ return createAgentOsSidecarInternal(options);
1428
+ }
1429
+ static async getSharedSidecar(options = {}) {
1430
+ return getSharedAgentOsSidecarInternal(options);
1431
+ }
1432
+ static async create(options) {
1433
+ const processed = processSoftware(options?.software ?? []);
1434
+ const localMounts = await resolveCompatLocalMounts(options?.mounts);
1435
+ const toolKits = options?.toolKits;
1436
+ if (toolKits && toolKits.length > 0) {
1437
+ validateToolkits(toolKits);
1438
+ }
1439
+ const createVmAdmin = async () => {
1440
+ const preparedCommandDirs = prepareCommandDirs(processed.commandPackages);
1441
+ const toolBootstrapCommands = collectToolkitBootstrapCommands(toolKits ?? []);
1442
+ const bootstrapLower = createKernelBootstrapLower(options?.rootFilesystem, [
1443
+ ...collectBootstrapWasmCommands(preparedCommandDirs.commandDirs),
1444
+ ...NODE_RUNTIME_BOOTSTRAP_COMMANDS,
1445
+ ...toolBootstrapCommands,
1446
+ ]);
1447
+ let toolReference = "";
1448
+ let rootBridge = null;
1449
+ let kernel = null;
1450
+ let client = null;
1451
+ let toolShimDir = null;
1452
+ let cleanedUp = false;
1453
+ const cleanup = async () => {
1454
+ if (cleanedUp) {
1455
+ return;
1456
+ }
1457
+ cleanedUp = true;
1458
+ if (toolShimDir) {
1459
+ rmSync(toolShimDir, { recursive: true, force: true });
1460
+ toolShimDir = null;
1461
+ }
1462
+ preparedCommandDirs.dispose();
1463
+ };
1464
+ try {
1465
+ const env = getBaseEnvironment();
1466
+ if (toolKits && toolKits.length > 0) {
1467
+ toolShimDir = materializeToolShimDir(toolKits);
1468
+ }
1469
+ const commandGuestPaths = collectGuestCommandPaths(preparedCommandDirs.commandDirs);
1470
+ const { sidecarMounts, hostMounts, hostPathMappings } = collectSidecarMountPlan({
1471
+ mounts: options?.mounts,
1472
+ softwareRoots: processed.softwareRoots,
1473
+ commandDirs: preparedCommandDirs.commandDirs,
1474
+ shimDir: toolShimDir,
1475
+ });
1476
+ client = SidecarProcess.spawn({
1477
+ cwd: REPO_ROOT,
1478
+ command: ensureNativeSidecarBinary(),
1479
+ args: [],
1480
+ frameTimeoutMs: NATIVE_SIDECAR_FRAME_TIMEOUT_MS,
1481
+ });
1482
+ const session = await client.authenticateAndOpenSession();
1483
+ const hostPermissions = options?.permissions ?? {
1484
+ ...allowAll,
1485
+ tool: "allow",
1486
+ };
1487
+ const sidecarPermissions = serializePermissionsForSidecar(hostPermissions);
1488
+ const createVmConfig = {
1489
+ env,
1490
+ rootFilesystem: serializeRootFilesystemForSidecar(options?.rootFilesystem, bootstrapLower),
1491
+ permissions: sidecarPermissions,
1492
+ limits: options?.limits,
1493
+ loopbackExemptPorts: options?.loopbackExemptPorts ?? [],
1494
+ // 0.3: the Node builtin allow-list moved from configureVm to
1495
+ // VM creation. `undefined` => engine default allow-list;
1496
+ // `[]` => deny all; `[..]` => exactly those. Platform and
1497
+ // module resolution keep their engine defaults (full Node
1498
+ // emulation), matching the prior behavior where Agent OS only
1499
+ // constrained the builtin allow-list.
1500
+ ...(options?.allowedNodeBuiltins !== undefined
1501
+ ? {
1502
+ jsRuntime: {
1503
+ platform: "node",
1504
+ moduleResolution: "node",
1505
+ allowedBuiltins: options.allowedNodeBuiltins,
1506
+ },
1507
+ }
1508
+ : {}),
1509
+ };
1510
+ const nativeVm = await client.createVm(session, {
1511
+ runtime: "java_script",
1512
+ config: createVmConfig,
1513
+ });
1514
+ await client.waitForEvent((event) => event.payload.type === "vm_lifecycle" &&
1515
+ event.payload.state === "ready", 10_000);
1516
+ await client.configureVm(session, nativeVm, {
1517
+ mounts: sidecarMounts,
1518
+ permissions: sidecarPermissions,
1519
+ commandPermissions: processed.commandPermissions,
1520
+ loopbackExemptPorts: options?.loopbackExemptPorts,
1521
+ });
1522
+ if (toolKits && toolKits.length > 0) {
1523
+ toolReference = await registerToolkitsOnSidecar(client, session, nativeVm, toolKits);
1524
+ commandGuestPaths.set("agentos", "/bin/agentos");
1525
+ for (const toolKit of toolKits) {
1526
+ commandGuestPaths.set(`agentos-${toolKit.name}`, `/bin/agentos-${toolKit.name}`);
1527
+ }
1528
+ }
1529
+ rootBridge = new NativeSidecarKernelProxy({
1530
+ client,
1531
+ session,
1532
+ vm: nativeVm,
1533
+ env,
1534
+ cwd: "/home/user",
1535
+ localMounts,
1536
+ commandGuestPaths,
1537
+ onDispose: cleanup,
1538
+ });
1539
+ await bootstrapLiveBootstrapDirectories(client, session, nativeVm, options?.rootFilesystem);
1540
+ kernel = rootBridge;
1541
+ const snapshotClient = client;
1542
+ return {
1543
+ env,
1544
+ hostMounts,
1545
+ kernel,
1546
+ rootView: rootBridge.createRootView(),
1547
+ sidecarClient: client,
1548
+ sidecarSession: session,
1549
+ sidecarVm: nativeVm,
1550
+ permissions: hostPermissions,
1551
+ snapshotRootFilesystem: async () => createSnapshotExport(convertSidecarRootSnapshotEntries(await snapshotClient.snapshotRootFilesystem(session, nativeVm))),
1552
+ toolKits: toolKits ?? [],
1553
+ toolReference,
1554
+ async dispose() {
1555
+ if (kernel) {
1556
+ const currentKernel = kernel;
1557
+ kernel = null;
1558
+ await currentKernel.dispose();
1559
+ }
1560
+ if (rootBridge) {
1561
+ const currentRootBridge = rootBridge;
1562
+ rootBridge = null;
1563
+ await currentRootBridge.dispose();
1564
+ return;
1565
+ }
1566
+ await cleanup();
1567
+ },
1568
+ };
1569
+ }
1570
+ catch (error) {
1571
+ if (kernel) {
1572
+ await kernel.dispose().catch(() => { });
1573
+ }
1574
+ if (rootBridge) {
1575
+ await rootBridge.dispose().catch(() => { });
1576
+ }
1577
+ else {
1578
+ await client?.dispose().catch(() => { });
1579
+ await cleanup();
1580
+ }
1581
+ throw error;
1582
+ }
1583
+ };
1584
+ const sidecar = resolveAgentOsSidecar(options?.sidecar);
1585
+ let sidecarLease = null;
1586
+ try {
1587
+ sidecarLease = await leaseAgentOsSidecarVm(sidecar, {
1588
+ createVm: async () => createVmAdmin(),
1589
+ });
1590
+ const vmAdmin = sidecarLease.admin;
1591
+ const vm = new AgentOs(vmAdmin.kernel, sidecar, processed.softwareRoots, processed.agentConfigs, vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm);
1592
+ vm._sidecarLease = sidecarLease;
1593
+ vm._toolKits = vmAdmin.toolKits;
1594
+ vm._toolReference = vmAdmin.toolReference;
1595
+ vm._permissions = vmAdmin.permissions;
1596
+ vm._installSidecarRequestHandler();
1597
+ vm._cronManager = new CronManager(vm, options?.scheduleDriver ?? new TimerScheduleDriver());
1598
+ return vm;
1599
+ }
1600
+ catch (error) {
1601
+ await sidecarLease?.dispose().catch(() => { });
1602
+ throw error;
1603
+ }
1604
+ }
1605
+ async exec(command, options) {
1606
+ return this.#kernel.exec(command, options);
1607
+ }
1608
+ _trackProcess(proc, command, args, stdoutHandlers, stderrHandlers, exitHandlers) {
1609
+ const entry = {
1610
+ proc,
1611
+ command,
1612
+ args,
1613
+ stdoutHandlers,
1614
+ stderrHandlers,
1615
+ exitHandlers,
1616
+ };
1617
+ this._processes.set(proc.pid, entry);
1618
+ void proc.wait().then((code) => {
1619
+ for (const h of exitHandlers)
1620
+ h(code);
1621
+ });
1622
+ return { pid: proc.pid };
1623
+ }
1624
+ spawn(command, args, options) {
1625
+ const stdoutHandlers = new Set();
1626
+ const stderrHandlers = new Set();
1627
+ const exitHandlers = new Set();
1628
+ // Include caller-provided callbacks in the initial handler sets.
1629
+ if (options?.onStdout)
1630
+ stdoutHandlers.add(options.onStdout);
1631
+ if (options?.onStderr)
1632
+ stderrHandlers.add(options.onStderr);
1633
+ const proc = this.#kernel.spawn(command, args, {
1634
+ ...options,
1635
+ onStdout: (data) => {
1636
+ for (const h of stdoutHandlers)
1637
+ h(data);
1638
+ },
1639
+ onStderr: (data) => {
1640
+ for (const h of stderrHandlers)
1641
+ h(data);
1642
+ },
1643
+ });
1644
+ return this._trackProcess(proc, command, args, stdoutHandlers, stderrHandlers, exitHandlers);
1645
+ }
1646
+ /** Write data to a process's stdin. */
1647
+ writeProcessStdin(pid, data) {
1648
+ const entry = this._processes.get(pid);
1649
+ if (!entry)
1650
+ throw new Error(`Process not found: ${pid}`);
1651
+ entry.proc.writeStdin(data);
1652
+ }
1653
+ /** Close a process's stdin stream. */
1654
+ closeProcessStdin(pid) {
1655
+ const entry = this._processes.get(pid);
1656
+ if (!entry)
1657
+ throw new Error(`Process not found: ${pid}`);
1658
+ entry.proc.closeStdin();
1659
+ }
1660
+ /** Subscribe to stdout data from a process. Returns an unsubscribe function. */
1661
+ onProcessStdout(pid, handler) {
1662
+ const entry = this._processes.get(pid);
1663
+ if (!entry)
1664
+ throw new Error(`Process not found: ${pid}`);
1665
+ entry.stdoutHandlers.add(handler);
1666
+ return () => {
1667
+ entry.stdoutHandlers.delete(handler);
1668
+ };
1669
+ }
1670
+ /** Subscribe to stderr data from a process. Returns an unsubscribe function. */
1671
+ onProcessStderr(pid, handler) {
1672
+ const entry = this._processes.get(pid);
1673
+ if (!entry)
1674
+ throw new Error(`Process not found: ${pid}`);
1675
+ entry.stderrHandlers.add(handler);
1676
+ return () => {
1677
+ entry.stderrHandlers.delete(handler);
1678
+ };
1679
+ }
1680
+ /** Subscribe to process exit. Returns an unsubscribe function. */
1681
+ onProcessExit(pid, handler) {
1682
+ const entry = this._processes.get(pid);
1683
+ if (!entry)
1684
+ throw new Error(`Process not found: ${pid}`);
1685
+ // If already exited, call immediately.
1686
+ if (entry.proc.exitCode !== null) {
1687
+ handler(entry.proc.exitCode);
1688
+ return () => { };
1689
+ }
1690
+ entry.exitHandlers.add(handler);
1691
+ return () => {
1692
+ entry.exitHandlers.delete(handler);
1693
+ };
1694
+ }
1695
+ /** Wait for a process to exit. Returns the exit code. */
1696
+ waitProcess(pid) {
1697
+ const entry = this._processes.get(pid);
1698
+ if (!entry)
1699
+ throw new Error(`Process not found: ${pid}`);
1700
+ return entry.proc.wait();
1701
+ }
1702
+ _assertSafeAbsolutePath(path) {
1703
+ if (!path.startsWith("/")) {
1704
+ throw new Error(`Path must be absolute: ${path}`);
1705
+ }
1706
+ if (posixPath.normalize(path) !== path) {
1707
+ throw new Error(`Path must be normalized: ${path}`);
1708
+ }
1709
+ }
1710
+ _assertWritableAbsolutePath(path) {
1711
+ this._assertSafeAbsolutePath(path);
1712
+ if (path === "/proc" || path.startsWith("/proc/")) {
1713
+ throw new Error(`Path is read-only: ${path}`);
1714
+ }
1715
+ }
1716
+ _vfs() {
1717
+ return this.#kernel.vfs;
1718
+ }
1719
+ async _copyPath(from, to) {
1720
+ const stat = await this._vfs().lstat(from);
1721
+ if (stat.isSymbolicLink) {
1722
+ const target = await this._vfs().readlink(from);
1723
+ await this._vfs().symlink(target, to);
1724
+ return;
1725
+ }
1726
+ if (stat.isDirectory) {
1727
+ await this._mkdirp(posixPath.dirname(to));
1728
+ if (!(await this.#kernel.exists(to))) {
1729
+ await this.#kernel.mkdir(to);
1730
+ }
1731
+ await this._vfs().chmod(to, stat.mode);
1732
+ await this._vfs().chown(to, stat.uid, stat.gid);
1733
+ const entries = await this.#kernel.readdir(from);
1734
+ for (const entry of entries) {
1735
+ if (entry === "." || entry === "..")
1736
+ continue;
1737
+ const fromPath = from === "/" ? `/${entry}` : `${from}/${entry}`;
1738
+ const toPath = to === "/" ? `/${entry}` : `${to}/${entry}`;
1739
+ await this._copyPath(fromPath, toPath);
1740
+ }
1741
+ return;
1742
+ }
1743
+ const content = await this.#kernel.readFile(from);
1744
+ await this.writeFile(to, content);
1745
+ await this._vfs().chmod(to, stat.mode);
1746
+ await this._vfs().chown(to, stat.uid, stat.gid);
1747
+ }
1748
+ async readFile(path) {
1749
+ this._assertSafeAbsolutePath(path);
1750
+ return this.#kernel.readFile(path);
1751
+ }
1752
+ async writeFile(path, content) {
1753
+ this._assertWritableAbsolutePath(path);
1754
+ return this.#kernel.writeFile(path, content);
1755
+ }
1756
+ async writeFiles(entries) {
1757
+ const results = [];
1758
+ for (const entry of entries) {
1759
+ try {
1760
+ this._assertWritableAbsolutePath(entry.path);
1761
+ // Create parent directories as needed
1762
+ const parentDir = entry.path.substring(0, entry.path.lastIndexOf("/"));
1763
+ if (parentDir) {
1764
+ await this._mkdirp(parentDir);
1765
+ }
1766
+ await this.#kernel.writeFile(entry.path, entry.content);
1767
+ results.push({ path: entry.path, success: true });
1768
+ }
1769
+ catch (err) {
1770
+ results.push({
1771
+ path: entry.path,
1772
+ success: false,
1773
+ error: err instanceof Error ? err.message : String(err),
1774
+ });
1775
+ }
1776
+ }
1777
+ return results;
1778
+ }
1779
+ async readFiles(paths) {
1780
+ const results = [];
1781
+ for (const path of paths) {
1782
+ try {
1783
+ this._assertSafeAbsolutePath(path);
1784
+ const content = await this.#kernel.readFile(path);
1785
+ results.push({ path, content });
1786
+ }
1787
+ catch (err) {
1788
+ results.push({
1789
+ path,
1790
+ content: null,
1791
+ error: err instanceof Error ? err.message : String(err),
1792
+ });
1793
+ }
1794
+ }
1795
+ return results;
1796
+ }
1797
+ /** Recursively create directories (mkdir -p). */
1798
+ async _mkdirp(path) {
1799
+ this._assertWritableAbsolutePath(path);
1800
+ const parts = path.split("/").filter(Boolean);
1801
+ let current = "";
1802
+ for (const part of parts) {
1803
+ current += `/${part}`;
1804
+ if (!(await this.#kernel.exists(current))) {
1805
+ await this.#kernel.mkdir(current);
1806
+ }
1807
+ }
1808
+ }
1809
+ async mkdir(path, options) {
1810
+ if (options?.recursive) {
1811
+ return this._mkdirp(path);
1812
+ }
1813
+ this._assertSafeAbsolutePath(path);
1814
+ return this.#kernel.mkdir(path);
1815
+ }
1816
+ async readdir(path) {
1817
+ this._assertSafeAbsolutePath(path);
1818
+ return this.#kernel.readdir(path);
1819
+ }
1820
+ async readdirRecursive(path, options) {
1821
+ this._assertSafeAbsolutePath(path);
1822
+ const maxDepth = options?.maxDepth;
1823
+ const exclude = options?.exclude ? new Set(options.exclude) : undefined;
1824
+ const results = [];
1825
+ // BFS queue: [dirPath, currentDepth]
1826
+ const queue = [[path, 0]];
1827
+ while (queue.length > 0) {
1828
+ const item = queue.shift();
1829
+ if (!item)
1830
+ break;
1831
+ const [dirPath, depth] = item;
1832
+ const entries = await this.#kernel.readdir(dirPath);
1833
+ for (const name of entries) {
1834
+ if (name === "." || name === "..")
1835
+ continue;
1836
+ if (exclude?.has(name))
1837
+ continue;
1838
+ const fullPath = dirPath === "/" ? `/${name}` : `${dirPath}/${name}`;
1839
+ const s = await this.#kernel.stat(fullPath);
1840
+ if (s.isSymbolicLink) {
1841
+ results.push({
1842
+ path: fullPath,
1843
+ type: "symlink",
1844
+ size: s.size,
1845
+ });
1846
+ }
1847
+ else if (s.isDirectory) {
1848
+ results.push({
1849
+ path: fullPath,
1850
+ type: "directory",
1851
+ size: s.size,
1852
+ });
1853
+ if (maxDepth === undefined || depth < maxDepth) {
1854
+ queue.push([fullPath, depth + 1]);
1855
+ }
1856
+ }
1857
+ else {
1858
+ results.push({
1859
+ path: fullPath,
1860
+ type: "file",
1861
+ size: s.size,
1862
+ });
1863
+ }
1864
+ }
1865
+ }
1866
+ return results;
1867
+ }
1868
+ async stat(path) {
1869
+ this._assertSafeAbsolutePath(path);
1870
+ return this.#kernel.stat(path);
1871
+ }
1872
+ async exists(path) {
1873
+ this._assertSafeAbsolutePath(path);
1874
+ return this.#kernel.exists(path);
1875
+ }
1876
+ async snapshotRootFilesystem() {
1877
+ const nativeSnapshot = this._sidecarLease?.admin.snapshotRootFilesystem;
1878
+ if (nativeSnapshot) {
1879
+ return nativeSnapshot();
1880
+ }
1881
+ return createSnapshotExport(await snapshotVirtualFilesystem(this._rootFilesystem));
1882
+ }
1883
+ mountFs(path, driver, options) {
1884
+ this._assertSafeAbsolutePath(path);
1885
+ this.#kernel.mountFs(path, driver, { readOnly: options?.readOnly });
1886
+ }
1887
+ unmountFs(path) {
1888
+ this._assertSafeAbsolutePath(path);
1889
+ this.#kernel.unmountFs(path);
1890
+ }
1891
+ async move(from, to) {
1892
+ this._assertSafeAbsolutePath(from);
1893
+ this._assertSafeAbsolutePath(to);
1894
+ const sourceStat = await this._vfs().lstat(from);
1895
+ if (!sourceStat.isDirectory || sourceStat.isSymbolicLink) {
1896
+ return this.#kernel.rename(from, to);
1897
+ }
1898
+ await this._copyPath(from, to);
1899
+ await this.delete(from, { recursive: true });
1900
+ }
1901
+ async delete(path, options) {
1902
+ this._assertSafeAbsolutePath(path);
1903
+ const s = await this.#kernel.stat(path);
1904
+ if (s.isDirectory) {
1905
+ if (options?.recursive) {
1906
+ const entries = await this.#kernel.readdir(path);
1907
+ for (const entry of entries) {
1908
+ if (entry === "." || entry === "..")
1909
+ continue;
1910
+ await this.delete(`${path}/${entry}`, { recursive: true });
1911
+ }
1912
+ }
1913
+ return this.#kernel.removeDir(path);
1914
+ }
1915
+ return this.#kernel.removeFile(path);
1916
+ }
1917
+ async fetch(port, request) {
1918
+ const url = new URL(request.url);
1919
+ const responsePayload = JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
1920
+ port,
1921
+ method: request.method,
1922
+ path: `${url.pathname}${url.search}`,
1923
+ headersJson: JSON.stringify(Object.fromEntries(request.headers.entries())),
1924
+ ...(request.method !== "GET" && request.method !== "HEAD"
1925
+ ? { body: await request.text() }
1926
+ : {}),
1927
+ }));
1928
+ const headers = new Headers();
1929
+ for (const [key, value] of responsePayload.headers ?? []) {
1930
+ headers.append(key, value);
1931
+ }
1932
+ return new Response(Buffer.from(responsePayload.body ?? "", "base64"), {
1933
+ status: responsePayload.status,
1934
+ statusText: responsePayload.statusText,
1935
+ headers,
1936
+ });
1937
+ }
1938
+ openShell(options) {
1939
+ const shellId = `shell-${++this._shellCounter}`;
1940
+ this._closedShellIds.delete(shellId);
1941
+ const dataHandlers = new Set();
1942
+ const handle = this.#kernel.openShell(options);
1943
+ handle.onData = (data) => {
1944
+ for (const h of dataHandlers)
1945
+ h(data);
1946
+ };
1947
+ const entry = {
1948
+ handle,
1949
+ dataHandlers,
1950
+ exitPromise: Promise.resolve(),
1951
+ };
1952
+ const exitPromise = handle.wait().then(() => undefined, () => undefined);
1953
+ entry.exitPromise = exitPromise.finally(() => {
1954
+ this._pendingShellExitPromises.delete(entry.exitPromise);
1955
+ if (this._shells.get(shellId) === entry) {
1956
+ this._shells.delete(shellId);
1957
+ this._closedShellIds.add(shellId);
1958
+ }
1959
+ });
1960
+ this._pendingShellExitPromises.add(entry.exitPromise);
1961
+ this._shells.set(shellId, entry);
1962
+ return { shellId };
1963
+ }
1964
+ async connectTerminal(options) {
1965
+ return this.#kernel.connectTerminal(options);
1966
+ }
1967
+ /** Write data to a shell's PTY input. */
1968
+ writeShell(shellId, data) {
1969
+ const entry = this._shells.get(shellId);
1970
+ if (!entry)
1971
+ throw new Error(`Shell not found: ${shellId}`);
1972
+ entry.handle.write(data);
1973
+ }
1974
+ /** Subscribe to data output from a shell. Returns an unsubscribe function. */
1975
+ onShellData(shellId, handler) {
1976
+ const entry = this._shells.get(shellId);
1977
+ if (!entry)
1978
+ throw new Error(`Shell not found: ${shellId}`);
1979
+ entry.dataHandlers.add(handler);
1980
+ return () => {
1981
+ entry.dataHandlers.delete(handler);
1982
+ };
1983
+ }
1984
+ /** Notify a shell of terminal resize. */
1985
+ resizeShell(shellId, cols, rows) {
1986
+ const entry = this._shells.get(shellId);
1987
+ if (!entry)
1988
+ throw new Error(`Shell not found: ${shellId}`);
1989
+ entry.handle.resize(cols, rows);
1990
+ }
1991
+ /** Kill a shell process and remove it from tracking. */
1992
+ closeShell(shellId) {
1993
+ const entry = this._shells.get(shellId);
1994
+ if (!entry) {
1995
+ if (this._closedShellIds.has(shellId)) {
1996
+ return;
1997
+ }
1998
+ throw new Error(`Shell not found: ${shellId}`);
1999
+ }
2000
+ entry.handle.kill();
2001
+ this._shells.delete(shellId);
2002
+ this._closedShellIds.add(shellId);
2003
+ }
2004
+ _resolveVmPathToHostPath(vmPath) {
2005
+ const normalizedVmPath = posixPath.normalize(vmPath);
2006
+ for (const mount of this._hostMounts) {
2007
+ if (normalizedVmPath === mount.vmPath ||
2008
+ normalizedVmPath.startsWith(`${mount.vmPath}/`)) {
2009
+ const relativePath = posixPath.relative(mount.vmPath, normalizedVmPath);
2010
+ if (!relativePath) {
2011
+ return mount.hostPath;
2012
+ }
2013
+ return join(mount.hostPath, ...relativePath.split("/").filter(Boolean));
2014
+ }
2015
+ }
2016
+ return null;
2017
+ }
2018
+ /** Returns info about all processes spawned via spawn(). */
2019
+ listProcesses() {
2020
+ return [...this._processes.values()].map(({ proc, command, args }) => ({
2021
+ pid: proc.pid,
2022
+ command,
2023
+ args,
2024
+ running: proc.exitCode === null,
2025
+ exitCode: proc.exitCode,
2026
+ }));
2027
+ }
2028
+ /** Returns all kernel processes across all active runtimes (WASM and Node). */
2029
+ allProcesses() {
2030
+ if (this.#kernel instanceof NativeSidecarKernelProxy) {
2031
+ return this.#kernel.snapshotProcesses();
2032
+ }
2033
+ return [...this.#kernel.processes.values()];
2034
+ }
2035
+ /** Returns processes organized as a tree using ppid relationships. */
2036
+ processTree() {
2037
+ const all = this.allProcesses();
2038
+ const nodeMap = new Map();
2039
+ // Index: create a tree node for each process
2040
+ for (const proc of all) {
2041
+ nodeMap.set(proc.pid, { ...proc, children: [] });
2042
+ }
2043
+ // Wire: attach each node to its parent
2044
+ const roots = [];
2045
+ for (const node of nodeMap.values()) {
2046
+ const parent = nodeMap.get(node.ppid);
2047
+ if (parent) {
2048
+ parent.children.push(node);
2049
+ }
2050
+ else {
2051
+ roots.push(node);
2052
+ }
2053
+ }
2054
+ return roots;
2055
+ }
2056
+ /** Returns info about a specific process by PID. Throws if not found. */
2057
+ getProcess(pid) {
2058
+ const entry = this._processes.get(pid);
2059
+ if (!entry) {
2060
+ throw new Error(`Process not found: ${pid}`);
2061
+ }
2062
+ return {
2063
+ pid: entry.proc.pid,
2064
+ command: entry.command,
2065
+ args: entry.args,
2066
+ running: entry.proc.exitCode === null,
2067
+ exitCode: entry.proc.exitCode,
2068
+ };
2069
+ }
2070
+ /** Send SIGTERM to gracefully stop a process. No-op if already exited. */
2071
+ stopProcess(pid) {
2072
+ const entry = this._processes.get(pid);
2073
+ if (!entry) {
2074
+ throw new Error(`Process not found: ${pid}`);
2075
+ }
2076
+ if (entry.proc.exitCode !== null)
2077
+ return;
2078
+ entry.proc.kill();
2079
+ }
2080
+ /** Send SIGKILL to force-kill a process. No-op if already exited. */
2081
+ killProcess(pid) {
2082
+ const entry = this._processes.get(pid);
2083
+ if (!entry) {
2084
+ throw new Error(`Process not found: ${pid}`);
2085
+ }
2086
+ if (entry.proc.exitCode !== null)
2087
+ return;
2088
+ entry.proc.kill(9);
2089
+ }
2090
+ /** Returns all active sessions with their IDs and agent types. */
2091
+ listSessions() {
2092
+ return [...this._sessions.values()].map((s) => ({
2093
+ sessionId: s.sessionId,
2094
+ agentType: s.agentType,
2095
+ }));
2096
+ }
2097
+ /** Internal helper: retrieve a session or throw. */
2098
+ _requireSession(sessionId) {
2099
+ const session = this._sessions.get(sessionId);
2100
+ if (!session) {
2101
+ throw new Error(`Session not found: ${sessionId}`);
2102
+ }
2103
+ return session;
2104
+ }
2105
+ /** Returns all registered agents with their installation status. */
2106
+ listAgents() {
2107
+ // Collect all agent IDs from both package configs and hardcoded configs.
2108
+ const allIds = new Set([
2109
+ ...this._softwareAgentConfigs.keys(),
2110
+ ...Object.keys(AGENT_CONFIGS),
2111
+ ]);
2112
+ return [...allIds]
2113
+ .map((id) => {
2114
+ const config = this._resolveAgentConfig(id);
2115
+ if (!config)
2116
+ return null;
2117
+ let installed = false;
2118
+ try {
2119
+ // Check software roots first, then the host dir behind the
2120
+ // `/root/node_modules` mount.
2121
+ const vmPrefix = `/root/node_modules/${config.acpAdapter}`;
2122
+ let hostPkgJsonPath = null;
2123
+ for (const root of this._softwareRoots) {
2124
+ if (root.vmPath === vmPrefix) {
2125
+ hostPkgJsonPath = join(root.hostPath, "package.json");
2126
+ break;
2127
+ }
2128
+ }
2129
+ if (!hostPkgJsonPath) {
2130
+ const nodeModulesRoot = this._nodeModulesHostRoot();
2131
+ if (nodeModulesRoot) {
2132
+ hostPkgJsonPath = join(resolvePackageDir(nodeModulesRoot, config.acpAdapter), "package.json");
2133
+ }
2134
+ }
2135
+ if (!hostPkgJsonPath) {
2136
+ throw new Error("no package source");
2137
+ }
2138
+ readFileSync(hostPkgJsonPath);
2139
+ installed = true;
2140
+ }
2141
+ catch {
2142
+ // Package not installed
2143
+ }
2144
+ return {
2145
+ id,
2146
+ acpAdapter: config.acpAdapter,
2147
+ agentPackage: config.agentPackage,
2148
+ installed,
2149
+ };
2150
+ })
2151
+ .filter((entry) => entry !== null);
2152
+ }
2153
+ _syncSessionState(session, state) {
2154
+ session.processId = state.processId;
2155
+ session.pid = state.pid ?? null;
2156
+ session.closed = state.closed;
2157
+ session.modes = toSessionModes(state.modes);
2158
+ session.configOptions = toSessionConfigOptions(state.configOptions);
2159
+ this._applySyntheticConfigOverrides(session);
2160
+ session.capabilities = toAgentCapabilities(state.agentCapabilities);
2161
+ session.agentInfo = toAgentInfo(state.agentInfo);
2162
+ }
2163
+ _applySessionUpdate(session, notification) {
2164
+ if (notification.method !== "session/update") {
2165
+ return;
2166
+ }
2167
+ const params = toRecord(notification.params);
2168
+ const update = toRecord(params.update ?? params);
2169
+ const sessionUpdate = update.sessionUpdate;
2170
+ if (sessionUpdate === "current_mode_update" &&
2171
+ typeof update.currentModeId === "string" &&
2172
+ session.modes) {
2173
+ session.modes = {
2174
+ ...session.modes,
2175
+ currentModeId: update.currentModeId,
2176
+ };
2177
+ }
2178
+ if ((sessionUpdate === "config_option_update" ||
2179
+ sessionUpdate === "config_options_update") &&
2180
+ Array.isArray(update.configOptions)) {
2181
+ session.configOptions = update.configOptions;
2182
+ }
2183
+ }
2184
+ _recordSessionNotification(session, notification) {
2185
+ this._applySessionUpdate(session, notification);
2186
+ if (shouldDispatchToSessionEventHandlers(notification)) {
2187
+ this._dispatchSessionEvent(session, notification);
2188
+ }
2189
+ if (notification.method === LEGACY_PERMISSION_METHOD ||
2190
+ notification.method === ACP_PERMISSION_METHOD) {
2191
+ const params = toRecord(notification.params);
2192
+ const permissionId = params.permissionId;
2193
+ if (typeof permissionId === "string" ||
2194
+ typeof permissionId === "number") {
2195
+ const request = {
2196
+ permissionId: String(permissionId),
2197
+ description: typeof params.description === "string"
2198
+ ? params.description
2199
+ : undefined,
2200
+ params,
2201
+ };
2202
+ for (const handler of session.permissionHandlers) {
2203
+ handler(request);
2204
+ }
2205
+ }
2206
+ }
2207
+ }
2208
+ _dispatchSessionEvent(session, notification) {
2209
+ if (session.eventHandlers.size === 0) {
2210
+ return;
2211
+ }
2212
+ for (const subscriber of [...session.eventHandlers]) {
2213
+ try {
2214
+ subscriber.handler(notification);
2215
+ }
2216
+ catch {
2217
+ // Ignore subscriber callback failures and keep event delivery moving.
2218
+ }
2219
+ }
2220
+ }
2221
+ _subscribeSessionEvents(session, handler) {
2222
+ const subscriber = {
2223
+ handler,
2224
+ };
2225
+ session.eventHandlers.add(subscriber);
2226
+ return () => {
2227
+ session.eventHandlers.delete(subscriber);
2228
+ };
2229
+ }
2230
+ _applySyntheticConfigOverrides(session) {
2231
+ if (session.configOverrides.size === 0) {
2232
+ return;
2233
+ }
2234
+ session.configOptions = session.configOptions.map((option) => {
2235
+ const override = session.configOverrides.get(option.id) ??
2236
+ (typeof option.category === "string"
2237
+ ? session.configOverrides.get(option.category)
2238
+ : undefined);
2239
+ return override === undefined
2240
+ ? option
2241
+ : { ...option, currentValue: override };
2242
+ });
2243
+ }
2244
+ _handleSidecarEvent(event) {
2245
+ if (event.payload.type === "ext") {
2246
+ this._handleAcpExtEvent(event.payload.envelope);
2247
+ return;
2248
+ }
2249
+ if (event.payload.type !== "structured") {
2250
+ return;
2251
+ }
2252
+ if (event.payload.name !== "acp.session_event") {
2253
+ return;
2254
+ }
2255
+ const sessionId = event.payload.detail.session_id;
2256
+ const session = sessionId ? this._sessions.get(sessionId) : undefined;
2257
+ if (!session) {
2258
+ return;
2259
+ }
2260
+ const notificationText = event.payload.detail.notification;
2261
+ if (typeof notificationText !== "string") {
2262
+ return;
2263
+ }
2264
+ try {
2265
+ this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(notificationText)));
2266
+ }
2267
+ catch {
2268
+ // Ignore malformed event payloads from the sidecar.
2269
+ }
2270
+ }
2271
+ _handleAcpExtEvent(envelope) {
2272
+ if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
2273
+ return;
2274
+ }
2275
+ try {
2276
+ const event = decodeAcpEvent(envelope.payload);
2277
+ switch (event.tag) {
2278
+ case "AcpSessionEvent": {
2279
+ const session = this._sessions.get(event.val.sessionId);
2280
+ if (!session) {
2281
+ return;
2282
+ }
2283
+ this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(event.val.notification)));
2284
+ return;
2285
+ }
2286
+ }
2287
+ }
2288
+ catch {
2289
+ // Ignore malformed event payloads from the sidecar.
2290
+ }
2291
+ }
2292
+ _unsupportedConfigResponse(agentType, category) {
2293
+ const message = agentType === "opencode" && category === "model"
2294
+ ? "OpenCode reports available models, but model switching must be configured before createSession() because ACP session/set_config_option is not implemented."
2295
+ : `The ${category} config option is read-only for ${agentType} sessions.`;
2296
+ return {
2297
+ jsonrpc: "2.0",
2298
+ id: null,
2299
+ error: {
2300
+ code: -32601,
2301
+ message,
2302
+ },
2303
+ };
2304
+ }
2305
+ async _sendAcpRequest(request) {
2306
+ const envelope = await this._sidecarClient.extensionRequest(this._sidecarSession, this._sidecarVm, {
2307
+ namespace: ACP_EXTENSION_NAMESPACE,
2308
+ payload: encodeAcpRequest(request),
2309
+ });
2310
+ if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
2311
+ throw new Error(`unexpected ACP Ext namespace: ${envelope.namespace}`);
2312
+ }
2313
+ const response = decodeAcpResponse(envelope.payload);
2314
+ if (response.tag === "AcpErrorResponse") {
2315
+ const error = new Error(response.val.message);
2316
+ error.code = response.val.code;
2317
+ throw error;
2318
+ }
2319
+ return response;
2320
+ }
2321
+ async _sendSessionRequest(sessionId, method, params) {
2322
+ const session = this._requireSession(sessionId);
2323
+ const response = await new Promise((resolve, reject) => {
2324
+ const resolvers = this._pendingSessionRequestResolvers.get(sessionId) ?? new Set();
2325
+ const resolver = {
2326
+ method,
2327
+ resolve: (response) => {
2328
+ resolve(response);
2329
+ },
2330
+ };
2331
+ resolvers.add(resolver);
2332
+ this._pendingSessionRequestResolvers.set(sessionId, resolvers);
2333
+ void this._sendAcpRequest({
2334
+ tag: "AcpSessionRequest",
2335
+ val: {
2336
+ sessionId,
2337
+ method,
2338
+ params: params === undefined ? null : JSON.stringify(params),
2339
+ },
2340
+ })
2341
+ .then((response) => {
2342
+ if (response.tag !== "AcpSessionRpcResponse") {
2343
+ throw new Error(`unexpected response to AcpSessionRequest: ${response.tag}`);
2344
+ }
2345
+ return toJsonRpcResponse(JSON.parse(response.val.response));
2346
+ })
2347
+ .then(resolve, reject)
2348
+ .finally(() => {
2349
+ const nextResolvers = this._pendingSessionRequestResolvers.get(sessionId);
2350
+ if (!nextResolvers) {
2351
+ return;
2352
+ }
2353
+ nextResolvers.delete(resolver);
2354
+ if (nextResolvers.size === 0) {
2355
+ this._pendingSessionRequestResolvers.delete(sessionId);
2356
+ }
2357
+ });
2358
+ });
2359
+ const liveSession = this._sessions.get(sessionId);
2360
+ if (liveSession && !isLocalCancelledPromptResponse(method, response)) {
2361
+ await this._hydrateSessionState(liveSession).catch(() => { });
2362
+ }
2363
+ if (!response.error) {
2364
+ if (method === "session/set_mode" &&
2365
+ typeof params?.modeId === "string" &&
2366
+ session.modes) {
2367
+ session.modes = {
2368
+ ...session.modes,
2369
+ currentModeId: params.modeId,
2370
+ };
2371
+ }
2372
+ if (method === "session/set_config_option" &&
2373
+ typeof params?.configId === "string" &&
2374
+ typeof params?.value === "string") {
2375
+ const nextValue = params.value;
2376
+ const updatedOption = session.configOptions.find((option) => option.id === params.configId);
2377
+ session.configOverrides.set(params.configId, nextValue);
2378
+ if (typeof updatedOption?.category === "string") {
2379
+ session.configOverrides.set(updatedOption.category, nextValue);
2380
+ }
2381
+ session.configOptions = session.configOptions.map((option) => option.id === params.configId
2382
+ ? { ...option, currentValue: nextValue }
2383
+ : option);
2384
+ }
2385
+ }
2386
+ return response;
2387
+ }
2388
+ async _setSessionConfigByCategory(sessionId, category, value) {
2389
+ const session = this._requireSession(sessionId);
2390
+ const option = session.configOptions.find((entry) => entry.category === category);
2391
+ if (option?.readOnly) {
2392
+ return this._unsupportedConfigResponse(session.agentType, category);
2393
+ }
2394
+ const response = await this._sendSessionRequest(sessionId, "session/set_config_option", {
2395
+ configId: option?.id ?? category,
2396
+ value,
2397
+ });
2398
+ return response;
2399
+ }
2400
+ _removeSession(sessionId) {
2401
+ this._sessions.delete(sessionId);
2402
+ }
2403
+ _abortPendingSessionRequests(sessionId) {
2404
+ const resolvers = this._pendingSessionRequestResolvers.get(sessionId);
2405
+ if (!resolvers) {
2406
+ return;
2407
+ }
2408
+ this._pendingSessionRequestResolvers.delete(sessionId);
2409
+ const response = {
2410
+ jsonrpc: "2.0",
2411
+ id: null,
2412
+ error: {
2413
+ code: -32_000,
2414
+ message: `Session closed: ${sessionId}`,
2415
+ },
2416
+ };
2417
+ for (const resolver of resolvers) {
2418
+ resolver.resolve(response);
2419
+ }
2420
+ }
2421
+ _cancelPendingPromptRequests(sessionId) {
2422
+ const resolvers = this._pendingSessionRequestResolvers.get(sessionId);
2423
+ if (!resolvers) {
2424
+ return false;
2425
+ }
2426
+ const response = {
2427
+ jsonrpc: "2.0",
2428
+ id: null,
2429
+ result: {
2430
+ stopReason: "cancelled",
2431
+ },
2432
+ };
2433
+ let cancelledPrompt = false;
2434
+ for (const resolver of [...resolvers]) {
2435
+ if (resolver.method !== "session/prompt") {
2436
+ continue;
2437
+ }
2438
+ resolvers.delete(resolver);
2439
+ resolver.resolve(response);
2440
+ cancelledPrompt = true;
2441
+ }
2442
+ if (resolvers.size === 0) {
2443
+ this._pendingSessionRequestResolvers.delete(sessionId);
2444
+ }
2445
+ return cancelledPrompt;
2446
+ }
2447
+ _rejectPendingPermissionReplies(sessionId) {
2448
+ const session = this._sessions.get(sessionId);
2449
+ if (!session) {
2450
+ return;
2451
+ }
2452
+ this._rejectPendingPermissionRepliesFromSession(session);
2453
+ }
2454
+ _rejectPendingPermissionRepliesFromSession(session) {
2455
+ for (const [permissionId, pendingReply,] of session.pendingPermissionReplies) {
2456
+ clearTimeout(pendingReply.timer);
2457
+ pendingReply.reject(new Error(`Session closed before permission reply: ${permissionId}`));
2458
+ }
2459
+ session.pendingPermissionReplies.clear();
2460
+ }
2461
+ async _closeSessionInternal(sessionId) {
2462
+ const closing = this._sessionClosePromises.get(sessionId);
2463
+ if (closing) {
2464
+ return closing;
2465
+ }
2466
+ if (this._closedSessionIds.has(sessionId)) {
2467
+ return;
2468
+ }
2469
+ this._abortPendingSessionRequests(sessionId);
2470
+ this._rejectPendingPermissionReplies(sessionId);
2471
+ this._requireSession(sessionId);
2472
+ this._removeSession(sessionId);
2473
+ this._closedSessionIds.add(sessionId);
2474
+ const closePromise = this._sendAcpRequest({
2475
+ tag: "AcpCloseSessionRequest",
2476
+ val: { sessionId },
2477
+ })
2478
+ .then((response) => {
2479
+ if (response.tag !== "AcpSessionClosedResponse") {
2480
+ throw new Error(`unexpected response to AcpCloseSessionRequest: ${response.tag}`);
2481
+ }
2482
+ })
2483
+ .finally(() => {
2484
+ this._sessionClosePromises.delete(sessionId);
2485
+ });
2486
+ this._sessionClosePromises.set(sessionId, closePromise);
2487
+ await closePromise;
2488
+ }
2489
+ async _hydrateSessionState(session) {
2490
+ const response = await this._sendAcpRequest({
2491
+ tag: "AcpGetSessionStateRequest",
2492
+ val: { sessionId: session.sessionId },
2493
+ });
2494
+ if (response.tag !== "AcpSessionStateResponse") {
2495
+ throw new Error(`unexpected response to AcpGetSessionStateRequest: ${response.tag}`);
2496
+ }
2497
+ const state = sidecarSessionStateFromAcp(response.val);
2498
+ this._syncSessionState(session, state);
2499
+ }
2500
+ async createSession(agentType, options) {
2501
+ const config = this._resolveAgentConfig(agentType);
2502
+ if (!config) {
2503
+ throw new Error(`Unknown agent type: ${agentType}`);
2504
+ }
2505
+ // System-prompt assembly and injection (launch args / OPENCODE_CONTEXTPATHS) are owned by
2506
+ // the sidecar at AcpCreateSessionRequest. The host only forwards additionalInstructions /
2507
+ // skipOsInstructions plus the agent's static launch args and env.
2508
+ const launchArgs = [...(config.launchArgs ?? [])];
2509
+ let launchEnv = { ...config.defaultEnv, ...options?.env };
2510
+ const sessionCwd = options?.cwd ?? "/home/user";
2511
+ const adapterEntrypoint = this._resolveAdapterBin(config.acpAdapter);
2512
+ if ((agentType === "pi" || agentType === "pi-cli") &&
2513
+ !launchEnv.PI_ACP_PI_COMMAND) {
2514
+ launchEnv = {
2515
+ ...launchEnv,
2516
+ PI_ACP_PI_COMMAND: this._resolvePackageBin(config.agentPackage, "pi"),
2517
+ };
2518
+ }
2519
+ const response = await this._sendAcpRequest({
2520
+ tag: "AcpCreateSessionRequest",
2521
+ val: {
2522
+ agentType: String(agentType),
2523
+ runtime: AcpRuntimeKind.JavaScript,
2524
+ adapterEntrypoint,
2525
+ args: launchArgs,
2526
+ env: new Map(Object.entries(launchEnv)),
2527
+ cwd: sessionCwd,
2528
+ mcpServers: JSON.stringify(options?.mcpServers ?? []),
2529
+ protocolVersion: ACP_PROTOCOL_VERSION,
2530
+ clientCapabilities: JSON.stringify(defaultAcpClientCapabilities()),
2531
+ additionalInstructions: combineInstructions(options?.additionalInstructions, this._toolReference),
2532
+ skipOsInstructions: options?.skipOsInstructions ?? false,
2533
+ },
2534
+ });
2535
+ if (response.tag !== "AcpSessionCreatedResponse") {
2536
+ throw new Error(`unexpected create_session response: ${response.tag}`);
2537
+ }
2538
+ const created = sidecarSessionCreatedFromAcp(response.val);
2539
+ // The sessionId is chosen by the (untrusted/buggy) ACP adapter or sidecar. If it collides
2540
+ // with a live session already in `_sessions`, blindly overwriting the entry would orphan the
2541
+ // first session's event/permission handlers and pending permission replies. Fail closed:
2542
+ // reject the colliding create and leave the original session intact. (A previously-closed,
2543
+ // evicted id may still be re-used; only a live, non-closed entry is a collision.)
2544
+ const existing = this._sessions.get(created.sessionId);
2545
+ if (existing !== undefined && !existing.closed) {
2546
+ throw new Error(`session id collision: ${created.sessionId}`);
2547
+ }
2548
+ const initData = {
2549
+ modes: toSessionModes(created.modes) ?? undefined,
2550
+ configOptions: toSessionConfigOptions(created.configOptions),
2551
+ capabilities: toAgentCapabilities(created.agentCapabilities),
2552
+ agentInfo: toAgentInfo(created.agentInfo) ?? undefined,
2553
+ };
2554
+ const session = sessionEntryFromInit(created.sessionId, String(agentType), initData);
2555
+ this._closedSessionIds.delete(created.sessionId);
2556
+ this._sessions.set(created.sessionId, session);
2557
+ try {
2558
+ await this._hydrateSessionState(session);
2559
+ }
2560
+ catch (error) {
2561
+ this._removeSession(created.sessionId);
2562
+ throw error;
2563
+ }
2564
+ return { sessionId: created.sessionId };
2565
+ }
2566
+ /**
2567
+ * Resume a session that exists in durable storage but is not live in this VM
2568
+ * (e.g. after a Rivet actor slept and woke with a fresh VM). Thin forwarder:
2569
+ * resolves the agent config + adapter entrypoint exactly as {@link createSession}
2570
+ * does, then forwards a single `AcpResumeSessionRequest` to the sidecar, which
2571
+ * owns the resume state machine (native `session/load` when the agent supports
2572
+ * it, else `session/new` + a transcript-continuation preamble). The returned
2573
+ * `sessionId` is the live id in this VM (equal to the requested id for native
2574
+ * loads, freshly assigned for the fallback); the caller remaps `external -> live`.
2575
+ * The new live session is registered + hydrated locally so subsequent prompts
2576
+ * route to it.
2577
+ *
2578
+ * Resume depends on a durable root; on a non-durable (default in-memory) root
2579
+ * there is no surviving store and the fallback tier always runs.
2580
+ */
2581
+ async resumeSession(sessionId, agentType, options) {
2582
+ const config = this._resolveAgentConfig(agentType);
2583
+ if (!config) {
2584
+ throw new Error(`Unknown agent type: ${agentType}`);
2585
+ }
2586
+ const adapterEntrypoint = this._resolveAdapterBin(config.acpAdapter);
2587
+ let launchEnv = { ...config.defaultEnv, ...options?.env };
2588
+ const sessionCwd = options?.cwd ?? "/home/user";
2589
+ if ((agentType === "pi" || agentType === "pi-cli") &&
2590
+ !launchEnv.PI_ACP_PI_COMMAND) {
2591
+ launchEnv = {
2592
+ ...launchEnv,
2593
+ PI_ACP_PI_COMMAND: this._resolvePackageBin(config.agentPackage, "pi"),
2594
+ };
2595
+ }
2596
+ // The resume wire request has no dedicated `adapterEntrypoint` field; carry
2597
+ // the resolved entrypoint through env under the sidecar's reserved key. The
2598
+ // sidecar reads it and strips it before launching the adapter.
2599
+ launchEnv = {
2600
+ ...launchEnv,
2601
+ [RESUME_ADAPTER_ENTRYPOINT_ENV]: adapterEntrypoint,
2602
+ };
2603
+ const response = await this._sendAcpRequest({
2604
+ tag: "AcpResumeSessionRequest",
2605
+ val: {
2606
+ sessionId,
2607
+ agentType: String(agentType),
2608
+ transcriptPath: options?.transcriptPath ?? null,
2609
+ cwd: sessionCwd,
2610
+ env: new Map(Object.entries(launchEnv)),
2611
+ },
2612
+ });
2613
+ if (response.tag !== "AcpSessionResumedResponse") {
2614
+ throw new Error(`unexpected resume_session response: ${response.tag}`);
2615
+ }
2616
+ const { sessionId: liveSessionId, mode } = response.val;
2617
+ // Register + hydrate the live session so subsequent prompts route to it.
2618
+ const session = sessionEntryFromInit(liveSessionId, String(agentType), {});
2619
+ this._closedSessionIds.delete(liveSessionId);
2620
+ this._sessions.set(liveSessionId, session);
2621
+ try {
2622
+ await this._hydrateSessionState(session);
2623
+ }
2624
+ catch (error) {
2625
+ this._removeSession(liveSessionId);
2626
+ throw error;
2627
+ }
2628
+ return { sessionId: liveSessionId, mode };
2629
+ }
2630
+ /**
2631
+ * Resolve the VM bin entry point of an ACP adapter package.
2632
+ * Reads from the host filesystem since kernel.readFile() resolves through
2633
+ * mounts; adapter package.json is read directly off the host dir backing
2634
+ * the relevant mount.
2635
+ */
2636
+ _resolveAdapterBin(adapterPackage) {
2637
+ return this._resolvePackageBin(adapterPackage);
2638
+ }
2639
+ /**
2640
+ * Parent of the host directory backing the `/root/node_modules` mount, if
2641
+ * the caller supplied one (e.g. via `nodeModulesMount(...)`). Used as the
2642
+ * `resolvePackageDir` start dir for adapter/agent package resolution when no
2643
+ * software root matches. `nodeModulesMount` mounts `<dir>/node_modules`, so
2644
+ * the start dir is `<dir>` (one level above the node_modules tree).
2645
+ */
2646
+ _nodeModulesHostRoot() {
2647
+ for (const mount of this._hostMounts) {
2648
+ if (mount.vmPath === "/root/node_modules") {
2649
+ return dirname(mount.hostPath);
2650
+ }
2651
+ }
2652
+ return null;
2653
+ }
2654
+ _resolvePackageBin(packageName, binName) {
2655
+ const vmPrefix = `/root/node_modules/${packageName}`;
2656
+ let hostPkgJsonPath = null;
2657
+ for (const root of this._softwareRoots) {
2658
+ if (root.vmPath === vmPrefix) {
2659
+ hostPkgJsonPath = join(root.hostPath, "package.json");
2660
+ break;
2661
+ }
2662
+ }
2663
+ // Fall back to the host dir behind the `/root/node_modules` mount.
2664
+ if (!hostPkgJsonPath) {
2665
+ const nodeModulesRoot = this._nodeModulesHostRoot();
2666
+ if (!nodeModulesRoot) {
2667
+ throw new Error(`Cannot resolve package "${packageName}": no software root provides it and ` +
2668
+ `no /root/node_modules mount was supplied. Pass ` +
2669
+ `mounts: [nodeModulesMount("<host>/node_modules")] to AgentOs.create().`);
2670
+ }
2671
+ hostPkgJsonPath = join(resolvePackageDir(nodeModulesRoot, packageName), "package.json");
2672
+ }
2673
+ const pkg = JSON.parse(readFileSync(hostPkgJsonPath, "utf-8"));
2674
+ let binEntry;
2675
+ if (typeof pkg.bin === "string") {
2676
+ binEntry = pkg.bin;
2677
+ }
2678
+ else if (typeof pkg.bin === "object" && pkg.bin !== null) {
2679
+ binEntry =
2680
+ (binName ? pkg.bin[binName] : undefined) ??
2681
+ pkg.bin[packageName] ??
2682
+ Object.values(pkg.bin)[0];
2683
+ }
2684
+ if (!binEntry) {
2685
+ throw new Error(`No bin entry found in ${packageName}/package.json`);
2686
+ }
2687
+ return `${vmPrefix}/${binEntry}`;
2688
+ }
2689
+ _installSidecarRequestHandler() {
2690
+ const context = {
2691
+ toolKits: this._toolKits,
2692
+ toolMap: buildToolMap(this._toolKits),
2693
+ permissions: this._permissions,
2694
+ readFile: (path) => this.readFile(path),
2695
+ };
2696
+ this._sidecarClient.setSidecarRequestHandler((request) => {
2697
+ switch (request.payload.type) {
2698
+ case "host_callback":
2699
+ return handleHostCallback(request, context);
2700
+ case "js_bridge_call":
2701
+ return Promise.resolve({
2702
+ type: "js_bridge_result",
2703
+ call_id: request.payload.call_id,
2704
+ error: `unsupported sidecar request type: ${request.payload.type}`,
2705
+ });
2706
+ case "ext":
2707
+ return this._handleAcpExtSidecarRequest(request.payload.envelope);
2708
+ }
2709
+ });
2710
+ }
2711
+ async _handleAcpExtSidecarRequest(envelope) {
2712
+ if (envelope.namespace !== ACP_EXTENSION_NAMESPACE) {
2713
+ return {
2714
+ type: "ext_result",
2715
+ envelope: {
2716
+ namespace: envelope.namespace,
2717
+ payload: Buffer.from("unknown extension namespace", "utf8"),
2718
+ },
2719
+ };
2720
+ }
2721
+ const callback = decodeAcpCallback(envelope.payload);
2722
+ switch (callback.tag) {
2723
+ case "AcpPermissionCallback": {
2724
+ const reply = await this._handleAcpPermissionCallback(callback.val.sessionId, callback.val.permissionId, {
2725
+ ...toRecord(JSON.parse(callback.val.params)),
2726
+ _acpMethod: ACP_PERMISSION_METHOD,
2727
+ });
2728
+ return {
2729
+ type: "ext_result",
2730
+ envelope: {
2731
+ namespace: ACP_EXTENSION_NAMESPACE,
2732
+ payload: encodeAcpCallbackResponse({
2733
+ tag: "AcpPermissionCallbackResponse",
2734
+ val: {
2735
+ permissionId: callback.val.permissionId,
2736
+ reply,
2737
+ },
2738
+ }),
2739
+ },
2740
+ };
2741
+ }
2742
+ case "AcpHostRequestCallback": {
2743
+ const response = await this._dispatchAcpSidecarRequest(toJsonRpcRequest(JSON.parse(callback.val.request)));
2744
+ return {
2745
+ type: "ext_result",
2746
+ envelope: {
2747
+ namespace: ACP_EXTENSION_NAMESPACE,
2748
+ payload: encodeAcpCallbackResponse({
2749
+ tag: "AcpHostRequestCallbackResponse",
2750
+ val: {
2751
+ response: JSON.stringify(response),
2752
+ },
2753
+ }),
2754
+ },
2755
+ };
2756
+ }
2757
+ }
2758
+ }
2759
+ async _dispatchAcpSidecarRequest(request) {
2760
+ try {
2761
+ const result = await this._handleSupportedAcpSidecarRequest(request);
2762
+ return {
2763
+ jsonrpc: "2.0",
2764
+ id: request.id,
2765
+ result,
2766
+ };
2767
+ }
2768
+ catch (error) {
2769
+ if (error instanceof AcpDispatchError) {
2770
+ return {
2771
+ jsonrpc: "2.0",
2772
+ id: request.id,
2773
+ error: {
2774
+ code: error.code,
2775
+ message: error.message,
2776
+ ...(error.data ? { data: error.data } : {}),
2777
+ },
2778
+ };
2779
+ }
2780
+ return {
2781
+ jsonrpc: "2.0",
2782
+ id: request.id,
2783
+ error: {
2784
+ code: -32603,
2785
+ message: error instanceof Error ? error.message : String(error),
2786
+ },
2787
+ };
2788
+ }
2789
+ }
2790
+ async _handleSupportedAcpSidecarRequest(request) {
2791
+ const params = this._acpParams(request);
2792
+ switch (request.method) {
2793
+ case ACP_PERMISSION_METHOD:
2794
+ return this._handleAcpPermissionRequest(request, params);
2795
+ case "fs/read":
2796
+ case "fs/read_text_file":
2797
+ return this._handleAcpReadFile(params);
2798
+ case "fs/write":
2799
+ case "fs/write_text_file":
2800
+ return this._handleAcpWriteFile(params);
2801
+ case "fs/readDir":
2802
+ case "fs/read_dir":
2803
+ return this._handleAcpReadDir(params);
2804
+ case "terminal/create":
2805
+ return this._handleAcpCreateTerminal(params);
2806
+ case "terminal/write":
2807
+ return this._handleAcpWriteTerminal(params);
2808
+ case "terminal/output":
2809
+ case "terminal/read":
2810
+ return this._handleAcpReadTerminal(params);
2811
+ case "terminal/wait_for_exit":
2812
+ case "terminal/waitForExit":
2813
+ return this._handleAcpWaitForTerminalExit(params);
2814
+ case "terminal/kill":
2815
+ return this._handleAcpKillTerminal(params);
2816
+ case "terminal/release":
2817
+ case "terminal/close":
2818
+ return this._handleAcpReleaseTerminal(params);
2819
+ case "terminal/resize":
2820
+ return this._handleAcpResizeTerminal(params);
2821
+ default:
2822
+ throw new AcpDispatchError(-32601, `Method not found: ${request.method}`, {
2823
+ method: request.method,
2824
+ });
2825
+ }
2826
+ }
2827
+ _normalizeAcpPermissionOptionId(options, reply) {
2828
+ const optionTargets = reply === "always"
2829
+ ? {
2830
+ optionIds: new Set(["always", "allow_always"]),
2831
+ kinds: new Set(["allow_always"]),
2832
+ }
2833
+ : reply === "once"
2834
+ ? {
2835
+ optionIds: new Set(["once", "allow_once"]),
2836
+ kinds: new Set(["allow_once"]),
2837
+ }
2838
+ : {
2839
+ optionIds: new Set(["reject", "reject_once"]),
2840
+ kinds: new Set(["reject_once"]),
2841
+ };
2842
+ const matched = options?.find((option) => {
2843
+ const optionId = typeof option.optionId === "string" ? option.optionId : undefined;
2844
+ const kind = typeof option.kind === "string" ? option.kind : undefined;
2845
+ return ((optionId !== undefined && optionTargets.optionIds.has(optionId)) ||
2846
+ (kind !== undefined && optionTargets.kinds.has(kind)));
2847
+ });
2848
+ if (matched && typeof matched.optionId === "string") {
2849
+ return matched.optionId;
2850
+ }
2851
+ if (reply === "always") {
2852
+ return "allow_always";
2853
+ }
2854
+ if (reply === "once") {
2855
+ return "allow_once";
2856
+ }
2857
+ return "reject_once";
2858
+ }
2859
+ _buildAcpPermissionResult(reply, params) {
2860
+ const options = Array.isArray(params.options)
2861
+ ? params.options.filter((option) => typeof option === "object" && option !== null)
2862
+ : undefined;
2863
+ const optionId = this._normalizeAcpPermissionOptionId(options, reply);
2864
+ return {
2865
+ outcome: optionId
2866
+ ? {
2867
+ outcome: "selected",
2868
+ optionId,
2869
+ }
2870
+ : {
2871
+ outcome: "cancelled",
2872
+ },
2873
+ };
2874
+ }
2875
+ async _handleAcpPermissionRequest(request, params) {
2876
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
2877
+ if (!sessionId) {
2878
+ throw new AcpDispatchError(-32602, `${ACP_PERMISSION_METHOD} requires a sessionId`);
2879
+ }
2880
+ const session = this._sessions.get(sessionId);
2881
+ if (!session) {
2882
+ throw new AcpDispatchError(-32602, `Session not found: ${sessionId}`);
2883
+ }
2884
+ const permissionId = String(request.id);
2885
+ const permissionParams = {
2886
+ ...params,
2887
+ permissionId,
2888
+ _acpMethod: request.method,
2889
+ };
2890
+ if (session.permissionHandlers.size === 0) {
2891
+ return this._buildAcpPermissionResult("reject", permissionParams);
2892
+ }
2893
+ const reply = await new Promise((resolve, reject) => {
2894
+ const timer = setTimeout(() => {
2895
+ session.pendingPermissionReplies.delete(permissionId);
2896
+ reject(new Error(`Timed out waiting for permission reply: ${permissionId}`));
2897
+ }, 120_000);
2898
+ session.pendingPermissionReplies.set(permissionId, {
2899
+ resolve,
2900
+ reject,
2901
+ timer,
2902
+ });
2903
+ const permissionRequest = {
2904
+ permissionId,
2905
+ description: typeof permissionParams["description"] === "string"
2906
+ ? permissionParams["description"]
2907
+ : undefined,
2908
+ params: permissionParams,
2909
+ };
2910
+ for (const handler of session.permissionHandlers) {
2911
+ handler(permissionRequest);
2912
+ }
2913
+ });
2914
+ return this._buildAcpPermissionResult(reply, permissionParams);
2915
+ }
2916
+ _acpParams(request) {
2917
+ if (!request.params) {
2918
+ return {};
2919
+ }
2920
+ if (typeof request.params !== "object" ||
2921
+ request.params === null ||
2922
+ Array.isArray(request.params)) {
2923
+ throw new AcpDispatchError(-32602, `${request.method} requires object params`);
2924
+ }
2925
+ return request.params;
2926
+ }
2927
+ _requireAcpStringParam(params, name, method) {
2928
+ const value = params[name];
2929
+ if (typeof value !== "string") {
2930
+ throw new AcpDispatchError(-32602, `${method} requires a string ${name}`);
2931
+ }
2932
+ return value;
2933
+ }
2934
+ _optionalAcpStringParam(params, name, method) {
2935
+ const value = params[name];
2936
+ if (value === undefined || value === null) {
2937
+ return undefined;
2938
+ }
2939
+ if (typeof value !== "string") {
2940
+ throw new AcpDispatchError(-32602, `${method} requires ${name} to be a string when provided`);
2941
+ }
2942
+ return value;
2943
+ }
2944
+ _optionalAcpNumberParam(params, name, method) {
2945
+ const value = params[name];
2946
+ if (value === undefined || value === null) {
2947
+ return undefined;
2948
+ }
2949
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2950
+ throw new AcpDispatchError(-32602, `${method} requires ${name} to be a number when provided`);
2951
+ }
2952
+ return value;
2953
+ }
2954
+ _optionalAcpStringArrayParam(params, name, method) {
2955
+ const value = params[name];
2956
+ if (value === undefined || value === null) {
2957
+ return undefined;
2958
+ }
2959
+ if (!Array.isArray(value) ||
2960
+ value.some((entry) => typeof entry !== "string")) {
2961
+ throw new AcpDispatchError(-32602, `${method} requires ${name} to be an array of strings when provided`);
2962
+ }
2963
+ return [...value];
2964
+ }
2965
+ _optionalAcpEnvParam(params, name, method) {
2966
+ const value = params[name];
2967
+ if (value === undefined || value === null) {
2968
+ return undefined;
2969
+ }
2970
+ if (Array.isArray(value)) {
2971
+ const env = {};
2972
+ for (const entry of value) {
2973
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
2974
+ throw new AcpDispatchError(-32602, `${method} requires ${name} entries to be { name, value } objects`);
2975
+ }
2976
+ const record = entry;
2977
+ if (typeof record.name !== "string" ||
2978
+ typeof record.value !== "string") {
2979
+ throw new AcpDispatchError(-32602, `${method} requires ${name} entries to be { name, value } objects`);
2980
+ }
2981
+ env[record.name] = record.value;
2982
+ }
2983
+ return env;
2984
+ }
2985
+ if (typeof value !== "object") {
2986
+ throw new AcpDispatchError(-32602, `${method} requires ${name} to be an object or name/value array`);
2987
+ }
2988
+ const env = {};
2989
+ for (const [key, entryValue] of Object.entries(value)) {
2990
+ if (typeof entryValue !== "string") {
2991
+ throw new AcpDispatchError(-32602, `${method} requires ${name} values to be strings`);
2992
+ }
2993
+ env[key] = entryValue;
2994
+ }
2995
+ return env;
2996
+ }
2997
+ _requireAcpTerminal(params, method) {
2998
+ const terminalId = this._requireAcpStringParam(params, "terminalId", method);
2999
+ const terminal = this._acpTerminals.get(terminalId);
3000
+ if (!terminal) {
3001
+ throw new AcpDispatchError(-32602, `ACP terminal not found: ${terminalId}`);
3002
+ }
3003
+ return terminal;
3004
+ }
3005
+ _appendAcpTerminalOutput(terminal, data) {
3006
+ const chunk = Buffer.from(data).toString("utf8");
3007
+ if (!chunk) {
3008
+ return;
3009
+ }
3010
+ terminal.output += chunk;
3011
+ if (Number.isFinite(terminal.outputByteLimit) &&
3012
+ terminal.outputByteLimit >= 0 &&
3013
+ terminal.output.length > terminal.outputByteLimit) {
3014
+ terminal.output = terminal.output.slice(terminal.output.length - terminal.outputByteLimit);
3015
+ terminal.truncated = true;
3016
+ }
3017
+ }
3018
+ async _handleAcpReadFile(params) {
3019
+ const method = "fs/read";
3020
+ const path = this._requireAcpStringParam(params, "path", method);
3021
+ const line = this._optionalAcpNumberParam(params, "line", method);
3022
+ const limit = this._optionalAcpNumberParam(params, "limit", method);
3023
+ const encoding = this._optionalAcpStringParam(params, "encoding", method);
3024
+ const bytes = await this.readFile(path);
3025
+ if (encoding === "base64") {
3026
+ return { content: Buffer.from(bytes).toString("base64") };
3027
+ }
3028
+ const text = new TextDecoder().decode(bytes);
3029
+ if (line === undefined && limit === undefined) {
3030
+ return { content: text };
3031
+ }
3032
+ const startLine = Math.max(1, Math.trunc(line ?? 1));
3033
+ const lineLimit = limit === undefined
3034
+ ? Number.POSITIVE_INFINITY
3035
+ : Math.max(0, Math.trunc(limit));
3036
+ return {
3037
+ content: text
3038
+ .split("\n")
3039
+ .slice(startLine - 1, startLine - 1 + lineLimit)
3040
+ .join("\n"),
3041
+ };
3042
+ }
3043
+ async _handleAcpWriteFile(params) {
3044
+ const method = "fs/write";
3045
+ const path = this._requireAcpStringParam(params, "path", method);
3046
+ const content = this._requireAcpStringParam(params, "content", method);
3047
+ const encoding = this._optionalAcpStringParam(params, "encoding", method);
3048
+ await this.writeFile(path, encoding === "base64" ? Buffer.from(content, "base64") : content);
3049
+ return null;
3050
+ }
3051
+ async _handleAcpReadDir(params) {
3052
+ const method = "fs/readDir";
3053
+ const path = this._requireAcpStringParam(params, "path", method);
3054
+ const entries = await this._vfs().readDirWithTypes(path);
3055
+ return {
3056
+ entries: entries
3057
+ .filter((entry) => entry.name !== "." && entry.name !== "..")
3058
+ .map((entry) => ({
3059
+ name: entry.name,
3060
+ path: path === "/" ? `/${entry.name}` : `${path}/${entry.name}`,
3061
+ type: entry.isSymbolicLink
3062
+ ? "symlink"
3063
+ : entry.isDirectory
3064
+ ? "directory"
3065
+ : "file",
3066
+ })),
3067
+ };
3068
+ }
3069
+ _handleAcpCreateTerminal(params) {
3070
+ const method = "terminal/create";
3071
+ const command = this._requireAcpStringParam(params, "command", method);
3072
+ const args = this._optionalAcpStringArrayParam(params, "args", method);
3073
+ const env = this._optionalAcpEnvParam(params, "env", method);
3074
+ const cwd = this._optionalAcpStringParam(params, "cwd", method);
3075
+ const cols = this._optionalAcpNumberParam(params, "cols", method);
3076
+ const rows = this._optionalAcpNumberParam(params, "rows", method);
3077
+ const outputByteLimit = Math.max(0, Math.trunc(this._optionalAcpNumberParam(params, "outputByteLimit", method) ??
3078
+ 1_048_576));
3079
+ const terminalId = `acp-terminal-${++this._acpTerminalCounter}`;
3080
+ const terminal = {
3081
+ handle: this.#kernel.openShell({
3082
+ command,
3083
+ ...(args ? { args } : {}),
3084
+ ...(env ? { env } : {}),
3085
+ ...(cwd ? { cwd } : {}),
3086
+ ...(cols !== undefined ? { cols: Math.trunc(cols) } : {}),
3087
+ ...(rows !== undefined ? { rows: Math.trunc(rows) } : {}),
3088
+ onStderr: (data) => {
3089
+ this._appendAcpTerminalOutput(terminal, data);
3090
+ },
3091
+ }),
3092
+ output: "",
3093
+ truncated: false,
3094
+ outputByteLimit,
3095
+ exitCode: null,
3096
+ waitPromise: Promise.resolve(0),
3097
+ };
3098
+ terminal.handle.onData = (data) => {
3099
+ this._appendAcpTerminalOutput(terminal, data);
3100
+ };
3101
+ terminal.waitPromise = terminal.handle.wait().then((exitCode) => {
3102
+ terminal.exitCode = exitCode;
3103
+ return exitCode;
3104
+ });
3105
+ this._acpTerminals.set(terminalId, terminal);
3106
+ return { terminalId };
3107
+ }
3108
+ _handleAcpWriteTerminal(params) {
3109
+ const method = "terminal/write";
3110
+ const terminal = this._requireAcpTerminal(params, method);
3111
+ const data = this._requireAcpStringParam(params, "data", method);
3112
+ const encoding = this._optionalAcpStringParam(params, "encoding", method);
3113
+ terminal.handle.write(encoding === "base64" ? Buffer.from(data, "base64") : data);
3114
+ return null;
3115
+ }
3116
+ _handleAcpReadTerminal(params) {
3117
+ const terminal = this._requireAcpTerminal(params, "terminal/output");
3118
+ return {
3119
+ output: terminal.output,
3120
+ truncated: terminal.truncated,
3121
+ ...(terminal.exitCode !== null
3122
+ ? {
3123
+ exitStatus: {
3124
+ exitCode: terminal.exitCode,
3125
+ signal: null,
3126
+ },
3127
+ }
3128
+ : {}),
3129
+ };
3130
+ }
3131
+ async _handleAcpWaitForTerminalExit(params) {
3132
+ const terminal = this._requireAcpTerminal(params, "terminal/wait_for_exit");
3133
+ const exitCode = await terminal.waitPromise;
3134
+ return { exitCode, signal: null };
3135
+ }
3136
+ _handleAcpKillTerminal(params) {
3137
+ const method = "terminal/kill";
3138
+ const terminal = this._requireAcpTerminal(params, method);
3139
+ const signal = this._optionalAcpNumberParam(params, "signal", method) ?? 15;
3140
+ terminal.handle.kill(Math.trunc(signal));
3141
+ return null;
3142
+ }
3143
+ _handleAcpReleaseTerminal(params) {
3144
+ const method = "terminal/release";
3145
+ const terminalId = this._requireAcpStringParam(params, "terminalId", method);
3146
+ const terminal = this._acpTerminals.get(terminalId);
3147
+ if (!terminal) {
3148
+ throw new AcpDispatchError(-32602, `ACP terminal not found: ${terminalId}`);
3149
+ }
3150
+ if (terminal.exitCode === null) {
3151
+ terminal.handle.kill();
3152
+ }
3153
+ this._acpTerminals.delete(terminalId);
3154
+ return null;
3155
+ }
3156
+ _handleAcpResizeTerminal(params) {
3157
+ const method = "terminal/resize";
3158
+ const terminal = this._requireAcpTerminal(params, method);
3159
+ const cols = this._optionalAcpNumberParam(params, "cols", method);
3160
+ const rows = this._optionalAcpNumberParam(params, "rows", method);
3161
+ if (cols === undefined || rows === undefined) {
3162
+ throw new AcpDispatchError(-32602, `${method} requires numeric cols and rows`);
3163
+ }
3164
+ terminal.handle.resize(Math.trunc(cols), Math.trunc(rows));
3165
+ return null;
3166
+ }
3167
+ async _handleAcpPermissionCallback(sessionId, permissionId, params) {
3168
+ const session = this._sessions.get(sessionId);
3169
+ if (!session) {
3170
+ return "reject";
3171
+ }
3172
+ if (session.permissionHandlers.size === 0) {
3173
+ return "reject";
3174
+ }
3175
+ try {
3176
+ return await new Promise((resolve, reject) => {
3177
+ const timer = setTimeout(() => {
3178
+ session.pendingPermissionReplies.delete(permissionId);
3179
+ reject(new Error(`Timed out waiting for permission reply: ${permissionId}`));
3180
+ }, 120_000);
3181
+ session.pendingPermissionReplies.set(permissionId, {
3182
+ resolve,
3183
+ reject,
3184
+ timer,
3185
+ });
3186
+ const permissionRequest = {
3187
+ permissionId,
3188
+ description: typeof params.description === "string"
3189
+ ? params.description
3190
+ : undefined,
3191
+ params,
3192
+ };
3193
+ for (const handler of session.permissionHandlers) {
3194
+ handler(permissionRequest);
3195
+ }
3196
+ });
3197
+ }
3198
+ catch {
3199
+ return "reject";
3200
+ }
3201
+ }
3202
+ /**
3203
+ * Resolve an agent config by ID. Package-provided configs take
3204
+ * precedence over the hardcoded AGENT_CONFIGS.
3205
+ */
3206
+ _resolveAgentConfig(agentType) {
3207
+ return (this._softwareAgentConfigs.get(agentType) ??
3208
+ AGENT_CONFIGS[agentType]);
3209
+ }
3210
+ /**
3211
+ * Gracefully destroy a session: cancel any pending work, close the client,
3212
+ * and remove from tracking. Unlike close() which is abrupt, this attempts
3213
+ * a graceful shutdown sequence.
3214
+ */
3215
+ async destroySession(sessionId) {
3216
+ this._requireSession(sessionId);
3217
+ try {
3218
+ await this.cancelSession(sessionId);
3219
+ }
3220
+ catch {
3221
+ // Ignore cancellation failures during teardown.
3222
+ }
3223
+ await this._closeSessionInternal(sessionId);
3224
+ }
3225
+ // ── Flat session API (ID-based) ───────────────────────────────
3226
+ async prompt(sessionId, text) {
3227
+ const session = this._requireSession(sessionId);
3228
+ let agentText = "";
3229
+ const handler = (event) => {
3230
+ const params = toRecord(event.params);
3231
+ const update = toRecord(params.update);
3232
+ if (update?.sessionUpdate === "agent_message_chunk") {
3233
+ const content = toRecord(update.content);
3234
+ if (typeof content.text === "string") {
3235
+ agentText += content.text;
3236
+ }
3237
+ }
3238
+ };
3239
+ const unsubscribe = this._subscribeSessionEvents(session, handler);
3240
+ try {
3241
+ const response = await this._sendSessionRequest(sessionId, "session/prompt", {
3242
+ prompt: [{ type: "text", text }],
3243
+ });
3244
+ return { response, text: agentText };
3245
+ }
3246
+ finally {
3247
+ unsubscribe();
3248
+ }
3249
+ }
3250
+ /** Cancel ongoing agent work for a session. */
3251
+ async cancelSession(sessionId) {
3252
+ this._requireSession(sessionId);
3253
+ const cancelledPendingPrompt = this._cancelPendingPromptRequests(sessionId);
3254
+ if (cancelledPendingPrompt) {
3255
+ // Session control requests share the same framed sidecar transport as an
3256
+ // in-flight prompt request. If the adapter is blocked in prompt I/O, a
3257
+ // synchronous cancel RPC can wedge behind that prompt until the transport
3258
+ // timeout fires. Resolve the local prompt immediately, then let the sidecar
3259
+ // cancellation continue in the background as best effort.
3260
+ void this._sendSessionRequest(sessionId, "session/cancel").catch(() => { });
3261
+ return {
3262
+ jsonrpc: "2.0",
3263
+ id: null,
3264
+ result: {
3265
+ cancelled: true,
3266
+ requested: true,
3267
+ via: "prompt-fallback",
3268
+ },
3269
+ };
3270
+ }
3271
+ const response = await this._sendSessionRequest(sessionId, "session/cancel");
3272
+ if (response.error?.code === -32601) {
3273
+ return {
3274
+ jsonrpc: "2.0",
3275
+ id: null,
3276
+ result: {
3277
+ cancelled: false,
3278
+ requested: true,
3279
+ via: "unsupported",
3280
+ },
3281
+ };
3282
+ }
3283
+ return response;
3284
+ }
3285
+ closeSession(sessionId) {
3286
+ if (!this._sessions.has(sessionId) &&
3287
+ !this._closedSessionIds.has(sessionId) &&
3288
+ !this._sessionClosePromises.has(sessionId)) {
3289
+ throw new Error(`Session not found: ${sessionId}`);
3290
+ }
3291
+ const closePromise = this._closeSessionInternal(sessionId);
3292
+ // `closeSession()` is intentionally fire-and-forget; suppress unhandled
3293
+ // rejections here and let tracked close promises surface errors to any
3294
+ // internal/test callers awaiting `_sessionClosePromises`.
3295
+ void closePromise.catch(() => { });
3296
+ }
3297
+ async respondPermission(sessionId, permissionId, reply) {
3298
+ const session = this._requireSession(sessionId);
3299
+ const pendingReply = session.pendingPermissionReplies.get(permissionId);
3300
+ if (pendingReply) {
3301
+ session.pendingPermissionReplies.delete(permissionId);
3302
+ clearTimeout(pendingReply.timer);
3303
+ pendingReply.resolve(reply);
3304
+ return {
3305
+ jsonrpc: "2.0",
3306
+ id: null,
3307
+ result: {
3308
+ permissionId,
3309
+ reply,
3310
+ via: "sidecar-request",
3311
+ },
3312
+ };
3313
+ }
3314
+ return this._sendSessionRequest(sessionId, LEGACY_PERMISSION_METHOD, {
3315
+ permissionId,
3316
+ reply,
3317
+ });
3318
+ }
3319
+ async setSessionMode(sessionId, modeId) {
3320
+ return this._sendSessionRequest(sessionId, "session/set_mode", {
3321
+ modeId,
3322
+ });
3323
+ }
3324
+ getSessionModes(sessionId) {
3325
+ return this._requireSession(sessionId).modes;
3326
+ }
3327
+ async setSessionModel(sessionId, model) {
3328
+ return this._setSessionConfigByCategory(sessionId, "model", model);
3329
+ }
3330
+ async setSessionThoughtLevel(sessionId, level) {
3331
+ return this._setSessionConfigByCategory(sessionId, "thought_level", level);
3332
+ }
3333
+ getSessionConfigOptions(sessionId) {
3334
+ return [...this._requireSession(sessionId).configOptions];
3335
+ }
3336
+ getSessionCapabilities(sessionId) {
3337
+ const caps = this._requireSession(sessionId).capabilities;
3338
+ return Object.keys(caps).length > 0 ? caps : null;
3339
+ }
3340
+ getSessionAgentInfo(sessionId) {
3341
+ return this._requireSession(sessionId).agentInfo;
3342
+ }
3343
+ async rawSessionSend(sessionId, method, params) {
3344
+ return this._sendSessionRequest(sessionId, method, params);
3345
+ }
3346
+ async rawSend(sessionId, method, params) {
3347
+ return this.rawSessionSend(sessionId, method, params);
3348
+ }
3349
+ onSessionEvent(sessionId, handler) {
3350
+ const session = this._requireSession(sessionId);
3351
+ return this._subscribeSessionEvents(session, handler);
3352
+ }
3353
+ onPermissionRequest(sessionId, handler) {
3354
+ const session = this._requireSession(sessionId);
3355
+ session.permissionHandlers.add(handler);
3356
+ return () => {
3357
+ session.permissionHandlers.delete(handler);
3358
+ };
3359
+ }
3360
+ // ── Cron ────────────────────────────────────────────────────
3361
+ /** Schedule a cron job. Returns a handle with the job ID and a cancel method. */
3362
+ scheduleCron(options) {
3363
+ return this._cronManager.schedule(options);
3364
+ }
3365
+ /** List all registered cron jobs. */
3366
+ listCronJobs() {
3367
+ return this._cronManager.list();
3368
+ }
3369
+ /** Cancel a cron job by ID. */
3370
+ cancelCronJob(id) {
3371
+ this._cronManager.cancel(id);
3372
+ }
3373
+ /** Subscribe to cron lifecycle events (fire, complete, error). */
3374
+ onCronEvent(handler) {
3375
+ this._cronManager.onEvent(handler);
3376
+ }
3377
+ async dispose() {
3378
+ this._cronManager.dispose();
3379
+ for (const sessionId of [...this._sessions.keys()]) {
3380
+ await this._closeSessionInternal(sessionId).catch(() => { });
3381
+ }
3382
+ for (const [id, entry] of this._shells) {
3383
+ entry.handle.kill();
3384
+ }
3385
+ const shellExitPromises = [...this._pendingShellExitPromises];
3386
+ this._shells.clear();
3387
+ const terminalExitPromises = [];
3388
+ for (const terminal of this._acpTerminals.values()) {
3389
+ terminal.handle.kill();
3390
+ terminalExitPromises.push(terminal.waitPromise.then(() => undefined, () => undefined));
3391
+ }
3392
+ this._acpTerminals.clear();
3393
+ await waitForTrackedExitPromises([...shellExitPromises, ...terminalExitPromises], SHELL_DISPOSE_TIMEOUT_MS);
3394
+ this._disposeSidecarEventListener();
3395
+ const sidecarLease = this._sidecarLease;
3396
+ this._sidecarLease = null;
3397
+ if (sidecarLease) {
3398
+ return sidecarLease.dispose();
3399
+ }
3400
+ return this.#kernel.dispose();
3401
+ }
3402
+ }
3403
+ const agentOsRuntimeAdmins = new WeakMap();
3404
+ export function getAgentOsRuntimeAdmin(vm) {
3405
+ const admin = agentOsRuntimeAdmins.get(vm);
3406
+ if (!admin) {
3407
+ throw new Error("Agent OS runtime admin is not available for this VM");
3408
+ }
3409
+ return admin;
3410
+ }
3411
+ export function getAgentOsKernel(vm) {
3412
+ return getAgentOsRuntimeAdmin(vm).kernel;
3413
+ }
3414
+ function resolveAgentOsSidecar(config) {
3415
+ if (!config || config.kind === "shared") {
3416
+ return getSharedAgentOsSidecarInternal(config?.kind === "shared" ? { pool: config.pool } : undefined);
3417
+ }
3418
+ return config.handle;
3419
+ }
3420
+ const sidecarStates = new WeakMap();
3421
+ const sharedSidecars = new Map();
3422
+ export class AgentOsSidecar {
3423
+ constructor(sidecarId, placement, sharedPool) {
3424
+ sidecarStates.set(this, {
3425
+ description: {
3426
+ sidecarId,
3427
+ placement: cloneSidecarPlacement(placement),
3428
+ state: "ready",
3429
+ activeVmCount: 0,
3430
+ },
3431
+ activeLeases: new Set(),
3432
+ sharedPool,
3433
+ });
3434
+ }
3435
+ describe() {
3436
+ const state = getSidecarState(this);
3437
+ return cloneSidecarDescription(state.description);
3438
+ }
3439
+ async dispose() {
3440
+ const state = getSidecarState(this);
3441
+ if (state.description.state === "disposed") {
3442
+ return;
3443
+ }
3444
+ state.description.state = "disposing";
3445
+ const errors = [];
3446
+ for (const lease of [...state.activeLeases]) {
3447
+ try {
3448
+ await lease.dispose();
3449
+ }
3450
+ catch (error) {
3451
+ errors.push(error instanceof Error ? error : new Error(String(error)));
3452
+ }
3453
+ }
3454
+ state.activeLeases.clear();
3455
+ state.description.activeVmCount = 0;
3456
+ state.description.state = "disposed";
3457
+ if (state.sharedPool && sharedSidecars.get(state.sharedPool) === this) {
3458
+ sharedSidecars.delete(state.sharedPool);
3459
+ }
3460
+ if (errors.length > 0) {
3461
+ throw new Error(errors.map((error) => error.message).join("; "));
3462
+ }
3463
+ }
3464
+ }
3465
+ function createAgentOsSidecarInternal(options = {}) {
3466
+ const sidecarId = options.sidecarId ?? `agentos-sidecar-${randomUUID()}`;
3467
+ return new AgentOsSidecar(sidecarId, {
3468
+ kind: "explicit",
3469
+ sidecarId,
3470
+ });
3471
+ }
3472
+ /**
3473
+ * Test-only escape hatch: dispose every cached shared sidecar so vitest
3474
+ * workers can exit cleanly. The shared sidecar is normally process-global and
3475
+ * keeps its native subprocess alive across `AgentOs.create()` calls; without
3476
+ * this hook the vitest worker can hold open piped stdio handles after the
3477
+ * test suite finishes and stall `pnpm test` indefinitely.
3478
+ */
3479
+ export async function __disposeAllSharedSidecarsForTesting() {
3480
+ const sidecars = Array.from(sharedSidecars.values());
3481
+ sharedSidecars.clear();
3482
+ const errors = [];
3483
+ for (const sidecar of sidecars) {
3484
+ try {
3485
+ await sidecar.dispose();
3486
+ }
3487
+ catch (error) {
3488
+ errors.push(error instanceof Error ? error : new Error(String(error)));
3489
+ }
3490
+ }
3491
+ if (errors.length > 0) {
3492
+ throw new Error(`failed to dispose shared sidecars: ${errors.map((error) => error.message).join("; ")}`);
3493
+ }
3494
+ }
3495
+ function getSharedAgentOsSidecarInternal(options = {}) {
3496
+ const pool = options.pool ?? "default";
3497
+ const existing = sharedSidecars.get(pool);
3498
+ if (existing && existing.describe().state !== "disposed") {
3499
+ return existing;
3500
+ }
3501
+ const sidecar = new AgentOsSidecar(`agent-os-shared-sidecar:${pool}`, { kind: "shared", ...(pool ? { pool } : {}) }, pool);
3502
+ sharedSidecars.set(pool, sidecar);
3503
+ return sidecar;
3504
+ }
3505
+ async function leaseAgentOsSidecarVm(sidecar, options) {
3506
+ const state = getSidecarState(sidecar);
3507
+ if (state.description.state !== "ready") {
3508
+ throw new Error(`Cannot lease VM from sidecar ${state.description.sidecarId} while it is ${state.description.state}`);
3509
+ }
3510
+ let transport;
3511
+ const client = createAgentOsSidecarClient({
3512
+ async createSessionTransport(sessionBootstrap) {
3513
+ transport = await createInProcessSidecarTransport(sessionBootstrap, options);
3514
+ return transport;
3515
+ },
3516
+ });
3517
+ let disposed = false;
3518
+ let leaseRecord;
3519
+ try {
3520
+ const session = await client.createSession({
3521
+ placement: cloneSidecarPlacement(state.description.placement),
3522
+ });
3523
+ const vm = await session.createVm();
3524
+ const admin = transport?.getVmAdmin(vm.vmId);
3525
+ if (!admin) {
3526
+ throw new Error(`Sidecar VM admin was not registered for ${vm.vmId}`);
3527
+ }
3528
+ const lease = {
3529
+ sidecar,
3530
+ session,
3531
+ vm,
3532
+ admin,
3533
+ async dispose() {
3534
+ if (disposed) {
3535
+ return;
3536
+ }
3537
+ disposed = true;
3538
+ state.activeLeases.delete(leaseRecord);
3539
+ state.description.activeVmCount = state.activeLeases.size;
3540
+ await client.dispose();
3541
+ },
3542
+ };
3543
+ leaseRecord = {
3544
+ dispose: () => lease.dispose(),
3545
+ };
3546
+ state.activeLeases.add(leaseRecord);
3547
+ state.description.activeVmCount = state.activeLeases.size;
3548
+ return lease;
3549
+ }
3550
+ catch (error) {
3551
+ await client.dispose().catch(() => { });
3552
+ throw error;
3553
+ }
3554
+ }
3555
+ async function createInProcessSidecarTransport(sessionBootstrap, options) {
3556
+ const vmAdmins = new Map();
3557
+ let disposed = false;
3558
+ async function disposeVmAdmin(vmId) {
3559
+ const admin = vmAdmins.get(vmId);
3560
+ if (!admin) {
3561
+ return;
3562
+ }
3563
+ vmAdmins.delete(vmId);
3564
+ await admin.dispose();
3565
+ }
3566
+ return {
3567
+ async createVm(vmBootstrap) {
3568
+ if (disposed) {
3569
+ throw new Error(`Cannot create VM ${vmBootstrap.vmId} for disposed sidecar session ${sessionBootstrap.sessionId}`);
3570
+ }
3571
+ const admin = await options.createVm(sessionBootstrap, vmBootstrap);
3572
+ vmAdmins.set(vmBootstrap.vmId, admin);
3573
+ },
3574
+ async disposeVm(vmId) {
3575
+ await disposeVmAdmin(vmId);
3576
+ },
3577
+ async dispose() {
3578
+ if (disposed) {
3579
+ return;
3580
+ }
3581
+ disposed = true;
3582
+ const errors = [];
3583
+ for (const vmId of [...vmAdmins.keys()]) {
3584
+ try {
3585
+ await disposeVmAdmin(vmId);
3586
+ }
3587
+ catch (error) {
3588
+ errors.push(error instanceof Error ? error : new Error(String(error)));
3589
+ }
3590
+ }
3591
+ if (errors.length > 0) {
3592
+ throw new Error(errors.map((error) => error.message).join("; "));
3593
+ }
3594
+ },
3595
+ getVmAdmin(vmId) {
3596
+ return vmAdmins.get(vmId);
3597
+ },
3598
+ };
3599
+ }
3600
+ function getSidecarState(sidecar) {
3601
+ const state = sidecarStates.get(sidecar);
3602
+ if (!state) {
3603
+ throw new Error("Unknown Agent OS sidecar handle");
3604
+ }
3605
+ return state;
3606
+ }
3607
+ function cloneSidecarDescription(description) {
3608
+ return {
3609
+ ...description,
3610
+ placement: cloneSidecarPlacement(description.placement),
3611
+ };
3612
+ }
3613
+ function cloneSidecarPlacement(placement) {
3614
+ if (placement.kind === "shared") {
3615
+ return {
3616
+ kind: "shared",
3617
+ ...(placement.pool ? { pool: placement.pool } : {}),
3618
+ };
3619
+ }
3620
+ return {
3621
+ kind: "explicit",
3622
+ sidecarId: placement.sidecarId,
3623
+ };
3624
+ }