@rivet-dev/agentos-core 0.2.8-rc.1 → 0.2.8-rc.3

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 (56) hide show
  1. package/README.md +41 -38
  2. package/dist/agent-os.d.ts +189 -231
  3. package/dist/agent-os.js +756 -1162
  4. package/dist/agentos-package.d.ts +5 -3
  5. package/dist/agentos-package.js +5 -2
  6. package/dist/{host-tools-zod.d.ts → bindings-zod.d.ts} +1 -1
  7. package/dist/{host-tools-zod.js → bindings-zod.js} +27 -20
  8. package/dist/bindings.d.ts +43 -0
  9. package/dist/bindings.js +34 -0
  10. package/dist/cron/cron-manager.js +43 -10
  11. package/dist/cron/index.d.ts +1 -1
  12. package/dist/cron/types.d.ts +25 -9
  13. package/dist/filesystem-snapshot.d.ts +1 -2
  14. package/dist/filesystem-snapshot.js +0 -43
  15. package/dist/generated/AcpLimitsConfig.d.ts +16 -0
  16. package/dist/generated/BindingLimitsConfig.d.ts +10 -0
  17. package/dist/generated/CreateVmConfig.d.ts +2 -0
  18. package/dist/generated/ProcessLimitsConfig.d.ts +7 -0
  19. package/dist/generated/ProcessLimitsConfig.js +2 -0
  20. package/dist/generated/VmGroupConfig.d.ts +5 -0
  21. package/dist/generated/VmGroupConfig.js +2 -0
  22. package/dist/generated/VmLimitsConfig.d.ts +4 -2
  23. package/dist/generated/VmUserAccountConfig.d.ts +9 -0
  24. package/dist/generated/VmUserAccountConfig.js +2 -0
  25. package/dist/generated/VmUserConfig.d.ts +19 -0
  26. package/dist/generated/WasmLimitsConfig.d.ts +1 -0
  27. package/dist/index.d.ts +4 -5
  28. package/dist/index.js +4 -5
  29. package/dist/layers.d.ts +5 -2
  30. package/dist/layers.js +0 -185
  31. package/dist/options-schema.d.ts +133 -31
  32. package/dist/options-schema.js +143 -26
  33. package/dist/runtime-compat.d.ts +6 -35
  34. package/dist/runtime-compat.js +84 -372
  35. package/dist/runtime.d.ts +5 -1
  36. package/dist/sandbox.d.ts +4 -4
  37. package/dist/sandbox.js +4 -4
  38. package/dist/session-api.d.ts +180 -0
  39. package/dist/session-api.js +1 -0
  40. package/dist/sidecar/agentos-protocol.d.ts +429 -29
  41. package/dist/sidecar/agentos-protocol.js +691 -93
  42. package/dist/sidecar/rpc-client.d.ts +18 -6
  43. package/dist/sidecar/rpc-client.js +78 -31
  44. package/dist/test/runtime.d.ts +2 -1
  45. package/dist/test/runtime.js +2 -1
  46. package/dist/test/terminal-harness.js +0 -3
  47. package/dist/types.d.ts +4 -5
  48. package/package.json +21 -20
  49. package/dist/agent-session-types.d.ts +0 -67
  50. package/dist/generated/ToolLimitsConfig.d.ts +0 -10
  51. package/dist/host-tools.d.ts +0 -45
  52. package/dist/host-tools.js +0 -35
  53. package/dist/overlay-filesystem.d.ts +0 -19
  54. package/dist/overlay-filesystem.js +0 -645
  55. /package/dist/generated/{ToolLimitsConfig.js → BindingLimitsConfig.js} +0 -0
  56. /package/dist/{agent-session-types.js → generated/VmUserConfig.js} +0 -0
package/dist/agent-os.js CHANGED
@@ -3,22 +3,139 @@ import { randomUUID } from "node:crypto";
3
3
  import { existsSync, readdirSync, statSync } from "node:fs";
4
4
  import { join, posix as posixPath, resolve as resolveHostPath, } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { validateToolkits } from "./host-tools.js";
7
- import { zodToJsonSchema } from "./host-tools-zod.js";
6
+ import { validateBindings } from "./bindings.js";
7
+ import { zodToJsonSchema } from "./bindings-zod.js";
8
8
  import { parseAgentOsOptions } from "./options-schema.js";
9
9
  import { resolvePublishedSidecarBinary } from "./sidecar/binary.js";
10
10
  import { findCargoBinary, resolveCargoBinary } from "./sidecar/cargo.js";
11
- export { isAcpTimeoutErrorData } from "./json-rpc.js";
12
11
  const ACP_PROTOCOL_VERSION = 1;
13
12
  const ACP_EXTENSION_NAMESPACE = "dev.rivet.agent-os.acp";
14
13
  const SHELL_DISPOSE_TIMEOUT_MS = 5_000;
15
- function defaultAcpClientCapabilities() {
14
+ function safeWireU64(value) {
15
+ const result = Number(value);
16
+ if (!Number.isSafeInteger(result) || result < 0) {
17
+ throw new RangeError(`wire integer ${value} exceeds JavaScript's safe range`);
18
+ }
19
+ return result;
20
+ }
21
+ function decodeDurableSessionInfo(value) {
16
22
  return {
17
- fs: {
18
- readTextFile: true,
19
- writeTextFile: true,
20
- },
21
- terminal: true,
23
+ sessionId: value.sessionId,
24
+ agent: value.agent,
25
+ cwd: value.cwd,
26
+ additionalDirectories: JSON.parse(value.additionalDirectories),
27
+ state: JSON.parse(value.state),
28
+ latestSequence: safeWireU64(value.latestSequence),
29
+ title: value.title ?? undefined,
30
+ metadata: value.metadata === null ? undefined : JSON.parse(value.metadata),
31
+ createdAt: value.createdAt,
32
+ updatedAt: value.updatedAt,
33
+ };
34
+ }
35
+ const PERMISSION_TERMINAL_REASONS = new Set([
36
+ "already_resolved",
37
+ "prompt_cancelled",
38
+ "adapter_exited",
39
+ "session_deleted",
40
+ "vm_shutdown",
41
+ "request_not_found",
42
+ ]);
43
+ function permissionTerminalReason(value) {
44
+ if (value !== null &&
45
+ PERMISSION_TERMINAL_REASONS.has(value)) {
46
+ return value;
47
+ }
48
+ throw new Error(`invalid permission terminal reason: ${value ?? "missing"}`);
49
+ }
50
+ function decodeDurableSessionEvent(value) {
51
+ const envelope = {
52
+ durability: "durable",
53
+ sessionId: value.sessionId,
54
+ sequence: safeWireU64(value.sequence),
55
+ timestamp: value.timestamp,
56
+ };
57
+ switch (value.event.tag) {
58
+ case "AcpDurableSessionUpdate": {
59
+ const update = JSON.parse(value.event.val.update);
60
+ const { sessionUpdate: type, ...payload } = update;
61
+ return {
62
+ ...envelope,
63
+ type,
64
+ ...payload,
65
+ };
66
+ }
67
+ case "AcpDurablePermissionRequest": {
68
+ const request = JSON.parse(value.event.val.request);
69
+ const { sessionId: _sessionId, ...payload } = request;
70
+ return {
71
+ ...envelope,
72
+ type: "permission_request",
73
+ requestId: value.event.val.requestId,
74
+ ...payload,
75
+ };
76
+ }
77
+ case "AcpDurablePermissionResponse": {
78
+ const status = value.event.val.status;
79
+ if (status !== "accepted" && status !== "not_pending") {
80
+ throw new Error(`invalid permission response event status: ${status}`);
81
+ }
82
+ const response = JSON.parse(value.event.val.response);
83
+ return {
84
+ ...envelope,
85
+ type: "permission_response",
86
+ requestId: value.event.val.requestId,
87
+ ...response,
88
+ status,
89
+ ...(value.event.val.reason === null
90
+ ? {}
91
+ : { reason: permissionTerminalReason(value.event.val.reason) }),
92
+ };
93
+ }
94
+ }
95
+ }
96
+ function normalizeSessionCapabilities(value) {
97
+ const capabilities = toRecord(value);
98
+ const prompt = toRecord(capabilities.promptCapabilities);
99
+ const mcp = toRecord(capabilities.mcpCapabilities);
100
+ const session = toRecord(capabilities.sessionCapabilities);
101
+ const extensions = Object.fromEntries(Object.entries(capabilities).filter(([key]) => ![
102
+ "loadSession",
103
+ "promptCapabilities",
104
+ "mcpCapabilities",
105
+ "sessionCapabilities",
106
+ ].includes(key)));
107
+ return {
108
+ protocolVersion: ACP_PROTOCOL_VERSION,
109
+ loadSession: capabilities.loadSession === true,
110
+ ...(Object.keys(prompt).length > 0
111
+ ? {
112
+ prompt: {
113
+ audio: prompt.audio === true || undefined,
114
+ embeddedContext: prompt.embeddedContext === true || undefined,
115
+ image: prompt.image === true || undefined,
116
+ },
117
+ }
118
+ : {}),
119
+ ...(Object.keys(mcp).length > 0
120
+ ? {
121
+ mcp: {
122
+ http: mcp.http === true || undefined,
123
+ sse: mcp.sse === true || undefined,
124
+ },
125
+ }
126
+ : {}),
127
+ ...(Object.keys(session).length > 0
128
+ ? {
129
+ session: {
130
+ list: typeof session.list === "object" || undefined,
131
+ resume: typeof session.resume === "object" || undefined,
132
+ close: typeof session.close === "object" || undefined,
133
+ delete: typeof session.delete === "object" || undefined,
134
+ additionalDirectories: typeof session.additionalDirectories === "object" || undefined,
135
+ },
136
+ }
137
+ : {}),
138
+ ...(Object.keys(extensions).length > 0 ? { extensions } : {}),
22
139
  };
23
140
  }
24
141
  async function waitForTrackedExitPromises(promises, timeoutMs) {
@@ -32,26 +149,24 @@ async function waitForTrackedExitPromises(promises, timeoutMs) {
32
149
  }),
33
150
  ]);
34
151
  }
152
+ import { OPT_AGENTOS_ROOT, } from "./agentos-package.js";
35
153
  import { getBaseEnvironment } from "./base-filesystem.js";
36
154
  import { CronManager } from "./cron/cron-manager.js";
37
155
  import { TimerScheduleDriver } from "./cron/timer-driver.js";
156
+ import { resolveDefaultSoftware } from "./default-software.js";
38
157
  import { snapshotVirtualFilesystem, sortFilesystemEntries, } from "./filesystem-snapshot.js";
39
158
  import { serializeMountConfigForSidecar, } from "./js-bridge.js";
40
159
  import { createSnapshotExport, } from "./layers.js";
41
- import { OPT_AGENTOS_ROOT, } from "./agentos-package.js";
42
- import { resolveDefaultSoftware } from "./default-software.js";
43
160
  import { allowAll } from "./runtime-compat.js";
44
- import { AcpRuntimeKind, decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
161
+ import { decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
45
162
  import { serializePermissionsForSidecar } from "./sidecar/permissions.js";
46
163
  import { createAgentOsSidecarClient, NativeSidecarKernelProxy, SidecarProcess, serializeRootFilesystemForSidecar, } from "./sidecar/rpc-client.js";
47
164
  function defaultAgentStderrHandler(event) {
48
165
  process.stderr.write(event.chunk);
49
166
  }
50
167
  function defaultAgentExitHandler(event) {
51
- process.stderr.write(`[agentos] agent adapter exited unexpectedly: session=${event.sessionId} agent=${event.agentType} exitCode=${event.exitCode ?? "unknown"} restart=${event.restart} (${event.restartCount}/${event.maxRestarts})\n`);
168
+ process.stderr.write(`[agentos] agent adapter exited unexpectedly: session=${event.sessionId} agent=${event.agentType} exitCode=${event.exitCode ?? "unknown"}; restore explicitly before retrying\n`);
52
169
  }
53
- const LEGACY_PERMISSION_METHOD = "request/permission";
54
- const ACP_PERMISSION_METHOD = "session/request_permission";
55
170
  class AcpDispatchError extends Error {
56
171
  code;
57
172
  data;
@@ -124,51 +239,6 @@ function normalizePackageRef(value) {
124
239
  }
125
240
  return undefined;
126
241
  }
127
- function parseAcpJson(value, context) {
128
- if (value === null) {
129
- return undefined;
130
- }
131
- try {
132
- return JSON.parse(value);
133
- }
134
- catch (error) {
135
- throw new Error(`invalid ACP ${context} JSON: ${error instanceof Error ? error.message : String(error)}`);
136
- }
137
- }
138
- function parseAcpJsonList(values, context) {
139
- return values.map((value, index) => parseAcpJson(value, `${context}[${index}]`));
140
- }
141
- function sidecarSessionCreatedFromAcp(response) {
142
- return {
143
- sessionId: response.sessionId,
144
- ...(response.pid !== null ? { pid: response.pid } : {}),
145
- modes: parseAcpJson(response.modes, "modes"),
146
- configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
147
- agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
148
- agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
149
- };
150
- }
151
- function sidecarSessionStateFromAcp(response) {
152
- return {
153
- sessionId: response.sessionId,
154
- agentType: response.agentType,
155
- processId: response.processId,
156
- ...(response.pid !== null ? { pid: response.pid } : {}),
157
- closed: response.closed,
158
- modes: parseAcpJson(response.modes, "modes"),
159
- configOptions: parseAcpJsonList(response.configOptions, "configOptions"),
160
- agentCapabilities: parseAcpJson(response.agentCapabilities, "agentCapabilities"),
161
- agentInfo: parseAcpJson(response.agentInfo, "agentInfo"),
162
- };
163
- }
164
- function isLocalCancelledPromptResponse(method, response) {
165
- const result = toRecord(response.result);
166
- return (method === "session/prompt" &&
167
- response.id === null &&
168
- response.error === undefined &&
169
- result.stopReason === "cancelled");
170
- }
171
- const CLOSED_SESSION_ID_RETENTION_LIMIT = 2048;
172
242
  const CLOSED_SHELL_ID_RETENTION_LIMIT = 2048;
173
243
  class BoundedSet {
174
244
  limit;
@@ -205,51 +275,6 @@ class BoundedSet {
205
275
  return this.#entries.size;
206
276
  }
207
277
  }
208
- function shouldDispatchToSessionEventHandlers(notification) {
209
- return notification.method === "session/update";
210
- }
211
- function toSessionModes(value) {
212
- if (!value || typeof value !== "object" || Array.isArray(value)) {
213
- return null;
214
- }
215
- return value;
216
- }
217
- function toSessionConfigOptions(value) {
218
- return Array.isArray(value) ? value : [];
219
- }
220
- function toAgentCapabilities(value) {
221
- if (!value || typeof value !== "object" || Array.isArray(value)) {
222
- return {};
223
- }
224
- return value;
225
- }
226
- function toAgentInfo(value) {
227
- if (!value || typeof value !== "object" || Array.isArray(value)) {
228
- return null;
229
- }
230
- if (typeof value.name !== "string") {
231
- return null;
232
- }
233
- return value;
234
- }
235
- function sessionEntryFromInit(sessionId, agentType, initData) {
236
- return {
237
- sessionId,
238
- agentType,
239
- processId: "",
240
- pid: null,
241
- closed: false,
242
- modes: initData.modes ?? null,
243
- configOptions: initData.configOptions ?? [],
244
- capabilities: initData.capabilities ?? {},
245
- agentInfo: initData.agentInfo ?? null,
246
- eventHandlers: new Set(),
247
- permissionHandlers: new Set(),
248
- warnedNoPermissionHandler: false,
249
- configOverrides: new Map(),
250
- pendingPermissionReplies: new Map(),
251
- };
252
- }
253
278
  function isOverlayMountConfig(config) {
254
279
  return "filesystem" in config;
255
280
  }
@@ -445,14 +470,28 @@ const SIDECAR_BUILD_INPUTS = [
445
470
  join(REPO_ROOT, "Cargo.toml"),
446
471
  join(REPO_ROOT, "Cargo.lock"),
447
472
  join(REPO_ROOT, "crates/bridge"),
473
+ join(REPO_ROOT, "crates/build-support"),
448
474
  join(REPO_ROOT, "crates/execution"),
449
475
  join(REPO_ROOT, "crates/kernel"),
450
- join(REPO_ROOT, "crates/sidecar"),
476
+ join(REPO_ROOT, "crates/agentos-protocol"),
477
+ join(REPO_ROOT, "crates/agentos-sidecar"),
478
+ join(REPO_ROOT, "crates/native-sidecar"),
479
+ join(REPO_ROOT, "crates/native-sidecar-core"),
480
+ join(REPO_ROOT, "crates/sidecar-protocol"),
481
+ join(REPO_ROOT, "crates/v8-runtime"),
482
+ join(REPO_ROOT, "crates/vfs"),
483
+ join(REPO_ROOT, "crates/vm-config"),
484
+ join(REPO_ROOT, "packages/build-tools/bridge-src"),
485
+ join(REPO_ROOT, "packages/build-tools/package.json"),
486
+ join(REPO_ROOT, "packages/build-tools/scripts/build-v8-bridge.mjs"),
487
+ join(REPO_ROOT, "packages/core/fixtures/base-filesystem.json"),
488
+ join(REPO_ROOT, "packages/runtime-core/fixtures/base-filesystem.json"),
489
+ join(REPO_ROOT, "pnpm-lock.yaml"),
451
490
  ];
452
491
  let ensuredSidecarBinary = null;
453
492
  function collectConfiguredLowerPaths(config) {
454
493
  const paths = new Set();
455
- for (const lower of config?.lowers ?? []) {
494
+ for (const lower of config?.type === "native" ? [] : (config?.lowers ?? [])) {
456
495
  if (lower.kind !== "snapshot-export") {
457
496
  continue;
458
497
  }
@@ -463,7 +502,7 @@ function collectConfiguredLowerPaths(config) {
463
502
  return paths;
464
503
  }
465
504
  function findBootstrapSeedEntry(config, path) {
466
- for (const lower of config?.lowers ?? []) {
505
+ for (const lower of config?.type === "native" ? [] : (config?.lowers ?? [])) {
467
506
  if (lower.kind !== "snapshot-export") {
468
507
  continue;
469
508
  }
@@ -479,6 +518,9 @@ function findBootstrapSeedEntry(config, path) {
479
518
  return meta ? { path, type: "directory", ...meta } : undefined;
480
519
  }
481
520
  function createKernelBootstrapLower(config, extraEntries = []) {
521
+ if (config?.type === "native") {
522
+ return null;
523
+ }
482
524
  const includesBundledBaseLayer = !(config?.disableDefaultBaseLayer ?? false);
483
525
  const existingPaths = collectConfiguredLowerPaths(config);
484
526
  const entries = [
@@ -547,7 +589,7 @@ function buildLiveBootstrapDirectoryEntries(existingPaths, config) {
547
589
  return entries;
548
590
  }
549
591
  async function bootstrapLiveBootstrapDirectories(client, session, vm, config) {
550
- const existingPaths = new Set((await client.snapshotRootFilesystem(session, vm)).map((entry) => entry.path));
592
+ const existingPaths = new Set((await client.snapshotRootFilesystem(session, vm, Number.MAX_SAFE_INTEGER)).map((entry) => entry.path));
551
593
  const entries = buildLiveBootstrapDirectoryEntries(existingPaths, config);
552
594
  if (entries.length === 0) {
553
595
  return;
@@ -720,11 +762,14 @@ function collectSidecarMountPlan(options) {
720
762
  hostPathMappings.sort((left, right) => right.vmPath.length - left.vmPath.length);
721
763
  return { sidecarMounts, hostMounts, hostPathMappings };
722
764
  }
723
- function collectToolkitBootstrapCommands(toolKits) {
724
- if (toolKits.length === 0) {
765
+ function collectBindingBootstrapCommands(bindings) {
766
+ if (bindings.length === 0) {
725
767
  return [];
726
768
  }
727
- return ["agentos", ...toolKits.map((toolKit) => `agentos-${toolKit.name}`)];
769
+ return [
770
+ "agentos",
771
+ ...bindings.map((bindingCollection) => `agentos-${bindingCollection.name}`),
772
+ ];
728
773
  }
729
774
  function validationMessage(error) {
730
775
  if (typeof error === "object" &&
@@ -742,14 +787,16 @@ function validationMessage(error) {
742
787
  }
743
788
  return error instanceof Error ? error.message : String(error);
744
789
  }
745
- function toolToSidecarDefinition(tool) {
790
+ function bindingToSidecarDefinition(definition) {
746
791
  return {
747
- description: tool.description,
748
- inputSchema: zodToJsonSchema(tool.inputSchema),
749
- ...(tool.timeout !== undefined ? { timeoutMs: tool.timeout } : {}),
750
- ...(tool.examples && tool.examples.length > 0
792
+ description: definition.description,
793
+ inputSchema: zodToJsonSchema(definition.inputSchema),
794
+ ...(definition.timeout !== undefined
795
+ ? { timeoutMs: definition.timeout }
796
+ : {}),
797
+ ...(definition.examples && definition.examples.length > 0
751
798
  ? {
752
- examples: tool.examples.map((example) => ({
799
+ examples: definition.examples.map((example) => ({
753
800
  description: example.description,
754
801
  input: example.input,
755
802
  })),
@@ -757,8 +804,8 @@ function toolToSidecarDefinition(tool) {
757
804
  : {}),
758
805
  };
759
806
  }
760
- function combineInstructions(additionalInstructions, toolReference) {
761
- const parts = [additionalInstructions, toolReference]
807
+ function combineInstructions(additionalInstructions, bindingReference) {
808
+ const parts = [additionalInstructions, bindingReference]
762
809
  .map((part) => part?.trim())
763
810
  .filter((part) => Boolean(part));
764
811
  if (parts.length === 0) {
@@ -766,48 +813,48 @@ function combineInstructions(additionalInstructions, toolReference) {
766
813
  }
767
814
  return parts.join("\n\n");
768
815
  }
769
- function buildHostToolReference(toolKits) {
770
- if (toolKits.length === 0) {
816
+ function buildBindingReference(bindings) {
817
+ if (bindings.length === 0) {
771
818
  return "";
772
819
  }
773
820
  const lines = [
774
- "## Available Host Tools",
821
+ "## Available Host Bindings",
775
822
  "",
776
- "Run `agentos list-tools` to see all available tools.",
823
+ "Run `agentos list-bindings` to see all available bindings.",
777
824
  "",
778
825
  ];
779
- for (const toolKit of toolKits) {
780
- lines.push(`### ${toolKit.name}`);
826
+ for (const bindingCollection of bindings) {
827
+ lines.push(`### ${bindingCollection.name}`);
781
828
  lines.push("");
782
- lines.push(toolKit.description);
829
+ lines.push(bindingCollection.description);
783
830
  lines.push("");
784
- for (const [toolName, tool] of Object.entries(toolKit.tools)) {
785
- const sidecarTool = toolToSidecarDefinition(tool);
786
- const signature = buildToolFlagSignature(sidecarTool.inputSchema);
831
+ for (const [bindingName, definition] of Object.entries(bindingCollection.bindings)) {
832
+ const sidecarBinding = bindingToSidecarDefinition(definition);
833
+ const signature = buildBindingFlagSignature(sidecarBinding.inputSchema);
787
834
  const suffix = signature.length > 0 ? ` ${signature}` : "";
788
- lines.push(`- \`agentos-${toolKit.name} ${toolName}${suffix}\` — ${tool.description}`);
835
+ lines.push(`- \`agentos-${bindingCollection.name} ${bindingName}${suffix}\` — ${definition.description}`);
789
836
  }
790
837
  lines.push("");
791
- const toolsWithExamples = Object.entries(toolKit.tools).filter(([, tool]) => tool.examples && tool.examples.length > 0);
792
- if (toolsWithExamples.length > 0) {
838
+ const bindingsWithExamples = Object.entries(bindingCollection.bindings).filter(([, definition]) => definition.examples && definition.examples.length > 0);
839
+ if (bindingsWithExamples.length > 0) {
793
840
  lines.push("**Examples:**");
794
841
  lines.push("");
795
- for (const [toolName, tool] of toolsWithExamples) {
796
- for (const example of tool.examples ?? []) {
797
- const args = inputToToolFlags(example.input);
842
+ for (const [bindingName, definition] of bindingsWithExamples) {
843
+ for (const example of definition.examples ?? []) {
844
+ const args = inputToBindingFlags(example.input);
798
845
  const suffix = args.length > 0 ? ` ${args}` : "";
799
- lines.push(`- ${example.description}: \`agentos-${toolKit.name} ${toolName}${suffix}\``);
846
+ lines.push(`- ${example.description}: \`agentos-${bindingCollection.name} ${bindingName}${suffix}\``);
800
847
  }
801
848
  }
802
849
  lines.push("");
803
850
  }
804
- lines.push(`Run \`agentos-${toolKit.name} <tool> --help\` for details.`);
851
+ lines.push(`Run \`agentos-${bindingCollection.name} <binding> --help\` for details.`);
805
852
  lines.push("");
806
853
  }
807
854
  return lines.join("\n");
808
855
  }
809
- function buildToolFlagSignature(schema) {
810
- return describeToolFlags(schema)
856
+ function buildBindingFlagSignature(schema) {
857
+ return describeBindingFlags(schema)
811
858
  .map((flag) => {
812
859
  if (flag.required) {
813
860
  return `${flag.name} <${flag.type}>`;
@@ -816,7 +863,7 @@ function buildToolFlagSignature(schema) {
816
863
  })
817
864
  .join(" ");
818
865
  }
819
- function describeToolFlags(schema) {
866
+ function describeBindingFlags(schema) {
820
867
  const schemaObject = asRecord(schema);
821
868
  const properties = asRecord(schemaObject.properties);
822
869
  const required = Array.isArray(schemaObject.required)
@@ -824,11 +871,11 @@ function describeToolFlags(schema) {
824
871
  : new Set();
825
872
  return Object.entries(properties).map(([fieldName, fieldSchema]) => ({
826
873
  name: `--${camelToKebab(fieldName)}`,
827
- type: describeToolFlagType(fieldSchema),
874
+ type: describeBindingFlagType(fieldSchema),
828
875
  required: required.has(fieldName),
829
876
  }));
830
877
  }
831
- function describeToolFlagType(schema) {
878
+ function describeBindingFlagType(schema) {
832
879
  const schemaObject = asRecord(schema);
833
880
  const type = typeof schemaObject.type === "string" ? schemaObject.type : undefined;
834
881
  if (type === "array") {
@@ -847,7 +894,7 @@ function describeJsonSchemaScalarType(schema) {
847
894
  const schemaObject = asRecord(schema);
848
895
  return typeof schemaObject.type === "string" ? schemaObject.type : "string";
849
896
  }
850
- function inputToToolFlags(input) {
897
+ function inputToBindingFlags(input) {
851
898
  const inputObject = asRecord(input);
852
899
  return Object.entries(inputObject)
853
900
  .flatMap(([key, value]) => {
@@ -859,13 +906,13 @@ function inputToToolFlags(input) {
859
906
  return [`--no-${camelToKebab(key)}`];
860
907
  }
861
908
  if (Array.isArray(value)) {
862
- return value.map((item) => `${flag} ${toolCliString(item)}`);
909
+ return value.map((item) => `${flag} ${bindingCliString(item)}`);
863
910
  }
864
- return [`${flag} ${toolCliString(value)}`];
911
+ return [`${flag} ${bindingCliString(value)}`];
865
912
  })
866
913
  .join(" ");
867
914
  }
868
- function toolCliString(value) {
915
+ function bindingCliString(value) {
869
916
  return typeof value === "string" ? value : (JSON.stringify(value) ?? "null");
870
917
  }
871
918
  function camelToKebab(value) {
@@ -903,15 +950,15 @@ async function handleHostCallback(request, context) {
903
950
  };
904
951
  }
905
952
  }
906
- const tool = context.toolMap.get(payload.callback_key);
907
- if (!tool) {
953
+ const definition = context.bindingMap.get(payload.callback_key);
954
+ if (!definition) {
908
955
  return {
909
956
  type: "host_callback_result",
910
957
  invocation_id: payload.invocation_id,
911
- error: `Unknown tool "${payload.callback_key}"`,
958
+ error: `Unknown binding "${payload.callback_key}"`,
912
959
  };
913
960
  }
914
- const permissionMode = toolPermissionMode(context.permissions, payload.callback_key);
961
+ const permissionMode = bindingPermissionMode(context.permissions, payload.callback_key);
915
962
  if (permissionMode !== "allow") {
916
963
  return {
917
964
  type: "host_callback_result",
@@ -919,7 +966,7 @@ async function handleHostCallback(request, context) {
919
966
  error: `EACCES: blocked by binding.invoke policy for ${payload.callback_key}`,
920
967
  };
921
968
  }
922
- const parsed = tool.inputSchema.safeParse(payload.input);
969
+ const parsed = definition.inputSchema.safeParse(payload.input);
923
970
  if (!parsed.success) {
924
971
  return {
925
972
  type: "host_callback_result",
@@ -931,7 +978,7 @@ async function handleHostCallback(request, context) {
931
978
  return {
932
979
  type: "host_callback_result",
933
980
  invocation_id: payload.invocation_id,
934
- result: await executeHostTool(tool, payload.callback_key, parsed.data),
981
+ result: await executeBinding(definition, payload.callback_key, parsed.data),
935
982
  };
936
983
  }
937
984
  catch (error) {
@@ -942,14 +989,14 @@ async function handleHostCallback(request, context) {
942
989
  };
943
990
  }
944
991
  }
945
- function buildToolMap(toolKits) {
946
- const toolMap = new Map();
947
- for (const toolKit of toolKits) {
948
- for (const [toolName, tool] of Object.entries(toolKit.tools)) {
949
- toolMap.set(`${toolKit.name}:${toolName}`, tool);
992
+ function buildBindingMap(bindings) {
993
+ const bindingMap = new Map();
994
+ for (const bindingCollection of bindings) {
995
+ for (const [bindingName, definition] of Object.entries(bindingCollection.bindings)) {
996
+ bindingMap.set(`${bindingCollection.name}:${bindingName}`, definition);
950
997
  }
951
998
  }
952
- return toolMap;
999
+ return bindingMap;
953
1000
  }
954
1001
  function bridgeErrorMessage(error) {
955
1002
  return error instanceof Error ? error.message : String(error);
@@ -1048,7 +1095,7 @@ async function handleJsBridgeCall(request, context) {
1048
1095
  await fs.chmod(path(), requireBridgeNumber(args.mode, "mode"));
1049
1096
  break;
1050
1097
  case "chown":
1051
- await fs.chown(path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"));
1098
+ await fs.chown(path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"), { followSymlinks: args.followSymlinks !== false });
1052
1099
  break;
1053
1100
  case "utimes":
1054
1101
  await fs.utimes(path(), requireBridgeNumber(args.atimeMs, "atimeMs"), requireBridgeNumber(args.mtimeMs, "mtimeMs"));
@@ -1096,90 +1143,90 @@ function parseHostCommandCallbackInput(input) {
1096
1143
  };
1097
1144
  }
1098
1145
  async function handleHostCommandCallback(command, context) {
1099
- const directToolKit = context.toolKits.find((toolKit) => `agentos-${toolKit.name}` === command.command);
1146
+ const directBindings = context.bindings.find((bindingCollection) => `agentos-${bindingCollection.name}` === command.command);
1100
1147
  if (command.command === "agentos") {
1101
1148
  return handleAgentOsRegistryCommand(command, context);
1102
1149
  }
1103
- if (directToolKit) {
1104
- return handleAgentOsToolkitCommand(command, context, directToolKit);
1150
+ if (directBindings) {
1151
+ return handleAgentOsBindingCommand(command, context, directBindings);
1105
1152
  }
1106
1153
  throw new Error(`Unknown host callback command "${command.command}"`);
1107
1154
  }
1108
1155
  async function handleAgentOsRegistryCommand(command, context) {
1109
- const [subcommand, toolkitName, toolName, ...toolArgs] = command.args;
1156
+ const [subcommand, collectionName, bindingName, ...bindingArgs] = command.args;
1110
1157
  if (!subcommand || isHelpFlag(subcommand)) {
1111
1158
  return {
1112
- usage: "agentos <command>: list-tools [toolkit], <toolkit> --help, or <toolkit> <tool> ...",
1159
+ usage: "agentos <command>: list-bindings [collection], <collection> --help, or <collection> <binding> ...",
1113
1160
  };
1114
1161
  }
1115
- if (subcommand === "list-tools") {
1116
- return toolkitName
1117
- ? describeToolkitPayload(context.toolKits, toolkitName)
1118
- : listToolkitsPayload(context.toolKits);
1162
+ if (subcommand === "list-bindings") {
1163
+ return collectionName
1164
+ ? describeBindingsPayload(context.bindings, collectionName)
1165
+ : listBindingsPayload(context.bindings);
1119
1166
  }
1120
- const toolKit = context.toolKits.find((kit) => kit.name === subcommand);
1121
- if (!toolKit) {
1122
- throw new Error(`No toolkit "${subcommand}". Available: ${toolkitNames(context.toolKits)}`);
1167
+ const bindingCollection = context.bindings.find((collection) => collection.name === subcommand);
1168
+ if (!bindingCollection) {
1169
+ throw new Error(`No binding collection "${subcommand}". Available: ${bindingsNames(context.bindings)}`);
1123
1170
  }
1124
- if (!toolkitName || isHelpFlag(toolkitName)) {
1125
- return describeToolkitPayload(context.toolKits, subcommand);
1171
+ if (!collectionName || isHelpFlag(collectionName)) {
1172
+ return describeBindingsPayload(context.bindings, subcommand);
1126
1173
  }
1127
- if (toolName && isHelpFlag(toolName)) {
1128
- return describeToolPayload(toolKit, toolkitName);
1174
+ if (bindingName && isHelpFlag(bindingName)) {
1175
+ return describeBindingPayload(bindingCollection, collectionName);
1129
1176
  }
1130
- return invokeHostTool({
1131
- toolKit,
1132
- toolName: toolkitName,
1133
- args: [toolName, ...toolArgs].filter((value) => typeof value === "string"),
1177
+ return invokeBinding({
1178
+ bindingCollection,
1179
+ bindingName: collectionName,
1180
+ args: [bindingName, ...bindingArgs].filter((value) => typeof value === "string"),
1134
1181
  cwd: command.cwd,
1135
1182
  context,
1136
1183
  });
1137
1184
  }
1138
- async function handleAgentOsToolkitCommand(command, context, toolKit) {
1139
- const [toolName, helpOrFirstArg, ...rest] = command.args;
1140
- if (!toolName || isHelpFlag(toolName)) {
1141
- return describeToolkitPayload(context.toolKits, toolKit.name);
1185
+ async function handleAgentOsBindingCommand(command, context, bindingCollection) {
1186
+ const [bindingName, helpOrFirstArg, ...rest] = command.args;
1187
+ if (!bindingName || isHelpFlag(bindingName)) {
1188
+ return describeBindingsPayload(context.bindings, bindingCollection.name);
1142
1189
  }
1143
1190
  if (helpOrFirstArg && isHelpFlag(helpOrFirstArg)) {
1144
- return describeToolPayload(toolKit, toolName);
1191
+ return describeBindingPayload(bindingCollection, bindingName);
1145
1192
  }
1146
- return invokeHostTool({
1147
- toolKit,
1148
- toolName,
1193
+ return invokeBinding({
1194
+ bindingCollection,
1195
+ bindingName,
1149
1196
  args: [helpOrFirstArg, ...rest].filter((value) => typeof value === "string"),
1150
1197
  cwd: command.cwd,
1151
1198
  context,
1152
1199
  });
1153
1200
  }
1154
- async function invokeHostTool({ toolKit, toolName, args, cwd, context, }) {
1155
- const tool = toolKit.tools[toolName];
1156
- if (!tool) {
1157
- throw new Error(`No tool "${toolName}" in toolkit "${toolKit.name}". Available: ${toolNames(toolKit)}`);
1201
+ async function invokeBinding({ bindingCollection, bindingName, args, cwd, context, }) {
1202
+ const definition = bindingCollection.bindings[bindingName];
1203
+ if (!definition) {
1204
+ throw new Error(`No binding "${bindingName}" in collection "${bindingCollection.name}". Available: ${bindingNames(bindingCollection)}`);
1158
1205
  }
1159
- const callbackKey = `${toolKit.name}:${toolName}`;
1160
- const permissionMode = toolPermissionMode(context.permissions, callbackKey);
1206
+ const callbackKey = `${bindingCollection.name}:${bindingName}`;
1207
+ const permissionMode = bindingPermissionMode(context.permissions, callbackKey);
1161
1208
  if (permissionMode !== "allow") {
1162
1209
  throw new Error(`EACCES: blocked by binding.invoke policy for ${callbackKey}`);
1163
1210
  }
1164
- const input = await parseHostToolInput(tool, args, cwd, context.readFile);
1165
- return executeHostTool(tool, callbackKey, input);
1211
+ const input = await parseBindingInput(definition, args, cwd, context.readFile);
1212
+ return executeBinding(definition, callbackKey, input);
1166
1213
  }
1167
- async function executeHostTool(tool, callbackKey, input) {
1168
- const parsed = tool.inputSchema.safeParse(input);
1214
+ async function executeBinding(definition, callbackKey, input) {
1215
+ const parsed = definition.inputSchema.safeParse(input);
1169
1216
  if (!parsed.success) {
1170
1217
  throw new Error(validationMessage(parsed.error));
1171
1218
  }
1172
1219
  return Promise.race([
1173
- Promise.resolve(tool.execute(parsed.data)),
1220
+ Promise.resolve(definition.execute(parsed.data)),
1174
1221
  new Promise((_, reject) => {
1175
- if (tool.timeout === undefined) {
1222
+ if (definition.timeout === undefined) {
1176
1223
  return;
1177
1224
  }
1178
- setTimeout(() => reject(new Error(`Tool "${callbackKey}" timed out after ${tool.timeout}ms`)), tool.timeout);
1225
+ setTimeout(() => reject(new Error(`Binding "${callbackKey}" timed out after ${definition.timeout}ms`)), definition.timeout);
1179
1226
  }),
1180
1227
  ]);
1181
1228
  }
1182
- async function parseHostToolInput(tool, args, cwd, readFile) {
1229
+ async function parseBindingInput(definition, args, cwd, readFile) {
1183
1230
  if (args[0] === "--json") {
1184
1231
  const value = args[1];
1185
1232
  if (value === undefined) {
@@ -1198,9 +1245,9 @@ async function parseHostToolInput(tool, args, cwd, readFile) {
1198
1245
  const text = new TextDecoder().decode(await readFile(guestPath));
1199
1246
  return JSON.parse(text);
1200
1247
  }
1201
- return parseToolArgv(toolToSidecarDefinition(tool).inputSchema, args);
1248
+ return parseBindingArgv(bindingToSidecarDefinition(definition).inputSchema, args);
1202
1249
  }
1203
- function parseToolArgv(schema, argv) {
1250
+ function parseBindingArgv(schema, argv) {
1204
1251
  const schemaObject = asRecord(schema);
1205
1252
  const properties = asRecord(schemaObject.properties);
1206
1253
  const required = Array.isArray(schemaObject.required)
@@ -1272,49 +1319,49 @@ function parseToolArgv(schema, argv) {
1272
1319
  }
1273
1320
  return input;
1274
1321
  }
1275
- function listToolkitsPayload(toolKits) {
1322
+ function listBindingsPayload(bindings) {
1276
1323
  return {
1277
- toolkits: toolKits.map((toolKit) => ({
1278
- name: toolKit.name,
1279
- description: toolKit.description,
1280
- tools: Object.keys(toolKit.tools),
1324
+ bindings: bindings.map((bindingCollection) => ({
1325
+ name: bindingCollection.name,
1326
+ description: bindingCollection.description,
1327
+ bindings: Object.keys(bindingCollection.bindings),
1281
1328
  })),
1282
1329
  };
1283
1330
  }
1284
- function describeToolkitPayload(toolKits, toolkitName) {
1285
- const toolKit = toolKits.find((kit) => kit.name === toolkitName);
1286
- if (!toolKit) {
1287
- throw new Error(`No toolkit "${toolkitName}". Available: ${toolkitNames(toolKits)}`);
1331
+ function describeBindingsPayload(bindings, collectionName) {
1332
+ const bindingCollection = bindings.find((collection) => collection.name === collectionName);
1333
+ if (!bindingCollection) {
1334
+ throw new Error(`No binding collection "${collectionName}". Available: ${bindingsNames(bindings)}`);
1288
1335
  }
1289
1336
  return {
1290
- name: toolKit.name,
1291
- description: toolKit.description,
1292
- tools: Object.fromEntries(Object.entries(toolKit.tools).map(([toolName, tool]) => [
1293
- toolName,
1337
+ name: bindingCollection.name,
1338
+ description: bindingCollection.description,
1339
+ bindings: Object.fromEntries(Object.entries(bindingCollection.bindings).map(([bindingName, definition]) => [
1340
+ bindingName,
1294
1341
  {
1295
- description: tool.description,
1296
- flags: describeToolFlags(toolToSidecarDefinition(tool).inputSchema),
1342
+ description: definition.description,
1343
+ flags: describeBindingFlags(bindingToSidecarDefinition(definition).inputSchema),
1297
1344
  },
1298
1345
  ])),
1299
1346
  };
1300
1347
  }
1301
- function describeToolPayload(toolKit, toolName) {
1302
- const tool = toolKit.tools[toolName];
1303
- if (!tool) {
1304
- throw new Error(`No tool "${toolName}" in toolkit "${toolKit.name}". Available: ${toolNames(toolKit)}`);
1348
+ function describeBindingPayload(bindingCollection, bindingName) {
1349
+ const definition = bindingCollection.bindings[bindingName];
1350
+ if (!definition) {
1351
+ throw new Error(`No binding "${bindingName}" in collection "${bindingCollection.name}". Available: ${bindingNames(bindingCollection)}`);
1305
1352
  }
1306
1353
  return {
1307
- toolkit: toolKit.name,
1308
- tool: toolName,
1309
- description: tool.description,
1310
- flags: describeToolFlags(toolToSidecarDefinition(tool).inputSchema),
1311
- examples: tool.examples?.map((example) => ({
1354
+ collection: bindingCollection.name,
1355
+ binding: bindingName,
1356
+ description: definition.description,
1357
+ flags: describeBindingFlags(bindingToSidecarDefinition(definition).inputSchema),
1358
+ examples: definition.examples?.map((example) => ({
1312
1359
  description: example.description,
1313
1360
  input: example.input,
1314
1361
  })) ?? [],
1315
1362
  };
1316
1363
  }
1317
- function toolPermissionMode(permissions, callbackKey) {
1364
+ function bindingPermissionMode(permissions, callbackKey) {
1318
1365
  const scope = permissions.binding;
1319
1366
  if (!scope) {
1320
1367
  return "deny";
@@ -1349,11 +1396,11 @@ function permissionPatternMatches(pattern, value) {
1349
1396
  .join("");
1350
1397
  return new RegExp(`^${source}$`).test(value);
1351
1398
  }
1352
- function toolkitNames(toolKits) {
1353
- return toolKits.map((toolKit) => toolKit.name).join(", ");
1399
+ function bindingsNames(bindings) {
1400
+ return bindings.map((bindingCollection) => bindingCollection.name).join(", ");
1354
1401
  }
1355
- function toolNames(toolKit) {
1356
- return Object.keys(toolKit.tools).join(", ");
1402
+ function bindingNames(bindingCollection) {
1403
+ return Object.keys(bindingCollection.bindings).join(", ");
1357
1404
  }
1358
1405
  function isHelpFlag(value) {
1359
1406
  return value === "--help" || value === "-h";
@@ -1362,31 +1409,29 @@ function jsonSchemaType(schema) {
1362
1409
  const schemaObject = asRecord(schema);
1363
1410
  return typeof schemaObject.type === "string" ? schemaObject.type : undefined;
1364
1411
  }
1365
- async function registerToolkitsOnSidecar(client, session, vm, toolKits) {
1366
- if (toolKits.length === 0) {
1412
+ async function registerBindingsOnSidecar(client, session, vm, bindings) {
1413
+ if (bindings.length === 0) {
1367
1414
  return "";
1368
1415
  }
1369
- for (const toolKit of toolKits) {
1416
+ for (const bindingCollection of bindings) {
1370
1417
  await client.registerHostCallbacks(session, vm, {
1371
- name: toolKit.name,
1372
- description: toolKit.description,
1373
- commandAliases: [`agentos-${toolKit.name}`],
1418
+ name: bindingCollection.name,
1419
+ description: bindingCollection.description,
1420
+ commandAliases: [`agentos-${bindingCollection.name}`],
1374
1421
  registryCommandAliases: ["agentos"],
1375
- callbacks: Object.fromEntries(Object.entries(toolKit.tools).map(([toolName, tool]) => [
1376
- toolName,
1377
- toolToSidecarDefinition(tool),
1422
+ callbacks: Object.fromEntries(Object.entries(bindingCollection.bindings).map(([bindingName, definition]) => [
1423
+ bindingName,
1424
+ bindingToSidecarDefinition(definition),
1378
1425
  ])),
1379
1426
  });
1380
1427
  }
1381
- return buildHostToolReference(toolKits);
1428
+ return buildBindingReference(bindings);
1382
1429
  }
1383
1430
  export class AgentOs {
1384
1431
  #kernel;
1385
1432
  sidecar;
1386
- _sessions = new Map();
1387
- _closedSessionIds = new BoundedSet(CLOSED_SESSION_ID_RETENTION_LIMIT);
1388
- _sessionClosePromises = new Map();
1389
- _pendingSessionRequestResolvers = new Map();
1433
+ _durableSessionEventHandlers = new Map();
1434
+ _agentExitHandlers = new Map();
1390
1435
  _processes = new Map();
1391
1436
  _shells = new Map();
1392
1437
  // Value is the recorded exit code (undefined until/unless the exit
@@ -1398,13 +1443,12 @@ export class AgentOs {
1398
1443
  _acpTerminalCounter = 0;
1399
1444
  _softwareRoots;
1400
1445
  _cronManager;
1401
- _toolKits = [];
1402
- _toolReference = "";
1446
+ _bindings = [];
1447
+ _bindingReference = "";
1403
1448
  _permissions = allowAll;
1404
1449
  _hostMounts;
1405
1450
  _env;
1406
1451
  _rootFilesystem;
1407
- _additionalInstructions;
1408
1452
  _sidecarLease = null;
1409
1453
  _sidecarClient;
1410
1454
  _sidecarSession;
@@ -1413,7 +1457,7 @@ export class AgentOs {
1413
1457
  _agentStderrHandler;
1414
1458
  _agentExitHandler;
1415
1459
  _limitWarningHandler;
1416
- constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, additionalInstructions, agentStderrHandler, agentExitHandler, limitWarningHandler) {
1460
+ constructor(kernel, sidecar, softwareRoots, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, agentStderrHandler, agentExitHandler, limitWarningHandler) {
1417
1461
  this.#kernel = kernel;
1418
1462
  this.sidecar = sidecar;
1419
1463
  this._softwareRoots = softwareRoots;
@@ -1423,7 +1467,6 @@ export class AgentOs {
1423
1467
  this._sidecarClient = sidecarClient;
1424
1468
  this._sidecarSession = sidecarSession;
1425
1469
  this._sidecarVm = sidecarVm;
1426
- this._additionalInstructions = additionalInstructions;
1427
1470
  this._agentStderrHandler = agentStderrHandler;
1428
1471
  this._agentExitHandler = agentExitHandler;
1429
1472
  this._limitWarningHandler = limitWarningHandler;
@@ -1448,7 +1491,7 @@ export class AgentOs {
1448
1491
  // Default software is FULLY DYNAMIC: this package's own NON-agent
1449
1492
  // @agentos-software/* dependencies (e.g. common), each default-exporting
1450
1493
  // its registry-built descriptor. Agent packages are NOT projected here —
1451
- // createSession(id) links the matching agent dependency into the running
1494
+ // openSession({ agent: id }) links the matching agent dependency into the running
1452
1495
  // VM on first use, so agent closures (and pi's V8 snapshot bundle) only
1453
1496
  // enter VMs that run them. Unbuilt packages throw with build
1454
1497
  // instructions; opt out via defaultSoftware: false.
@@ -1479,9 +1522,9 @@ export class AgentOs {
1479
1522
  // sidecar owns agent resolution, agent enumeration, and agent snapshot
1480
1523
  // bundle loading from the projected package dirs.
1481
1524
  const localMounts = await resolveCompatLocalMounts(options?.mounts);
1482
- const toolKits = options?.toolKits;
1483
- if (toolKits && toolKits.length > 0) {
1484
- validateToolkits(toolKits);
1525
+ const bindings = options?.bindings;
1526
+ if (bindings && bindings.length > 0) {
1527
+ validateBindings(bindings);
1485
1528
  }
1486
1529
  // Resolve the sidecar handle up front so every VM created here leases the
1487
1530
  // one shared native sidecar process owned by that handle.
@@ -1491,13 +1534,13 @@ export class AgentOs {
1491
1534
  // forwarded `packages` (it owns the staging dir + read-only mount, and
1492
1535
  // runtime `linkSoftware` appends to that live dir). The client no longer
1493
1536
  // stages packages host-side.
1494
- const toolBootstrapCommands = collectToolkitBootstrapCommands(toolKits ?? []);
1537
+ const bindingBootstrapCommands = collectBindingBootstrapCommands(bindings ?? []);
1495
1538
  const bootstrapCommands = [
1496
1539
  ...RUNTIME_BOOTSTRAP_COMMANDS,
1497
- ...toolBootstrapCommands,
1540
+ ...bindingBootstrapCommands,
1498
1541
  ];
1499
1542
  const bootstrapLower = createKernelBootstrapLower(options?.rootFilesystem);
1500
- let toolReference = "";
1543
+ let bindingReference = "";
1501
1544
  let rootBridge = null;
1502
1545
  let kernel = null;
1503
1546
  let client = null;
@@ -1514,7 +1557,7 @@ export class AgentOs {
1514
1557
  const env = getBaseEnvironment();
1515
1558
  // Guest command paths. The sidecar owns the `/opt/agentos` projection and
1516
1559
  // reports the exact projected package commands after `configureVm`.
1517
- // Tool-shim commands are added below.
1560
+ // Binding-shim commands are added below.
1518
1561
  const commandGuestPaths = new Map();
1519
1562
  const { sidecarMounts, hostMounts, hostPathMappings } = collectSidecarMountPlan({
1520
1563
  mounts: options?.mounts,
@@ -1532,11 +1575,24 @@ export class AgentOs {
1532
1575
  const sidecarPermissions = serializePermissionsForSidecar(hostPermissions);
1533
1576
  const createVmConfig = {
1534
1577
  env,
1578
+ database: options?.database,
1579
+ ...(options?.user ? { user: options.user } : {}),
1535
1580
  rootFilesystem: serializeRootFilesystemForSidecar(options?.rootFilesystem, bootstrapLower),
1536
1581
  permissions: sidecarPermissions,
1537
1582
  limits: options?.limits,
1538
1583
  loopbackExemptPorts: options?.loopbackExemptPorts ?? [],
1539
1584
  bootstrapCommands,
1585
+ ...(options?.rootFilesystem?.type === "native"
1586
+ ? {
1587
+ nativeRoot: {
1588
+ plugin: {
1589
+ id: options.rootFilesystem.plugin.id,
1590
+ config: options.rootFilesystem.plugin.config ?? {},
1591
+ },
1592
+ readOnly: options.rootFilesystem.readOnly ?? false,
1593
+ },
1594
+ }
1595
+ : {}),
1540
1596
  // 0.3: the Node builtin allow-list moved from configureVm to
1541
1597
  // VM creation. `undefined` => engine default allow-list;
1542
1598
  // `[]` => deny all; `[..]` => exactly those. Platform and
@@ -1577,16 +1633,16 @@ export class AgentOs {
1577
1633
  loopbackExemptPorts: options?.loopbackExemptPorts,
1578
1634
  packages: sidecarPackages,
1579
1635
  packagesMountAt: OPT_AGENTOS_ROOT,
1580
- toolShimCommands: toolBootstrapCommands,
1636
+ bindingShimCommands: bindingBootstrapCommands,
1581
1637
  });
1582
1638
  for (const command of configuredVm.projectedCommands) {
1583
1639
  commandGuestPaths.set(command.name, command.guestPath);
1584
1640
  }
1585
- if (toolKits && toolKits.length > 0) {
1586
- toolReference = await registerToolkitsOnSidecar(client, session, nativeVm, toolKits);
1641
+ if (bindings && bindings.length > 0) {
1642
+ bindingReference = await registerBindingsOnSidecar(client, session, nativeVm, bindings);
1587
1643
  commandGuestPaths.set("agentos", "/bin/agentos");
1588
- for (const toolKit of toolKits) {
1589
- commandGuestPaths.set(`agentos-${toolKit.name}`, `/bin/agentos-${toolKit.name}`);
1644
+ for (const bindingCollection of bindings) {
1645
+ commandGuestPaths.set(`agentos-${bindingCollection.name}`, `/bin/agentos-${bindingCollection.name}`);
1590
1646
  }
1591
1647
  }
1592
1648
  rootBridge = new NativeSidecarKernelProxy({
@@ -1602,17 +1658,19 @@ export class AgentOs {
1602
1658
  loopbackExemptPorts: options?.loopbackExemptPorts,
1603
1659
  // Retained for runtime mount reconfigures: `configure_vm` is
1604
1660
  // replace-on-write for the whole payload, so post-boot mountFs
1605
- // must resend the boot packages and tool shims.
1661
+ // must resend the boot packages and binding shims.
1606
1662
  packages: sidecarPackages,
1607
1663
  packagesMountAt: OPT_AGENTOS_ROOT,
1608
- toolShimCommands: toolBootstrapCommands,
1664
+ bindingShimCommands: bindingBootstrapCommands,
1609
1665
  commandGuestPaths,
1610
1666
  onDispose: cleanup,
1611
1667
  // The native process is owned by the AgentOsSidecar handle and
1612
1668
  // shared across VMs; disposing this VM must not kill the process.
1613
1669
  ownsClient: false,
1614
1670
  });
1615
- await bootstrapLiveBootstrapDirectories(client, session, nativeVm, options?.rootFilesystem);
1671
+ if (options?.rootFilesystem?.type !== "native") {
1672
+ await bootstrapLiveBootstrapDirectories(client, session, nativeVm, options?.rootFilesystem);
1673
+ }
1616
1674
  kernel = rootBridge;
1617
1675
  const snapshotClient = client;
1618
1676
  return {
@@ -1628,9 +1686,9 @@ export class AgentOs {
1628
1686
  sidecarSession: session,
1629
1687
  sidecarVm: nativeVm,
1630
1688
  permissions: hostPermissions,
1631
- snapshotRootFilesystem: async () => createSnapshotExport(convertSidecarRootSnapshotEntries(await snapshotClient.snapshotRootFilesystem(session, nativeVm))),
1632
- toolKits: toolKits ?? [],
1633
- toolReference,
1689
+ snapshotRootFilesystem: async (maxBytes) => createSnapshotExport(convertSidecarRootSnapshotEntries(await snapshotClient.snapshotRootFilesystem(session, nativeVm, maxBytes))),
1690
+ bindings: bindings ?? [],
1691
+ bindingReference,
1634
1692
  async dispose() {
1635
1693
  if (kernel) {
1636
1694
  const currentKernel = kernel;
@@ -1673,10 +1731,10 @@ export class AgentOs {
1673
1731
  createVm: async () => createVmAdmin(),
1674
1732
  });
1675
1733
  const vmAdmin = sidecarLease.admin;
1676
- const vm = new AgentOs(vmAdmin.kernel, sidecar, [], vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.additionalInstructions, options?.onAgentStderr ?? defaultAgentStderrHandler, options?.onAgentExit ?? defaultAgentExitHandler, options?.onLimitWarning);
1734
+ const vm = new AgentOs(vmAdmin.kernel, sidecar, [], vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.onAgentStderr ?? defaultAgentStderrHandler, options?.onAgentExit ?? defaultAgentExitHandler, options?.onLimitWarning);
1677
1735
  vm._sidecarLease = sidecarLease;
1678
- vm._toolKits = vmAdmin.toolKits;
1679
- vm._toolReference = vmAdmin.toolReference;
1736
+ vm._bindings = vmAdmin.bindings;
1737
+ vm._bindingReference = vmAdmin.bindingReference;
1680
1738
  vm._permissions = vmAdmin.permissions;
1681
1739
  vm._installSidecarRequestHandler();
1682
1740
  vm._cronManager = new CronManager(vm, options?.scheduleDriver ?? new TimerScheduleDriver());
@@ -1694,13 +1752,12 @@ export class AgentOs {
1694
1752
  const kernel = this.#kernel;
1695
1753
  return kernel.execArgv(command, args, options);
1696
1754
  }
1697
- _trackProcess(proc, command, args, stdoutHandlers, stderrHandlers, exitHandlers) {
1755
+ _trackProcess(proc, command, args, outputHandlers, exitHandlers) {
1698
1756
  const entry = {
1699
1757
  proc,
1700
1758
  command,
1701
1759
  args,
1702
- stdoutHandlers,
1703
- stderrHandlers,
1760
+ outputHandlers,
1704
1761
  exitHandlers,
1705
1762
  };
1706
1763
  this._processes.set(proc.pid, entry);
@@ -1711,31 +1768,29 @@ export class AgentOs {
1711
1768
  // in dispose(). (H5: the leak was that dispose() never cleared it.)
1712
1769
  void proc.wait().then((code) => {
1713
1770
  for (const h of exitHandlers)
1714
- h(code);
1771
+ h({ pid: proc.pid, exitCode: code });
1715
1772
  });
1716
1773
  return { pid: proc.pid };
1717
1774
  }
1718
1775
  spawn(command, args, options) {
1719
- const stdoutHandlers = new Set();
1720
- const stderrHandlers = new Set();
1776
+ const outputHandlers = new Set();
1721
1777
  const exitHandlers = new Set();
1722
- // Include caller-provided callbacks in the initial handler sets.
1723
- if (options?.onStdout)
1724
- stdoutHandlers.add(options.onStdout);
1725
- if (options?.onStderr)
1726
- stderrHandlers.add(options.onStderr);
1727
1778
  const proc = this.#kernel.spawn(command, args, {
1728
1779
  ...options,
1729
1780
  onStdout: (data) => {
1730
- for (const h of stdoutHandlers)
1731
- h(data);
1781
+ options?.onStdout?.(data);
1782
+ for (const h of outputHandlers) {
1783
+ h({ pid: proc.pid, stream: "stdout", data });
1784
+ }
1732
1785
  },
1733
1786
  onStderr: (data) => {
1734
- for (const h of stderrHandlers)
1735
- h(data);
1787
+ options?.onStderr?.(data);
1788
+ for (const h of outputHandlers) {
1789
+ h({ pid: proc.pid, stream: "stderr", data });
1790
+ }
1736
1791
  },
1737
1792
  });
1738
- return this._trackProcess(proc, command, args, stdoutHandlers, stderrHandlers, exitHandlers);
1793
+ return this._trackProcess(proc, command, args, outputHandlers, exitHandlers);
1739
1794
  }
1740
1795
  /** Write data to a process's stdin. */
1741
1796
  writeProcessStdin(pid, data) {
@@ -1751,24 +1806,14 @@ export class AgentOs {
1751
1806
  throw new Error(`Process not found: ${pid}`);
1752
1807
  return entry.proc.closeStdin();
1753
1808
  }
1754
- /** Subscribe to stdout data from a process. Returns an unsubscribe function. */
1755
- onProcessStdout(pid, handler) {
1809
+ /** Subscribe to stdout and stderr from a process. */
1810
+ onProcessOutput(pid, handler) {
1756
1811
  const entry = this._processes.get(pid);
1757
1812
  if (!entry)
1758
1813
  throw new Error(`Process not found: ${pid}`);
1759
- entry.stdoutHandlers.add(handler);
1814
+ entry.outputHandlers.add(handler);
1760
1815
  return () => {
1761
- entry.stdoutHandlers.delete(handler);
1762
- };
1763
- }
1764
- /** Subscribe to stderr data from a process. Returns an unsubscribe function. */
1765
- onProcessStderr(pid, handler) {
1766
- const entry = this._processes.get(pid);
1767
- if (!entry)
1768
- throw new Error(`Process not found: ${pid}`);
1769
- entry.stderrHandlers.add(handler);
1770
- return () => {
1771
- entry.stderrHandlers.delete(handler);
1816
+ entry.outputHandlers.delete(handler);
1772
1817
  };
1773
1818
  }
1774
1819
  /** Subscribe to process exit. Returns an unsubscribe function. */
@@ -1778,7 +1823,7 @@ export class AgentOs {
1778
1823
  throw new Error(`Process not found: ${pid}`);
1779
1824
  // If already exited, call immediately.
1780
1825
  if (entry.proc.exitCode !== null) {
1781
- handler(entry.proc.exitCode);
1826
+ handler({ pid, exitCode: entry.proc.exitCode });
1782
1827
  return () => { };
1783
1828
  }
1784
1829
  entry.exitHandlers.add(handler);
@@ -1881,6 +1926,15 @@ export class AgentOs {
1881
1926
  this._assertSafeAbsolutePath(path);
1882
1927
  return this.#kernel.readdir(path);
1883
1928
  }
1929
+ async readdirEntries(path) {
1930
+ this._assertSafeAbsolutePath(path);
1931
+ const entries = await this.#kernel.readdirRecursive(path, { maxDepth: 0 });
1932
+ return entries.map((entry) => ({
1933
+ name: entry.name,
1934
+ isDirectory: entry.isDirectory,
1935
+ isSymbolicLink: entry.isSymbolicLink,
1936
+ }));
1937
+ }
1884
1938
  async readdirRecursive(path, options) {
1885
1939
  this._assertSafeAbsolutePath(path);
1886
1940
  const exclude = options?.exclude ? new Set(options.exclude) : undefined;
@@ -1919,12 +1973,19 @@ export class AgentOs {
1919
1973
  this._assertSafeAbsolutePath(path);
1920
1974
  return this.#kernel.exists(path);
1921
1975
  }
1922
- async snapshotRootFilesystem() {
1976
+ async exportRootFilesystem(options) {
1977
+ if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes <= 0) {
1978
+ throw new RangeError("maxBytes must be a positive safe integer");
1979
+ }
1923
1980
  const nativeSnapshot = this._sidecarLease?.admin.snapshotRootFilesystem;
1924
- if (nativeSnapshot) {
1925
- return nativeSnapshot();
1981
+ const snapshot = nativeSnapshot
1982
+ ? await nativeSnapshot(options.maxBytes)
1983
+ : createSnapshotExport(await snapshotVirtualFilesystem(this._rootFilesystem));
1984
+ const size = Buffer.byteLength(JSON.stringify(snapshot));
1985
+ if (size > options.maxBytes) {
1986
+ throw new RangeError(`root filesystem export is ${size} bytes, limit is ${options.maxBytes}; raise maxBytes to export this filesystem`);
1926
1987
  }
1927
- return createSnapshotExport(await snapshotVirtualFilesystem(this._rootFilesystem));
1988
+ return snapshot;
1928
1989
  }
1929
1990
  /**
1930
1991
  * Mount a filesystem into the running VM. Resolves once the mount has been
@@ -1932,58 +1993,93 @@ export class AgentOs {
1932
1993
  * after the returned promise settles; a delivery failure rejects instead of
1933
1994
  * leaving the mount silently host-only.
1934
1995
  */
1935
- async mountFs(path, driver, options) {
1936
- this._assertSafeAbsolutePath(path);
1937
- await this.#kernel.mountFs(path, driver, { readOnly: options?.readOnly });
1996
+ async mountFs(descriptor) {
1997
+ this._assertSafeAbsolutePath(descriptor.path);
1998
+ if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
1999
+ throw new Error("portable dynamic mounts require the native sidecar");
2000
+ }
2001
+ await this.#kernel.mountDescriptor({
2002
+ guestPath: descriptor.path,
2003
+ readOnly: descriptor.readOnly ?? false,
2004
+ plugin: {
2005
+ id: descriptor.plugin.id,
2006
+ config: descriptor.plugin.config ?? {},
2007
+ },
2008
+ });
1938
2009
  }
1939
2010
  async unmountFs(path) {
1940
2011
  this._assertSafeAbsolutePath(path);
1941
- await this.#kernel.unmountFs(path);
2012
+ if (!(this.#kernel instanceof NativeSidecarKernelProxy)) {
2013
+ throw new Error("portable dynamic mounts require the native sidecar");
2014
+ }
2015
+ await this.#kernel.unmountDescriptor(path);
2016
+ }
2017
+ async listMounts() {
2018
+ if (!(this.#kernel instanceof NativeSidecarKernelProxy))
2019
+ return [];
2020
+ return this.#kernel.listMounts();
1942
2021
  }
1943
2022
  async move(from, to) {
1944
2023
  this._assertWritableAbsolutePath(from);
1945
2024
  this._assertWritableAbsolutePath(to);
1946
2025
  await this.#kernel.movePath(from, to);
1947
2026
  }
1948
- async delete(path, options) {
2027
+ async remove(path, options) {
1949
2028
  this._assertWritableAbsolutePath(path);
1950
2029
  await this.#kernel.removePath(path, {
1951
2030
  recursive: options?.recursive ?? false,
1952
2031
  });
1953
2032
  }
1954
- async fetch(port, request) {
1955
- const url = new URL(request.url);
2033
+ async httpRequest(request) {
2034
+ this._assertSafeAbsolutePath(request.path.split("?")[0] || "/");
2035
+ const method = request.method?.toUpperCase() ?? "GET";
2036
+ const body = typeof request.body === "string"
2037
+ ? request.body
2038
+ : request.body === undefined
2039
+ ? undefined
2040
+ : new TextDecoder().decode(request.body);
1956
2041
  const responsePayload = JSON.parse(await this._sidecarClient.vmFetch(this._sidecarSession, this._sidecarVm, {
1957
- port,
1958
- method: request.method,
1959
- path: `${url.pathname}${url.search}`,
1960
- headersJson: JSON.stringify(Object.fromEntries(request.headers.entries())),
1961
- ...(request.method !== "GET" && request.method !== "HEAD"
1962
- ? { body: await request.text() }
2042
+ port: request.port,
2043
+ method,
2044
+ path: request.path,
2045
+ headersJson: JSON.stringify(request.headers ?? {}),
2046
+ ...(method !== "GET" && method !== "HEAD" && body !== undefined
2047
+ ? { body }
1963
2048
  : {}),
1964
2049
  }));
1965
- const headers = new Headers();
2050
+ const headers = {};
1966
2051
  for (const [key, value] of responsePayload.headers ?? []) {
1967
- headers.append(key, value);
2052
+ headers[key] = value;
1968
2053
  }
1969
- return new Response(Buffer.from(responsePayload.body ?? "", "base64"), {
2054
+ return {
1970
2055
  status: responsePayload.status,
1971
- statusText: responsePayload.statusText,
2056
+ statusText: responsePayload.statusText ?? "",
1972
2057
  headers,
1973
- });
2058
+ body: new Uint8Array(Buffer.from(responsePayload.body ?? "", "base64")),
2059
+ };
1974
2060
  }
1975
2061
  openShell(options) {
1976
2062
  const shellId = `shell-${++this._shellCounter}`;
1977
2063
  this._closedShellIds.delete(shellId);
1978
2064
  const dataHandlers = new Set();
1979
- const handle = this.#kernel.openShell(options);
2065
+ const stderrHandlers = new Set();
2066
+ const exitHandlers = new Set();
2067
+ const handle = this.#kernel.openShell({
2068
+ ...options,
2069
+ onStderr: (data) => {
2070
+ for (const handler of stderrHandlers)
2071
+ handler({ shellId, data });
2072
+ },
2073
+ });
1980
2074
  handle.onData = (data) => {
1981
- for (const h of dataHandlers)
1982
- h(data);
2075
+ for (const handler of dataHandlers)
2076
+ handler({ shellId, data });
1983
2077
  };
1984
2078
  const entry = {
1985
2079
  handle,
1986
2080
  dataHandlers,
2081
+ stderrHandlers,
2082
+ exitHandlers,
1987
2083
  exitPromise: Promise.resolve(0),
1988
2084
  };
1989
2085
  const exitPromise = handle.wait();
@@ -1998,6 +2094,8 @@ export class AgentOs {
1998
2094
  };
1999
2095
  entry.exitPromise = exitPromise.then((exitCode) => {
2000
2096
  finalize(exitCode);
2097
+ for (const handler of exitHandlers)
2098
+ handler({ shellId, exitCode });
2001
2099
  return exitCode;
2002
2100
  }, (error) => {
2003
2101
  finalize();
@@ -2017,7 +2115,11 @@ export class AgentOs {
2017
2115
  throw new Error(`Shell not found: ${shellId}`);
2018
2116
  return entry.handle.write(data);
2019
2117
  }
2020
- /** Subscribe to data output from a shell. Returns an unsubscribe function. */
2118
+ /**
2119
+ * Subscribe to ordered PTY output (stdout and stderr). Returns an unsubscribe
2120
+ * function. `OpenShellOptions.onStderr` is a diagnostic tap for callers that
2121
+ * need channel identity; do not render both surfaces.
2122
+ */
2021
2123
  onShellData(shellId, handler) {
2022
2124
  const entry = this._shells.get(shellId);
2023
2125
  if (!entry)
@@ -2027,6 +2129,28 @@ export class AgentOs {
2027
2129
  entry.dataHandlers.delete(handler);
2028
2130
  };
2029
2131
  }
2132
+ /** Subscribe to the stderr-only diagnostic stream for a shell. */
2133
+ onShellStderr(shellId, handler) {
2134
+ const entry = this._shells.get(shellId);
2135
+ if (!entry)
2136
+ throw new Error(`Shell not found: ${shellId}`);
2137
+ entry.stderrHandlers.add(handler);
2138
+ return () => entry.stderrHandlers.delete(handler);
2139
+ }
2140
+ /** Subscribe to shell exit. */
2141
+ onShellExit(shellId, handler) {
2142
+ const entry = this._shells.get(shellId);
2143
+ if (!entry) {
2144
+ const exitCode = this._closedShellIds.get(shellId);
2145
+ if (exitCode !== undefined) {
2146
+ handler({ shellId, exitCode });
2147
+ return () => { };
2148
+ }
2149
+ throw new Error(`Shell not found: ${shellId}`);
2150
+ }
2151
+ entry.exitHandlers.add(handler);
2152
+ return () => entry.exitHandlers.delete(handler);
2153
+ }
2030
2154
  /** Notify a shell of terminal resize. */
2031
2155
  resizeShell(shellId, cols, rows) {
2032
2156
  const entry = this._shells.get(shellId);
@@ -2148,39 +2272,216 @@ export class AgentOs {
2148
2272
  return;
2149
2273
  entry.proc.kill(9);
2150
2274
  }
2151
- /** Returns all active sessions with their IDs and agent types. */
2152
- listSessions() {
2153
- return [...this._sessions.values()].map((s) => ({
2154
- sessionId: s.sessionId,
2155
- agentType: s.agentType,
2156
- }));
2275
+ async openSession(input) {
2276
+ const response = await this._sendAcpRequest({
2277
+ tag: "AcpOpenSessionRequest",
2278
+ val: {
2279
+ sessionId: input.sessionId ?? null,
2280
+ agent: input.agent,
2281
+ cwd: input.cwd ?? null,
2282
+ additionalDirectories: input.additionalDirectories === undefined
2283
+ ? null
2284
+ : JSON.stringify(input.additionalDirectories),
2285
+ env: input.env === undefined ? null : JSON.stringify(input.env),
2286
+ mcpServers: input.mcpServers === undefined
2287
+ ? null
2288
+ : JSON.stringify(input.mcpServers),
2289
+ permissionPolicy: input.permissionPolicy ?? null,
2290
+ skipOsInstructions: input.skipOsInstructions ?? null,
2291
+ additionalInstructions: combineInstructions(input.additionalInstructions, this._bindingReference) ?? null,
2292
+ },
2293
+ });
2294
+ if (response.tag !== "AcpOpenSessionResponse") {
2295
+ throw new Error(`unexpected openSession response: ${response.tag}`);
2296
+ }
2157
2297
  }
2158
- /** Internal helper: retrieve a session or throw. */
2159
- _requireSession(sessionId) {
2160
- const session = this._sessions.get(sessionId);
2161
- if (!session) {
2162
- throw new Error(`Session not found: ${sessionId}`);
2298
+ async getSession(input) {
2299
+ const response = await this._sendAcpRequest({
2300
+ tag: "AcpGetDurableSessionRequest",
2301
+ val: { sessionId: input?.sessionId ?? null },
2302
+ });
2303
+ if (response.tag !== "AcpGetDurableSessionResponse") {
2304
+ throw new Error(`unexpected getSession response: ${response.tag}`);
2163
2305
  }
2164
- return session;
2306
+ return decodeDurableSessionInfo(response.val.session);
2307
+ }
2308
+ async listSessions(input) {
2309
+ const response = await this._sendAcpRequest({
2310
+ tag: "AcpListDurableSessionsRequest",
2311
+ val: { cursor: input?.cursor ?? null, limit: input?.limit ?? null },
2312
+ });
2313
+ if (response.tag !== "AcpListDurableSessionsResponse") {
2314
+ throw new Error(`unexpected listSessions response: ${response.tag}`);
2315
+ }
2316
+ return {
2317
+ sessions: response.val.sessions.map(decodeDurableSessionInfo),
2318
+ nextCursor: response.val.nextCursor,
2319
+ };
2320
+ }
2321
+ async deleteSession(input = {}) {
2322
+ const response = await this._sendAcpRequest({
2323
+ tag: "AcpDeleteSessionRequest",
2324
+ val: { sessionId: input.sessionId ?? null },
2325
+ });
2326
+ if (response.tag !== "AcpDeleteSessionResponse") {
2327
+ throw new Error(`unexpected deleteSession response: ${response.tag}`);
2328
+ }
2329
+ }
2330
+ async unloadSession(input) {
2331
+ const response = await this._sendAcpRequest({
2332
+ tag: "AcpUnloadSessionRequest",
2333
+ val: { sessionId: input?.sessionId ?? null },
2334
+ });
2335
+ if (response.tag !== "AcpUnloadSessionResponse") {
2336
+ throw new Error(`unexpected unloadSession response: ${response.tag}`);
2337
+ }
2338
+ }
2339
+ async prompt(input) {
2340
+ const response = await this._sendAcpRequest({
2341
+ tag: "AcpPromptRequest",
2342
+ val: {
2343
+ sessionId: input.sessionId ?? null,
2344
+ idempotencyKey: input.idempotencyKey ?? null,
2345
+ content: JSON.stringify(input.content),
2346
+ },
2347
+ });
2348
+ if (response.tag !== "AcpPromptResponse") {
2349
+ throw new Error(`unexpected prompt response: ${response.tag}`);
2350
+ }
2351
+ return {
2352
+ sessionId: response.val.sessionId,
2353
+ message: response.val.message === null ? null : JSON.parse(response.val.message),
2354
+ stopReason: response.val.stopReason,
2355
+ };
2356
+ }
2357
+ async cancelPrompt(input) {
2358
+ const response = await this._sendAcpRequest({
2359
+ tag: "AcpCancelPromptRequest",
2360
+ val: { sessionId: input?.sessionId ?? null },
2361
+ });
2362
+ if (response.tag !== "AcpCancelPromptResponse") {
2363
+ throw new Error(`unexpected cancelPrompt response: ${response.tag}`);
2364
+ }
2365
+ if (response.val.status !== "cancelled" &&
2366
+ response.val.status !== "no_active_prompt") {
2367
+ throw new Error(`invalid cancelPrompt status: ${response.val.status}`);
2368
+ }
2369
+ return { status: response.val.status };
2370
+ }
2371
+ async respondPermission(input) {
2372
+ const response = await this._sendAcpRequest({
2373
+ tag: "AcpRespondPermissionRequest",
2374
+ val: {
2375
+ sessionId: input.sessionId,
2376
+ requestId: input.requestId,
2377
+ optionId: input.optionId,
2378
+ },
2379
+ });
2380
+ if (response.tag !== "AcpRespondPermissionResponse") {
2381
+ throw new Error(`unexpected respondPermission response: ${response.tag}`);
2382
+ }
2383
+ if (response.val.status !== "accepted" &&
2384
+ response.val.status !== "not_pending") {
2385
+ throw new Error(`invalid respondPermission status: ${response.val.status}`);
2386
+ }
2387
+ if (response.val.status === "accepted") {
2388
+ if (response.val.reason !== null) {
2389
+ throw new Error("accepted permission response must not include a reason");
2390
+ }
2391
+ return { status: "accepted" };
2392
+ }
2393
+ return {
2394
+ status: "not_pending",
2395
+ reason: permissionTerminalReason(response.val.reason),
2396
+ };
2397
+ }
2398
+ async readHistory(input) {
2399
+ const response = await this._sendAcpRequest({
2400
+ tag: "AcpReadHistoryRequest",
2401
+ val: {
2402
+ sessionId: input?.sessionId ?? null,
2403
+ before: input?.before === undefined ? null : BigInt(input.before),
2404
+ after: input?.after === undefined ? null : BigInt(input.after),
2405
+ limit: input?.limit ?? null,
2406
+ },
2407
+ });
2408
+ if (response.tag !== "AcpHistoryPageResponse") {
2409
+ throw new Error(`unexpected readHistory response: ${response.tag}`);
2410
+ }
2411
+ return {
2412
+ events: response.val.events.map(decodeDurableSessionEvent),
2413
+ hasMoreBefore: response.val.hasMoreBefore,
2414
+ hasMoreAfter: response.val.hasMoreAfter,
2415
+ };
2416
+ }
2417
+ async getSessionConfig(input) {
2418
+ const response = await this._sendAcpRequest({
2419
+ tag: "AcpGetSessionConfigRequest",
2420
+ val: { sessionId: input?.sessionId ?? null },
2421
+ });
2422
+ if (response.tag !== "AcpSessionConfigResponse") {
2423
+ throw new Error(`unexpected getSessionConfig response: ${response.tag}`);
2424
+ }
2425
+ return {
2426
+ revision: safeWireU64(response.val.revision),
2427
+ options: JSON.parse(response.val.options),
2428
+ };
2429
+ }
2430
+ async setSessionConfigOption(input) {
2431
+ const response = await this._sendAcpRequest({
2432
+ tag: "AcpSetSessionConfigOptionRequest",
2433
+ val: {
2434
+ sessionId: input.sessionId ?? null,
2435
+ configId: input.configId,
2436
+ value: JSON.stringify(input.value),
2437
+ },
2438
+ });
2439
+ if (response.tag !== "AcpSessionConfigResponse") {
2440
+ throw new Error(`unexpected setSessionConfigOption response: ${response.tag}`);
2441
+ }
2442
+ return {
2443
+ revision: safeWireU64(response.val.revision),
2444
+ options: JSON.parse(response.val.options),
2445
+ };
2446
+ }
2447
+ async getSessionCapabilities(input) {
2448
+ const response = await this._sendAcpRequest({
2449
+ tag: "AcpGetSessionCapabilitiesRequest",
2450
+ val: { sessionId: input?.sessionId ?? null },
2451
+ });
2452
+ if (response.tag !== "AcpSessionCapabilitiesResponse") {
2453
+ throw new Error(`unexpected getSessionCapabilities response: ${response.tag}`);
2454
+ }
2455
+ return response.val.capabilities === null
2456
+ ? null
2457
+ : normalizeSessionCapabilities(JSON.parse(response.val.capabilities));
2458
+ }
2459
+ async getSessionAgentInfo(input) {
2460
+ const response = await this._sendAcpRequest({
2461
+ tag: "AcpGetSessionAgentInfoRequest",
2462
+ val: { sessionId: input?.sessionId ?? null },
2463
+ });
2464
+ if (response.tag !== "AcpSessionAgentInfoResponse") {
2465
+ throw new Error(`unexpected getSessionAgentInfo response: ${response.tag}`);
2466
+ }
2467
+ return response.val.agentInfo === null
2468
+ ? null
2469
+ : JSON.parse(response.val.agentInfo);
2165
2470
  }
2166
2471
  /**
2167
2472
  * Dynamically link a software package into the RUNNING VM. The package's
2168
2473
  * `bin/` commands appear under `/opt/agentos/bin` (on `$PATH`) and its `share/man`
2169
2474
  * pages under MANPATH immediately — the `/opt/agentos` mount is host-backed, so
2170
2475
  * writing into its staging dir is reflected live with no reboot. An `agent`
2171
- * block registers the package for `createSession(name)`. Persists for the VM's
2476
+ * block registers the package for `openSession({ agent: name })`. Persists for the VM's
2172
2477
  * lifetime (and across a snapshot iff the volume persists).
2173
2478
  */
2174
2479
  async linkSoftware(descriptor) {
2175
- const ref = normalizePackageRef(descriptor);
2176
- if (!ref) {
2177
- throw new Error("Invalid agentOS package reference");
2178
- }
2179
2480
  // Forward to the sidecar, which owns the `/opt/agentos` projection and
2180
2481
  // appends the package to its live host-backed staging dir; the commands
2181
2482
  // appear under `/opt/agentos/bin` immediately. The sidecar rejects a
2182
2483
  // duplicate command, surfaced here as a thrown error.
2183
- const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm, { path: ref.path });
2484
+ const commands = await this._sidecarClient.linkPackage(this._sidecarSession, this._sidecarVm, descriptor);
2184
2485
  if (this.#kernel instanceof NativeSidecarKernelProxy) {
2185
2486
  this.#kernel.registerCommandGuestPaths(new Map(commands.projectedCommands.map((command) => [
2186
2487
  command.name,
@@ -2189,13 +2490,13 @@ export class AgentOs {
2189
2490
  // Retain the linked package for runtime mount reconfigures:
2190
2491
  // `configure_vm` is replace-on-write, so a later `mountFs` that
2191
2492
  // resent only the boot packages would unproject this one.
2192
- this.#kernel.registerLinkedPackage({ path: ref.path });
2493
+ this.#kernel.registerLinkedPackage(descriptor);
2193
2494
  }
2194
2495
  // The client parses no manifests: an `agent` block in the linked package is
2195
2496
  // picked up by the sidecar (it owns the projected `/opt/agentos` and answers
2196
- // createSession/listAgents from it). Nothing to record client-side.
2497
+ // openSession/listAgents from it). Nothing to record client-side.
2197
2498
  }
2198
- async providedCommands() {
2499
+ async listSoftware() {
2199
2500
  return this._sidecarClient.providedCommands(this._sidecarSession, this._sidecarVm);
2200
2501
  }
2201
2502
  /**
@@ -2215,145 +2516,10 @@ export class AgentOs {
2215
2516
  return response.val.agents.map((agent) => ({
2216
2517
  id: agent.id,
2217
2518
  installed: agent.installed,
2218
- adapterEntrypoint: agent.adapterEntrypoint,
2219
2519
  }));
2220
2520
  }
2221
- _syncSessionState(session, state) {
2222
- session.processId = state.processId;
2223
- session.pid = state.pid ?? null;
2224
- session.closed = state.closed;
2225
- session.modes = toSessionModes(state.modes);
2226
- session.configOptions = toSessionConfigOptions(state.configOptions);
2227
- this._applySyntheticConfigOverrides(session);
2228
- session.capabilities = toAgentCapabilities(state.agentCapabilities);
2229
- session.agentInfo = toAgentInfo(state.agentInfo);
2230
- }
2231
- _applySessionUpdate(session, notification) {
2232
- if (notification.method !== "session/update") {
2233
- return;
2234
- }
2235
- const params = toRecord(notification.params);
2236
- const update = toRecord(params.update ?? params);
2237
- const sessionUpdate = update.sessionUpdate;
2238
- if (sessionUpdate === "current_mode_update" &&
2239
- typeof update.currentModeId === "string" &&
2240
- session.modes) {
2241
- session.modes = {
2242
- ...session.modes,
2243
- currentModeId: update.currentModeId,
2244
- };
2245
- }
2246
- if ((sessionUpdate === "config_option_update" ||
2247
- sessionUpdate === "config_options_update") &&
2248
- Array.isArray(update.configOptions)) {
2249
- session.configOptions = update.configOptions;
2250
- }
2251
- }
2252
- _recordSessionNotification(session, notification) {
2253
- this._applySessionUpdate(session, notification);
2254
- if (shouldDispatchToSessionEventHandlers(notification)) {
2255
- this._dispatchSessionEvent(session, notification);
2256
- }
2257
- if (notification.method === LEGACY_PERMISSION_METHOD ||
2258
- notification.method === ACP_PERMISSION_METHOD) {
2259
- const params = toRecord(notification.params);
2260
- const permissionId = params.permissionId;
2261
- if (typeof permissionId === "string" ||
2262
- typeof permissionId === "number") {
2263
- const request = {
2264
- permissionId: String(permissionId),
2265
- description: typeof params.description === "string"
2266
- ? params.description
2267
- : undefined,
2268
- params,
2269
- };
2270
- for (const handler of session.permissionHandlers) {
2271
- handler(request);
2272
- }
2273
- }
2274
- }
2275
- }
2276
- _dispatchSessionEvent(session, notification) {
2277
- if (session.eventHandlers.size === 0) {
2278
- return;
2279
- }
2280
- for (const subscriber of [...session.eventHandlers]) {
2281
- try {
2282
- subscriber.handler(notification);
2283
- }
2284
- catch {
2285
- // Ignore subscriber callback failures and keep event delivery moving.
2286
- }
2287
- }
2288
- }
2289
- _subscribeSessionEvents(session, handler) {
2290
- const subscriber = {
2291
- handler,
2292
- };
2293
- session.eventHandlers.add(subscriber);
2294
- return () => {
2295
- session.eventHandlers.delete(subscriber);
2296
- };
2297
- }
2298
- /**
2299
- * Warn once per session (host-visible) that a tool-permission request was
2300
- * auto-denied because no `onPermissionRequest` handler is registered. Shared
2301
- * by both the bare-callback and JSON-RPC permission paths so the message and
2302
- * the once-per-session guard cannot drift between them.
2303
- */
2304
- _warnNoPermissionHandlerOnce(session, params) {
2305
- if (session.warnedNoPermissionHandler) {
2306
- return;
2307
- }
2308
- session.warnedNoPermissionHandler = true;
2309
- this._emitSessionWarning(session, `agentos: a tool-permission request (${this._permissionToolLabel(params)}) was ` +
2310
- `auto-denied because no onPermissionRequest handler is registered for session ` +
2311
- `${session.sessionId}. Register one with vm.onPermissionRequest(sessionId, ...) and ` +
2312
- `reply via vm.respondPermission(...) to let the agent use tools.`);
2313
- }
2314
- /** Best-effort human label for the tool named in a permission request. */
2315
- _permissionToolLabel(params) {
2316
- if (typeof params.toolName === "string") {
2317
- return params.toolName;
2318
- }
2319
- const toolCall = params.toolCall;
2320
- if (toolCall &&
2321
- typeof toolCall === "object" &&
2322
- typeof toolCall.title === "string") {
2323
- return toolCall.title;
2324
- }
2325
- return "a tool";
2326
- }
2327
- /**
2328
- * Emit a host-visible warning for a session through the same agent-process log
2329
- * channel that surfaces adapter stderr (`onAgentStderr`, default: process
2330
- * stderr). Used for agent-os-owned diagnostics — e.g. a permission request
2331
- * that was auto-denied because no host hook is registered — so they never fire
2332
- * silently inside the sidecar.
2333
- */
2334
- _emitSessionWarning(session, message) {
2335
- const handler = this._agentStderrHandler;
2336
- if (!handler) {
2337
- return;
2338
- }
2339
- try {
2340
- handler({
2341
- sessionId: session.sessionId,
2342
- agentType: session.agentType,
2343
- processId: session.processId,
2344
- pid: session.pid,
2345
- chunk: new TextEncoder().encode(`${message}\n`),
2346
- });
2347
- }
2348
- catch {
2349
- // A warning sink failure must never affect permission handling.
2350
- }
2351
- }
2352
2521
  _recordAgentStderr(event) {
2353
- const session = (event.sessionId ? this._sessions.get(event.sessionId) : undefined) ??
2354
- [...this._sessions.values()].find((candidate) => candidate.processId === event.processId);
2355
- const sessionId = event.sessionId || session?.sessionId;
2356
- if (!sessionId) {
2522
+ if (!event.sessionId) {
2357
2523
  return;
2358
2524
  }
2359
2525
  const handler = this._agentStderrHandler;
@@ -2362,52 +2528,47 @@ export class AgentOs {
2362
2528
  }
2363
2529
  try {
2364
2530
  handler({
2365
- sessionId,
2366
- agentType: event.agentType || session?.agentType || "",
2531
+ sessionId: event.sessionId,
2532
+ agentType: event.agentType,
2367
2533
  processId: event.processId,
2368
- pid: session?.pid ?? null,
2534
+ pid: null,
2369
2535
  chunk: new Uint8Array(event.chunk),
2370
2536
  });
2371
2537
  }
2372
- catch {
2373
- // Ignore subscriber callback failures and keep event delivery moving.
2538
+ catch (error) {
2539
+ console.error("AgentOS stderr handler failed", error);
2374
2540
  }
2375
2541
  }
2376
2542
  _recordAgentExit(event) {
2377
- const session = this._sessions.get(event.sessionId);
2543
+ const publicEvent = {
2544
+ sessionId: event.sessionId,
2545
+ agentType: event.agentType,
2546
+ processId: event.processId,
2547
+ pid: event.pid,
2548
+ exitCode: event.exitCode,
2549
+ restart: event.restart,
2550
+ restartCount: event.restartCount,
2551
+ maxRestarts: event.maxRestarts,
2552
+ };
2378
2553
  const handler = this._agentExitHandler;
2379
- if (!handler) {
2380
- return;
2381
- }
2382
- try {
2383
- handler({
2384
- sessionId: event.sessionId,
2385
- agentType: event.agentType || session?.agentType || "",
2386
- processId: event.processId,
2387
- pid: session?.pid ?? null,
2388
- exitCode: event.exitCode,
2389
- restart: event.restart,
2390
- restartCount: event.restartCount,
2391
- maxRestarts: event.maxRestarts,
2392
- });
2393
- }
2394
- catch {
2395
- // Ignore subscriber callback failures and keep event delivery moving.
2554
+ if (handler) {
2555
+ try {
2556
+ handler(publicEvent);
2557
+ }
2558
+ catch (error) {
2559
+ console.error("AgentOS agent-exit handler failed", error);
2560
+ }
2396
2561
  }
2397
- }
2398
- _applySyntheticConfigOverrides(session) {
2399
- if (session.configOverrides.size === 0) {
2400
- return;
2562
+ for (const key of ["*", event.sessionId]) {
2563
+ for (const subscription of this._agentExitHandlers.get(key) ?? []) {
2564
+ try {
2565
+ subscription(publicEvent);
2566
+ }
2567
+ catch (error) {
2568
+ console.error("AgentOS agent-exit subscription failed", error);
2569
+ }
2570
+ }
2401
2571
  }
2402
- session.configOptions = session.configOptions.map((option) => {
2403
- const override = session.configOverrides.get(option.id) ??
2404
- (typeof option.category === "string"
2405
- ? session.configOverrides.get(option.category)
2406
- : undefined);
2407
- return override === undefined
2408
- ? option
2409
- : { ...option, currentValue: override };
2410
- });
2411
2572
  }
2412
2573
  _handleSidecarEvent(event) {
2413
2574
  if (event.payload.type === "ext") {
@@ -2419,25 +2580,6 @@ export class AgentOs {
2419
2580
  }
2420
2581
  if (event.payload.name === "limit_warning") {
2421
2582
  this._handleLimitWarning(event.payload.detail);
2422
- return;
2423
- }
2424
- if (event.payload.name !== "acp.session_event") {
2425
- return;
2426
- }
2427
- const sessionId = event.payload.detail.session_id;
2428
- const session = sessionId ? this._sessions.get(sessionId) : undefined;
2429
- if (!session) {
2430
- return;
2431
- }
2432
- const notificationText = event.payload.detail.notification;
2433
- if (typeof notificationText !== "string") {
2434
- return;
2435
- }
2436
- try {
2437
- this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(notificationText)));
2438
- }
2439
- catch {
2440
- // Ignore malformed event payloads from the sidecar.
2441
2583
  }
2442
2584
  }
2443
2585
  _handleLimitWarning(detail) {
@@ -2457,8 +2599,8 @@ export class AgentOs {
2457
2599
  fillPercent: toNumber(detail.fillPercent),
2458
2600
  });
2459
2601
  }
2460
- catch {
2461
- // A throwing handler must never break the sidecar event loop.
2602
+ catch (error) {
2603
+ console.error("AgentOS limit-warning handler failed", error);
2462
2604
  }
2463
2605
  }
2464
2606
  _handleAcpExtEvent(envelope) {
@@ -2468,14 +2610,24 @@ export class AgentOs {
2468
2610
  try {
2469
2611
  const event = decodeAcpEvent(envelope.payload);
2470
2612
  switch (event.tag) {
2471
- case "AcpSessionEvent": {
2472
- const session = this._sessions.get(event.val.sessionId);
2473
- if (!session) {
2474
- return;
2475
- }
2476
- this._recordSessionNotification(session, toJsonRpcNotification(JSON.parse(event.val.notification)));
2613
+ case "AcpDurableSessionEvent": {
2614
+ this._emitDurableSessionEvent(decodeDurableSessionEvent(event.val));
2477
2615
  return;
2478
2616
  }
2617
+ case "AcpEphemeralSessionUpdateEvent": {
2618
+ const update = JSON.parse(event.val.update);
2619
+ const { sessionUpdate: type, ...payload } = update;
2620
+ this._emitDurableSessionEvent({
2621
+ durability: "ephemeral",
2622
+ type,
2623
+ sessionId: event.val.sessionId,
2624
+ afterSequence: safeWireU64(event.val.afterSequence),
2625
+ ...payload,
2626
+ });
2627
+ return;
2628
+ }
2629
+ case "AcpSessionEvent":
2630
+ return;
2479
2631
  case "AcpAgentStderrEvent": {
2480
2632
  this._recordAgentStderr(event.val);
2481
2633
  return;
@@ -2486,22 +2638,19 @@ export class AgentOs {
2486
2638
  }
2487
2639
  }
2488
2640
  }
2489
- catch {
2490
- // Ignore malformed event payloads from the sidecar.
2641
+ catch (error) {
2642
+ console.error("AgentOS failed to decode an ACP sidecar event", error);
2491
2643
  }
2492
2644
  }
2493
- _unsupportedConfigResponse(agentType, category) {
2494
- const message = agentType === "opencode" && category === "model"
2495
- ? "OpenCode reports available models, but model switching must be configured before createSession() because ACP session/set_config_option is not implemented."
2496
- : `The ${category} config option is read-only for ${agentType} sessions.`;
2497
- return {
2498
- jsonrpc: "2.0",
2499
- id: null,
2500
- error: {
2501
- code: -32601,
2502
- message,
2503
- },
2504
- };
2645
+ _emitDurableSessionEvent(entry) {
2646
+ for (const handler of this._durableSessionEventHandlers.get(entry.sessionId) ?? []) {
2647
+ try {
2648
+ handler(entry);
2649
+ }
2650
+ catch (error) {
2651
+ console.error("AgentOS session event handler failed", error);
2652
+ }
2653
+ }
2505
2654
  }
2506
2655
  async _sendAcpRequest(request) {
2507
2656
  const envelope = await this._sidecarClient.extensionRequest(this._sidecarSession, this._sidecarVm, {
@@ -2519,300 +2668,10 @@ export class AgentOs {
2519
2668
  }
2520
2669
  return response;
2521
2670
  }
2522
- async _sendSessionRequest(sessionId, method, params) {
2523
- const session = this._requireSession(sessionId);
2524
- const response = await new Promise((resolve, reject) => {
2525
- const resolvers = this._pendingSessionRequestResolvers.get(sessionId) ?? new Set();
2526
- const resolver = {
2527
- method,
2528
- resolve: (response) => {
2529
- resolve(response);
2530
- },
2531
- };
2532
- resolvers.add(resolver);
2533
- this._pendingSessionRequestResolvers.set(sessionId, resolvers);
2534
- void this._sendAcpRequest({
2535
- tag: "AcpSessionRequest",
2536
- val: {
2537
- sessionId,
2538
- method,
2539
- params: params === undefined ? null : JSON.stringify(params),
2540
- },
2541
- })
2542
- .then((response) => {
2543
- if (response.tag !== "AcpSessionRpcResponse") {
2544
- throw new Error(`unexpected response to AcpSessionRequest: ${response.tag}`);
2545
- }
2546
- return toJsonRpcResponse(JSON.parse(response.val.response));
2547
- })
2548
- .then(resolve, reject)
2549
- .finally(() => {
2550
- const nextResolvers = this._pendingSessionRequestResolvers.get(sessionId);
2551
- if (!nextResolvers) {
2552
- return;
2553
- }
2554
- nextResolvers.delete(resolver);
2555
- if (nextResolvers.size === 0) {
2556
- this._pendingSessionRequestResolvers.delete(sessionId);
2557
- }
2558
- });
2559
- });
2560
- const liveSession = this._sessions.get(sessionId);
2561
- if (liveSession && !isLocalCancelledPromptResponse(method, response)) {
2562
- await this._hydrateSessionState(liveSession).catch(() => { });
2563
- }
2564
- if (!response.error) {
2565
- if (method === "session/set_mode" &&
2566
- typeof params?.modeId === "string" &&
2567
- session.modes) {
2568
- session.modes = {
2569
- ...session.modes,
2570
- currentModeId: params.modeId,
2571
- };
2572
- }
2573
- if (method === "session/set_config_option" &&
2574
- typeof params?.configId === "string" &&
2575
- typeof params?.value === "string") {
2576
- const nextValue = params.value;
2577
- const updatedOption = session.configOptions.find((option) => option.id === params.configId);
2578
- session.configOverrides.set(params.configId, nextValue);
2579
- if (typeof updatedOption?.category === "string") {
2580
- session.configOverrides.set(updatedOption.category, nextValue);
2581
- }
2582
- session.configOptions = session.configOptions.map((option) => option.id === params.configId
2583
- ? { ...option, currentValue: nextValue }
2584
- : option);
2585
- }
2586
- }
2587
- return response;
2588
- }
2589
- async _setSessionConfigByCategory(sessionId, category, value) {
2590
- const session = this._requireSession(sessionId);
2591
- const option = session.configOptions.find((entry) => entry.category === category);
2592
- if (option?.readOnly) {
2593
- return this._unsupportedConfigResponse(session.agentType, category);
2594
- }
2595
- const response = await this._sendSessionRequest(sessionId, "session/set_config_option", {
2596
- configId: option?.id ?? category,
2597
- value,
2598
- });
2599
- return response;
2600
- }
2601
- _removeSession(sessionId) {
2602
- this._sessions.delete(sessionId);
2603
- }
2604
- _abortPendingSessionRequests(sessionId) {
2605
- const resolvers = this._pendingSessionRequestResolvers.get(sessionId);
2606
- if (!resolvers) {
2607
- return;
2608
- }
2609
- this._pendingSessionRequestResolvers.delete(sessionId);
2610
- const response = {
2611
- jsonrpc: "2.0",
2612
- id: null,
2613
- error: {
2614
- code: -32_000,
2615
- message: `Session closed: ${sessionId}`,
2616
- },
2617
- };
2618
- for (const resolver of resolvers) {
2619
- resolver.resolve(response);
2620
- }
2621
- }
2622
- _cancelPendingPromptRequests(sessionId) {
2623
- const resolvers = this._pendingSessionRequestResolvers.get(sessionId);
2624
- if (!resolvers) {
2625
- return false;
2626
- }
2627
- const response = {
2628
- jsonrpc: "2.0",
2629
- id: null,
2630
- result: {
2631
- stopReason: "cancelled",
2632
- },
2633
- };
2634
- let cancelledPrompt = false;
2635
- for (const resolver of [...resolvers]) {
2636
- if (resolver.method !== "session/prompt") {
2637
- continue;
2638
- }
2639
- resolvers.delete(resolver);
2640
- resolver.resolve(response);
2641
- cancelledPrompt = true;
2642
- }
2643
- if (resolvers.size === 0) {
2644
- this._pendingSessionRequestResolvers.delete(sessionId);
2645
- }
2646
- return cancelledPrompt;
2647
- }
2648
- _rejectPendingPermissionReplies(sessionId) {
2649
- const session = this._sessions.get(sessionId);
2650
- if (!session) {
2651
- return;
2652
- }
2653
- this._rejectPendingPermissionRepliesFromSession(session);
2654
- }
2655
- _rejectPendingPermissionRepliesFromSession(session) {
2656
- for (const [permissionId, pendingReply,] of session.pendingPermissionReplies) {
2657
- clearTimeout(pendingReply.timer);
2658
- pendingReply.reject(new Error(`Session closed before permission reply: ${permissionId}`));
2659
- }
2660
- session.pendingPermissionReplies.clear();
2661
- }
2662
- async _closeSessionInternal(sessionId) {
2663
- const closing = this._sessionClosePromises.get(sessionId);
2664
- if (closing) {
2665
- return closing;
2666
- }
2667
- if (this._closedSessionIds.has(sessionId)) {
2668
- return;
2669
- }
2670
- this._abortPendingSessionRequests(sessionId);
2671
- this._rejectPendingPermissionReplies(sessionId);
2672
- this._requireSession(sessionId);
2673
- this._removeSession(sessionId);
2674
- this._closedSessionIds.add(sessionId);
2675
- const closePromise = this._sendAcpRequest({
2676
- tag: "AcpCloseSessionRequest",
2677
- val: { sessionId },
2678
- })
2679
- .then((response) => {
2680
- if (response.tag !== "AcpSessionClosedResponse") {
2681
- throw new Error(`unexpected response to AcpCloseSessionRequest: ${response.tag}`);
2682
- }
2683
- })
2684
- .finally(() => {
2685
- this._sessionClosePromises.delete(sessionId);
2686
- });
2687
- this._sessionClosePromises.set(sessionId, closePromise);
2688
- await closePromise;
2689
- }
2690
- async _hydrateSessionState(session) {
2691
- const response = await this._sendAcpRequest({
2692
- tag: "AcpGetSessionStateRequest",
2693
- val: { sessionId: session.sessionId },
2694
- });
2695
- if (response.tag !== "AcpSessionStateResponse") {
2696
- throw new Error(`unexpected response to AcpGetSessionStateRequest: ${response.tag}`);
2697
- }
2698
- const state = sidecarSessionStateFromAcp(response.val);
2699
- this._syncSessionState(session, state);
2700
- }
2701
- async createSession(agentType, options) {
2702
- // The client is npm-agnostic: it sends only the agent NAME. The sidecar
2703
- // resolves the name -> package -> entrypoint/env/launchArgs from the
2704
- // projected `/opt/agentos/<name>/current/agentos-package.json` and spawns
2705
- // (including the agent's static launch args and manifest env defaults).
2706
- // System-prompt assembly/injection (launch args / OPENCODE_CONTEXTPATHS) is
2707
- // owned by the sidecar; the host only forwards additionalInstructions /
2708
- // skipOsInstructions plus the caller's env.
2709
- const launchEnv = { ...options?.env };
2710
- const sessionCwd = options?.cwd ?? "/workspace";
2711
- const response = await this._sendAcpRequest({
2712
- tag: "AcpCreateSessionRequest",
2713
- val: {
2714
- agentType: String(agentType),
2715
- runtime: AcpRuntimeKind.JavaScript,
2716
- args: [],
2717
- env: new Map(Object.entries(launchEnv)),
2718
- cwd: sessionCwd,
2719
- mcpServers: JSON.stringify(options?.mcpServers ?? []),
2720
- protocolVersion: ACP_PROTOCOL_VERSION,
2721
- clientCapabilities: JSON.stringify(defaultAcpClientCapabilities()),
2722
- additionalInstructions: combineInstructions([this._additionalInstructions, options?.additionalInstructions]
2723
- .map((part) => part?.trim())
2724
- .filter((part) => Boolean(part))
2725
- .join("\n\n") || undefined, this._toolReference),
2726
- skipOsInstructions: options?.skipOsInstructions ?? false,
2727
- },
2728
- });
2729
- if (response.tag !== "AcpSessionCreatedResponse") {
2730
- throw new Error(`unexpected create_session response: ${response.tag}`);
2731
- }
2732
- const created = sidecarSessionCreatedFromAcp(response.val);
2733
- // The sessionId is chosen by the (untrusted/buggy) ACP adapter or sidecar. If it collides
2734
- // with a live session already in `_sessions`, blindly overwriting the entry would orphan the
2735
- // first session's event/permission handlers and pending permission replies. Fail closed:
2736
- // reject the colliding create and leave the original session intact. (A previously-closed,
2737
- // evicted id may still be re-used; only a live, non-closed entry is a collision.)
2738
- const existing = this._sessions.get(created.sessionId);
2739
- if (existing !== undefined && !existing.closed) {
2740
- throw new Error(`session id collision: ${created.sessionId}`);
2741
- }
2742
- const initData = {
2743
- modes: toSessionModes(created.modes) ?? undefined,
2744
- configOptions: toSessionConfigOptions(created.configOptions),
2745
- capabilities: toAgentCapabilities(created.agentCapabilities),
2746
- agentInfo: toAgentInfo(created.agentInfo) ?? undefined,
2747
- };
2748
- const session = sessionEntryFromInit(created.sessionId, String(agentType), initData);
2749
- this._closedSessionIds.delete(created.sessionId);
2750
- this._sessions.set(created.sessionId, session);
2751
- try {
2752
- await this._hydrateSessionState(session);
2753
- }
2754
- catch (error) {
2755
- this._removeSession(created.sessionId);
2756
- throw error;
2757
- }
2758
- return { sessionId: created.sessionId };
2759
- }
2760
- /**
2761
- * Resume a session that exists in durable storage but is not live in this VM
2762
- * (e.g. after a Rivet actor slept and woke with a fresh VM). Thin forwarder:
2763
- * resolves the agent config + adapter entrypoint exactly as {@link createSession}
2764
- * does, then forwards a single `AcpResumeSessionRequest` to the sidecar, which
2765
- * owns the resume state machine (native `session/load` when the agent supports
2766
- * it, else `session/new` + a transcript-continuation preamble). The returned
2767
- * `sessionId` is the live id in this VM (equal to the requested id for native
2768
- * loads, freshly assigned for the fallback); the caller remaps `external -> live`.
2769
- * The new live session is registered + hydrated locally so subsequent prompts
2770
- * route to it.
2771
- *
2772
- * Resume depends on a durable root; on a non-durable (default in-memory) root
2773
- * there is no surviving store and the fallback tier always runs.
2774
- */
2775
- async resumeSession(sessionId, agentType, options) {
2776
- // The client is npm-agnostic: it sends only the agent NAME. The sidecar
2777
- // resolves the name -> package -> entrypoint/env/launchArgs from the
2778
- // projected manifest, exactly as createSession does.
2779
- const sessionCwd = options?.cwd ?? "/workspace";
2780
- const launchEnv = { ...options?.env };
2781
- const response = await this._sendAcpRequest({
2782
- tag: "AcpResumeSessionRequest",
2783
- val: {
2784
- sessionId,
2785
- agentType: String(agentType),
2786
- transcriptPath: options?.transcriptPath ?? null,
2787
- cwd: sessionCwd,
2788
- env: new Map(Object.entries(launchEnv)),
2789
- },
2790
- });
2791
- if (response.tag !== "AcpSessionResumedResponse") {
2792
- throw new Error(`unexpected resume_session response: ${response.tag}`);
2793
- }
2794
- const { sessionId: liveSessionId, mode } = response.val;
2795
- // Register + hydrate the live session so subsequent prompts route to it.
2796
- const existing = this._sessions.get(liveSessionId);
2797
- if (existing !== undefined && !existing.closed) {
2798
- throw new Error(`session id collision: ${liveSessionId}`);
2799
- }
2800
- const session = sessionEntryFromInit(liveSessionId, String(agentType), {});
2801
- this._closedSessionIds.delete(liveSessionId);
2802
- this._sessions.set(liveSessionId, session);
2803
- try {
2804
- await this._hydrateSessionState(session);
2805
- }
2806
- catch (error) {
2807
- this._removeSession(liveSessionId);
2808
- throw error;
2809
- }
2810
- return { sessionId: liveSessionId, mode };
2811
- }
2812
2671
  _installSidecarRequestHandler() {
2813
2672
  const context = {
2814
- toolKits: this._toolKits,
2815
- toolMap: buildToolMap(this._toolKits),
2673
+ bindings: this._bindings,
2674
+ bindingMap: buildBindingMap(this._bindings),
2816
2675
  permissions: this._permissions,
2817
2676
  readFile: (path) => this.readFile(path),
2818
2677
  };
@@ -2841,25 +2700,6 @@ export class AgentOs {
2841
2700
  }
2842
2701
  const callback = decodeAcpCallback(envelope.payload);
2843
2702
  switch (callback.tag) {
2844
- case "AcpPermissionCallback": {
2845
- const reply = await this._handleAcpPermissionCallback(callback.val.sessionId, callback.val.permissionId, {
2846
- ...toRecord(JSON.parse(callback.val.params)),
2847
- _acpMethod: ACP_PERMISSION_METHOD,
2848
- });
2849
- return {
2850
- type: "ext_result",
2851
- envelope: {
2852
- namespace: ACP_EXTENSION_NAMESPACE,
2853
- payload: encodeAcpCallbackResponse({
2854
- tag: "AcpPermissionCallbackResponse",
2855
- val: {
2856
- permissionId: callback.val.permissionId,
2857
- reply,
2858
- },
2859
- }),
2860
- },
2861
- };
2862
- }
2863
2703
  case "AcpHostRequestCallback": {
2864
2704
  const response = await this._dispatchAcpSidecarRequest(toJsonRpcRequest(JSON.parse(callback.val.request)));
2865
2705
  return {
@@ -2911,8 +2751,6 @@ export class AgentOs {
2911
2751
  async _handleSupportedAcpSidecarRequest(request) {
2912
2752
  const params = this._acpParams(request);
2913
2753
  switch (request.method) {
2914
- case ACP_PERMISSION_METHOD:
2915
- return this._handleAcpPermissionRequest(request, params);
2916
2754
  case "fs/read":
2917
2755
  case "fs/read_text_file":
2918
2756
  return this._handleAcpReadFile(params);
@@ -2945,99 +2783,6 @@ export class AgentOs {
2945
2783
  });
2946
2784
  }
2947
2785
  }
2948
- _normalizeAcpPermissionOptionId(options, reply) {
2949
- const optionTargets = reply === "always"
2950
- ? {
2951
- optionIds: new Set(["always", "allow_always"]),
2952
- kinds: new Set(["allow_always"]),
2953
- }
2954
- : reply === "once"
2955
- ? {
2956
- optionIds: new Set(["once", "allow_once"]),
2957
- kinds: new Set(["allow_once"]),
2958
- }
2959
- : {
2960
- optionIds: new Set(["reject", "reject_once"]),
2961
- kinds: new Set(["reject_once"]),
2962
- };
2963
- const matched = options?.find((option) => {
2964
- const optionId = typeof option.optionId === "string" ? option.optionId : undefined;
2965
- const kind = typeof option.kind === "string" ? option.kind : undefined;
2966
- return ((optionId !== undefined && optionTargets.optionIds.has(optionId)) ||
2967
- (kind !== undefined && optionTargets.kinds.has(kind)));
2968
- });
2969
- if (matched && typeof matched.optionId === "string") {
2970
- return matched.optionId;
2971
- }
2972
- if (reply === "always") {
2973
- return "allow_always";
2974
- }
2975
- if (reply === "once") {
2976
- return "allow_once";
2977
- }
2978
- return "reject_once";
2979
- }
2980
- _buildAcpPermissionResult(reply, params) {
2981
- const options = Array.isArray(params.options)
2982
- ? params.options.filter((option) => typeof option === "object" && option !== null)
2983
- : undefined;
2984
- const optionId = this._normalizeAcpPermissionOptionId(options, reply);
2985
- return {
2986
- outcome: optionId
2987
- ? {
2988
- outcome: "selected",
2989
- optionId,
2990
- }
2991
- : {
2992
- outcome: "cancelled",
2993
- },
2994
- };
2995
- }
2996
- async _handleAcpPermissionRequest(request, params) {
2997
- const sessionId = typeof params.sessionId === "string" ? params.sessionId : undefined;
2998
- if (!sessionId) {
2999
- throw new AcpDispatchError(-32602, `${ACP_PERMISSION_METHOD} requires a sessionId`);
3000
- }
3001
- const session = this._sessions.get(sessionId);
3002
- if (!session) {
3003
- throw new AcpDispatchError(-32602, `Session not found: ${sessionId}`);
3004
- }
3005
- const permissionId = String(request.id);
3006
- const permissionParams = {
3007
- ...params,
3008
- permissionId,
3009
- _acpMethod: request.method,
3010
- };
3011
- if (session.permissionHandlers.size === 0) {
3012
- // Default-closed deny; warn once (host-visible) so a forgotten
3013
- // onPermissionRequest handler is an observable cause rather than a
3014
- // silent denial. See _warnNoPermissionHandlerOnce.
3015
- this._warnNoPermissionHandlerOnce(session, permissionParams);
3016
- return this._buildAcpPermissionResult("reject", permissionParams);
3017
- }
3018
- const reply = await new Promise((resolve, reject) => {
3019
- const timer = setTimeout(() => {
3020
- session.pendingPermissionReplies.delete(permissionId);
3021
- reject(new Error(`Timed out waiting for permission reply: ${permissionId}`));
3022
- }, 120_000);
3023
- session.pendingPermissionReplies.set(permissionId, {
3024
- resolve,
3025
- reject,
3026
- timer,
3027
- });
3028
- const permissionRequest = {
3029
- permissionId,
3030
- description: typeof permissionParams["description"] === "string"
3031
- ? permissionParams["description"]
3032
- : undefined,
3033
- params: permissionParams,
3034
- };
3035
- for (const handler of session.permissionHandlers) {
3036
- handler(permissionRequest);
3037
- }
3038
- });
3039
- return this._buildAcpPermissionResult(reply, permissionParams);
3040
- }
3041
2786
  _acpParams(request) {
3042
2787
  if (!request.params) {
3043
2788
  return {};
@@ -3210,9 +2955,6 @@ export class AgentOs {
3210
2955
  ...(cwd ? { cwd } : {}),
3211
2956
  ...(cols !== undefined ? { cols: Math.trunc(cols) } : {}),
3212
2957
  ...(rows !== undefined ? { rows: Math.trunc(rows) } : {}),
3213
- onStderr: (data) => {
3214
- this._appendAcpTerminalOutput(terminal, data);
3215
- },
3216
2958
  }),
3217
2959
  output: "",
3218
2960
  truncated: false,
@@ -3289,193 +3031,38 @@ export class AgentOs {
3289
3031
  terminal.handle.resize(Math.trunc(cols), Math.trunc(rows));
3290
3032
  return null;
3291
3033
  }
3292
- async _handleAcpPermissionCallback(sessionId, permissionId, params) {
3293
- const session = this._sessions.get(sessionId);
3294
- if (!session) {
3295
- return "reject";
3296
- }
3297
- if (session.permissionHandlers.size === 0) {
3298
- // Default-closed: deny when no host hook is listening, and warn once
3299
- // (host-visible) so a forgotten onPermissionRequest handler is not an
3300
- // invisible cause of an agent that cannot use any tool.
3301
- this._warnNoPermissionHandlerOnce(session, params);
3302
- return "reject";
3303
- }
3304
- try {
3305
- return await new Promise((resolve, reject) => {
3306
- const timer = setTimeout(() => {
3307
- session.pendingPermissionReplies.delete(permissionId);
3308
- reject(new Error(`Timed out waiting for permission reply: ${permissionId}`));
3309
- }, 120_000);
3310
- session.pendingPermissionReplies.set(permissionId, {
3311
- resolve,
3312
- reject,
3313
- timer,
3314
- });
3315
- const permissionRequest = {
3316
- permissionId,
3317
- description: typeof params.description === "string"
3318
- ? params.description
3319
- : undefined,
3320
- params,
3321
- };
3322
- for (const handler of session.permissionHandlers) {
3323
- handler(permissionRequest);
3324
- }
3325
- });
3326
- }
3327
- catch {
3328
- return "reject";
3329
- }
3330
- }
3331
- /**
3332
- * Gracefully destroy a session: cancel any pending work, close the client,
3333
- * and remove from tracking. Unlike close() which is abrupt, this attempts
3334
- * a graceful shutdown sequence.
3335
- */
3336
- async destroySession(sessionId) {
3337
- this._requireSession(sessionId);
3338
- try {
3339
- await this.cancelSession(sessionId);
3034
+ onSessionEvent(sessionIdOrHandler, maybeHandler) {
3035
+ const sessionId = typeof sessionIdOrHandler === "string" ? sessionIdOrHandler : "main";
3036
+ const handler = typeof sessionIdOrHandler === "function"
3037
+ ? sessionIdOrHandler
3038
+ : maybeHandler;
3039
+ if (!handler) {
3040
+ throw new TypeError("onSessionEvent requires a handler");
3340
3041
  }
3341
- catch {
3342
- // Ignore cancellation failures during teardown.
3343
- }
3344
- await this._closeSessionInternal(sessionId);
3345
- }
3346
- // ── Flat session API (ID-based) ───────────────────────────────
3347
- async prompt(sessionId, text) {
3348
- const session = this._requireSession(sessionId);
3349
- let agentText = "";
3350
- const handler = (event) => {
3351
- const params = toRecord(event.params);
3352
- const update = toRecord(params.update);
3353
- if (update?.sessionUpdate === "agent_message_chunk") {
3354
- const content = toRecord(update.content);
3355
- if (typeof content.text === "string") {
3356
- agentText += content.text;
3357
- }
3042
+ const handlers = this._durableSessionEventHandlers.get(sessionId) ?? new Set();
3043
+ handlers.add(handler);
3044
+ this._durableSessionEventHandlers.set(sessionId, handlers);
3045
+ return () => {
3046
+ handlers.delete(handler);
3047
+ if (handlers.size === 0) {
3048
+ this._durableSessionEventHandlers.delete(sessionId);
3358
3049
  }
3359
3050
  };
3360
- const unsubscribe = this._subscribeSessionEvents(session, handler);
3361
- try {
3362
- const response = await this._sendSessionRequest(sessionId, "session/prompt", {
3363
- prompt: [{ type: "text", text }],
3364
- });
3365
- return { response, text: agentText };
3366
- }
3367
- finally {
3368
- unsubscribe();
3369
- }
3370
- }
3371
- /** Cancel ongoing agent work for a session. */
3372
- async cancelSession(sessionId) {
3373
- this._requireSession(sessionId);
3374
- const cancelledPendingPrompt = this._cancelPendingPromptRequests(sessionId);
3375
- if (cancelledPendingPrompt) {
3376
- // Session control requests share the same framed sidecar transport as an
3377
- // in-flight prompt request. If the adapter is blocked in prompt I/O, a
3378
- // synchronous cancel RPC can wedge behind that prompt until the transport
3379
- // timeout fires. Resolve the local prompt immediately, then let the sidecar
3380
- // cancellation continue in the background as best effort.
3381
- void this._sendSessionRequest(sessionId, "session/cancel").catch(() => { });
3382
- return {
3383
- jsonrpc: "2.0",
3384
- id: null,
3385
- result: {
3386
- cancelled: true,
3387
- requested: true,
3388
- via: "prompt-fallback",
3389
- },
3390
- };
3391
- }
3392
- const response = await this._sendSessionRequest(sessionId, "session/cancel");
3393
- if (response.error?.code === -32601) {
3394
- return {
3395
- jsonrpc: "2.0",
3396
- id: null,
3397
- result: {
3398
- cancelled: false,
3399
- requested: true,
3400
- via: "unsupported",
3401
- },
3402
- };
3403
- }
3404
- return response;
3405
3051
  }
3406
- closeSession(sessionId) {
3407
- if (!this._sessions.has(sessionId) &&
3408
- !this._closedSessionIds.has(sessionId) &&
3409
- !this._sessionClosePromises.has(sessionId)) {
3410
- throw new Error(`Session not found: ${sessionId}`);
3411
- }
3412
- const closePromise = this._closeSessionInternal(sessionId);
3413
- // `closeSession()` is intentionally fire-and-forget; suppress unhandled
3414
- // rejections here and let tracked close promises surface errors to any
3415
- // internal/test callers awaiting `_sessionClosePromises`.
3416
- void closePromise.catch(() => { });
3417
- }
3418
- async respondPermission(sessionId, permissionId, reply) {
3419
- const session = this._requireSession(sessionId);
3420
- const pendingReply = session.pendingPermissionReplies.get(permissionId);
3421
- if (pendingReply) {
3422
- session.pendingPermissionReplies.delete(permissionId);
3423
- clearTimeout(pendingReply.timer);
3424
- pendingReply.resolve(reply);
3425
- return {
3426
- jsonrpc: "2.0",
3427
- id: null,
3428
- result: {
3429
- permissionId,
3430
- reply,
3431
- via: "sidecar-request",
3432
- },
3433
- };
3434
- }
3435
- return this._sendSessionRequest(sessionId, LEGACY_PERMISSION_METHOD, {
3436
- permissionId,
3437
- reply,
3438
- });
3439
- }
3440
- async setSessionMode(sessionId, modeId) {
3441
- return this._sendSessionRequest(sessionId, "session/set_mode", {
3442
- modeId,
3443
- });
3444
- }
3445
- getSessionModes(sessionId) {
3446
- return this._requireSession(sessionId).modes;
3447
- }
3448
- async setSessionModel(sessionId, model) {
3449
- return this._setSessionConfigByCategory(sessionId, "model", model);
3450
- }
3451
- async setSessionThoughtLevel(sessionId, level) {
3452
- return this._setSessionConfigByCategory(sessionId, "thought_level", level);
3453
- }
3454
- getSessionConfigOptions(sessionId) {
3455
- return [...this._requireSession(sessionId).configOptions];
3456
- }
3457
- getSessionCapabilities(sessionId) {
3458
- const caps = this._requireSession(sessionId).capabilities;
3459
- return Object.keys(caps).length > 0 ? caps : null;
3460
- }
3461
- getSessionAgentInfo(sessionId) {
3462
- return this._requireSession(sessionId).agentInfo;
3463
- }
3464
- async rawSessionSend(sessionId, method, params) {
3465
- return this._sendSessionRequest(sessionId, method, params);
3466
- }
3467
- async rawSend(sessionId, method, params) {
3468
- return this.rawSessionSend(sessionId, method, params);
3469
- }
3470
- onSessionEvent(sessionId, handler) {
3471
- const session = this._requireSession(sessionId);
3472
- return this._subscribeSessionEvents(session, handler);
3473
- }
3474
- onPermissionRequest(sessionId, handler) {
3475
- const session = this._requireSession(sessionId);
3476
- session.permissionHandlers.add(handler);
3052
+ onAgentExit(sessionIdOrHandler, maybeHandler) {
3053
+ const sessionId = typeof sessionIdOrHandler === "string" ? sessionIdOrHandler : "*";
3054
+ const handler = typeof sessionIdOrHandler === "function"
3055
+ ? sessionIdOrHandler
3056
+ : maybeHandler;
3057
+ if (!handler)
3058
+ throw new TypeError("onAgentExit requires a handler");
3059
+ const handlers = this._agentExitHandlers.get(sessionId) ?? new Set();
3060
+ handlers.add(handler);
3061
+ this._agentExitHandlers.set(sessionId, handlers);
3477
3062
  return () => {
3478
- session.permissionHandlers.delete(handler);
3063
+ handlers.delete(handler);
3064
+ if (handlers.size === 0)
3065
+ this._agentExitHandlers.delete(sessionId);
3479
3066
  };
3480
3067
  }
3481
3068
  // ── Cron ────────────────────────────────────────────────────
@@ -3497,9 +3084,6 @@ export class AgentOs {
3497
3084
  }
3498
3085
  async dispose() {
3499
3086
  this._cronManager.dispose();
3500
- for (const sessionId of [...this._sessions.keys()]) {
3501
- await this._closeSessionInternal(sessionId).catch(() => { });
3502
- }
3503
3087
  for (const [id, entry] of this._shells) {
3504
3088
  entry.handle.kill();
3505
3089
  }
@@ -3583,7 +3167,17 @@ function applySharedSidecarHold(state) {
3583
3167
  if (!child)
3584
3168
  return;
3585
3169
  const hold = (state.eventLoopHolds ?? 0) > 0;
3586
- for (const handle of [child, child.stdin, child.stdout, child.stderr]) {
3170
+ // Include the complete stdio array, not just the named fd0-fd2 aliases.
3171
+ // Protocol v8 carries responses over fd3; leaving that control socket
3172
+ // referenced pins Node's event loop after the final VM lease is disposed.
3173
+ const handles = new Set([
3174
+ child,
3175
+ child.stdin,
3176
+ child.stdout,
3177
+ child.stderr,
3178
+ ...(child.stdio ?? []),
3179
+ ]);
3180
+ for (const handle of handles) {
3587
3181
  if (!handle)
3588
3182
  continue;
3589
3183
  try {
@@ -3790,7 +3384,7 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
3790
3384
  }
3791
3385
  let transport;
3792
3386
  const client = createAgentOsSidecarClient({
3793
- async createSessionTransport(sessionBootstrap) {
3387
+ async createOwnershipTransport(sessionBootstrap) {
3794
3388
  transport = await createInProcessSidecarTransport(sessionBootstrap, options);
3795
3389
  return transport;
3796
3390
  },
@@ -3810,7 +3404,7 @@ async function leaseAgentOsSidecarVm(sidecar, options) {
3810
3404
  let disposed = false;
3811
3405
  let leaseRecord;
3812
3406
  try {
3813
- const session = await client.createSession({
3407
+ const session = await client.createOwnershipSession({
3814
3408
  placement: cloneSidecarPlacement(state.description.placement),
3815
3409
  });
3816
3410
  const vm = await session.createVm();