@rivet-dev/agentos-core 0.0.0-nathan-agentos-combined.e833bb7 → 0.2.0

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.
@@ -378,6 +378,7 @@ export declare class AgentOs {
378
378
  private _hostMounts;
379
379
  private _env;
380
380
  private _rootFilesystem;
381
+ private readonly _additionalInstructions;
381
382
  private _sidecarLease;
382
383
  private readonly _sidecarClient;
383
384
  private readonly _sidecarSession;
package/dist/agent-os.js CHANGED
@@ -50,7 +50,7 @@ import { snapshotVirtualFilesystem, sortFilesystemEntries, } from "./filesystem-
50
50
  import { createHostDirBackend } from "./host-dir-mount.js";
51
51
  import { serializeMountConfigForSidecar, } from "./js-bridge.js";
52
52
  import { createSnapshotExport, } from "./layers.js";
53
- import { processSoftware, resolvePackageDir, } from "./packages.js";
53
+ import { processSoftware, resolveAgentSnapshotBundle, resolvePackageDir, } from "./packages.js";
54
54
  import { allowAll } from "./runtime-compat.js";
55
55
  import { AcpRuntimeKind, decodeAcpCallback, decodeAcpEvent, decodeAcpResponse, encodeAcpCallbackResponse, encodeAcpRequest, } from "./sidecar/agentos-protocol.js";
56
56
  import { serializePermissionsForSidecar } from "./sidecar/permissions.js";
@@ -802,6 +802,16 @@ function collectSidecarMountPlan(options) {
802
802
  }
803
803
  for (const mount of options.mounts ?? []) {
804
804
  if (!isNativeMountConfig(mount)) {
805
+ sidecarMounts.push({
806
+ guestPath: mount.path,
807
+ readOnly: isOverlayMountConfig(mount)
808
+ ? (mount.filesystem.mode ?? "ephemeral") === "read-only"
809
+ : (mount.readOnly ?? false),
810
+ plugin: {
811
+ id: "js_bridge",
812
+ config: {},
813
+ },
814
+ });
805
815
  continue;
806
816
  }
807
817
  pushMount(mount);
@@ -1044,7 +1054,7 @@ async function handleHostCallback(request, context) {
1044
1054
  return {
1045
1055
  type: "host_callback_result",
1046
1056
  invocation_id: payload.invocation_id,
1047
- error: `EACCES: blocked by tool.invoke policy for ${payload.callback_key}`,
1057
+ error: `EACCES: blocked by binding.invoke policy for ${payload.callback_key}`,
1048
1058
  };
1049
1059
  }
1050
1060
  const parsed = tool.inputSchema.safeParse(payload.input);
@@ -1079,6 +1089,134 @@ function buildToolMap(toolKits) {
1079
1089
  }
1080
1090
  return toolMap;
1081
1091
  }
1092
+ function bridgeErrorMessage(error) {
1093
+ return error instanceof Error ? error.message : String(error);
1094
+ }
1095
+ function toBridgeArgs(value) {
1096
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1097
+ throw new Error("js_bridge args must be an object");
1098
+ }
1099
+ return value;
1100
+ }
1101
+ function bridgePath(mountId, value) {
1102
+ if (!mountId.startsWith("/")) {
1103
+ throw new Error(`Unsupported js_bridge mount id: ${mountId}`);
1104
+ }
1105
+ if (typeof value !== "string") {
1106
+ throw new Error("js_bridge path argument must be a string");
1107
+ }
1108
+ return posixPath.normalize(posixPath.join(mountId, value));
1109
+ }
1110
+ function requireBridgeNumber(value, field) {
1111
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1112
+ throw new Error(`js_bridge args.${field} must be a number`);
1113
+ }
1114
+ return value;
1115
+ }
1116
+ function decodeBridgeBytes(value, field) {
1117
+ if (typeof value === "string") {
1118
+ return new Uint8Array(Buffer.from(value, "base64"));
1119
+ }
1120
+ if (Array.isArray(value) &&
1121
+ value.every((entry) => Number.isInteger(entry) && entry >= 0 && entry <= 255)) {
1122
+ return new Uint8Array(value);
1123
+ }
1124
+ throw new Error(`js_bridge args.${field} must be base64 bytes`);
1125
+ }
1126
+ async function handleJsBridgeCall(request, context) {
1127
+ try {
1128
+ const args = toBridgeArgs(request.args);
1129
+ const fs = context.filesystem;
1130
+ const path = () => bridgePath(request.mount_id, args.path);
1131
+ let result;
1132
+ switch (request.operation) {
1133
+ case "readFile":
1134
+ result = Buffer.from(await fs.readFile(path())).toString("base64");
1135
+ break;
1136
+ case "readDir":
1137
+ result = await fs.readDir(path());
1138
+ break;
1139
+ case "readDirWithTypes":
1140
+ result = await fs.readDirWithTypes(path());
1141
+ break;
1142
+ case "writeFile":
1143
+ await fs.writeFile(path(), decodeBridgeBytes(args.content, "content"));
1144
+ break;
1145
+ case "createDir":
1146
+ await fs.createDir(path());
1147
+ break;
1148
+ case "mkdir":
1149
+ await fs.mkdir(path(), { recursive: args.recursive !== false });
1150
+ break;
1151
+ case "exists":
1152
+ result = await fs.exists(path());
1153
+ break;
1154
+ case "stat":
1155
+ result = await fs.stat(path());
1156
+ break;
1157
+ case "removeFile":
1158
+ await fs.removeFile(path());
1159
+ break;
1160
+ case "removeDir":
1161
+ await fs.removeDir(path());
1162
+ break;
1163
+ case "rename":
1164
+ await fs.rename(bridgePath(request.mount_id, args.oldPath), bridgePath(request.mount_id, args.newPath));
1165
+ break;
1166
+ case "realpath":
1167
+ result = await fs.realpath(path());
1168
+ break;
1169
+ case "symlink": {
1170
+ if (typeof args.target !== "string") {
1171
+ throw new Error("js_bridge args.target must be a string");
1172
+ }
1173
+ await fs.symlink(args.target, bridgePath(request.mount_id, args.linkPath));
1174
+ break;
1175
+ }
1176
+ case "readlink":
1177
+ result = await fs.readlink(path());
1178
+ break;
1179
+ case "lstat":
1180
+ result = await fs.lstat(path());
1181
+ break;
1182
+ case "link":
1183
+ await fs.link(bridgePath(request.mount_id, args.oldPath), bridgePath(request.mount_id, args.newPath));
1184
+ break;
1185
+ case "chmod":
1186
+ await fs.chmod(path(), requireBridgeNumber(args.mode, "mode"));
1187
+ break;
1188
+ case "chown":
1189
+ await fs.chown(path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"));
1190
+ break;
1191
+ case "utimes":
1192
+ await fs.utimes(path(), requireBridgeNumber(args.atimeMs, "atimeMs"), requireBridgeNumber(args.mtimeMs, "mtimeMs"));
1193
+ break;
1194
+ case "truncate":
1195
+ await fs.truncate(path(), requireBridgeNumber(args.length, "length"));
1196
+ break;
1197
+ case "pread":
1198
+ result = Buffer.from(await fs.pread(path(), requireBridgeNumber(args.offset, "offset"), requireBridgeNumber(args.length, "length"))).toString("base64");
1199
+ break;
1200
+ case "pwrite":
1201
+ await fs.pwrite(path(), requireBridgeNumber(args.offset, "offset"), decodeBridgeBytes(args.content, "content"));
1202
+ break;
1203
+ default:
1204
+ throw new Error(`Unsupported js_bridge operation: ${request.operation}`);
1205
+ }
1206
+ return {
1207
+ type: "js_bridge_result",
1208
+ call_id: request.call_id,
1209
+ ...(result === undefined ? {} : { result }),
1210
+ };
1211
+ }
1212
+ catch (error) {
1213
+ return {
1214
+ type: "js_bridge_result",
1215
+ call_id: request.call_id,
1216
+ error: bridgeErrorMessage(error),
1217
+ };
1218
+ }
1219
+ }
1082
1220
  function parseHostCommandCallbackInput(input) {
1083
1221
  const value = asRecord(input);
1084
1222
  if (value.type !== "command" ||
@@ -1159,7 +1297,7 @@ async function invokeHostTool({ toolKit, toolName, args, cwd, context, }) {
1159
1297
  const callbackKey = `${toolKit.name}:${toolName}`;
1160
1298
  const permissionMode = toolPermissionMode(context.permissions, callbackKey);
1161
1299
  if (permissionMode !== "allow") {
1162
- throw new Error(`EACCES: blocked by tool.invoke policy for ${callbackKey}`);
1300
+ throw new Error(`EACCES: blocked by binding.invoke policy for ${callbackKey}`);
1163
1301
  }
1164
1302
  const input = await parseHostToolInput(tool, args, cwd, context.readFile);
1165
1303
  return executeHostTool(tool, callbackKey, input);
@@ -1315,7 +1453,7 @@ function describeToolPayload(toolKit, toolName) {
1315
1453
  };
1316
1454
  }
1317
1455
  function toolPermissionMode(permissions, callbackKey) {
1318
- const scope = permissions.tool;
1456
+ const scope = permissions.binding;
1319
1457
  if (!scope) {
1320
1458
  return "deny";
1321
1459
  }
@@ -1403,13 +1541,14 @@ export class AgentOs {
1403
1541
  _hostMounts;
1404
1542
  _env;
1405
1543
  _rootFilesystem;
1544
+ _additionalInstructions;
1406
1545
  _sidecarLease = null;
1407
1546
  _sidecarClient;
1408
1547
  _sidecarSession;
1409
1548
  _sidecarVm;
1410
1549
  _disposeSidecarEventListener;
1411
1550
  _agentStderrHandler;
1412
- constructor(kernel, sidecar, softwareRoots, softwareAgentConfigs, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, agentStderrHandler) {
1551
+ constructor(kernel, sidecar, softwareRoots, softwareAgentConfigs, hostMounts, env, rootFilesystem, sidecarClient, sidecarSession, sidecarVm, additionalInstructions, agentStderrHandler) {
1413
1552
  this.#kernel = kernel;
1414
1553
  this.sidecar = sidecar;
1415
1554
  this._softwareRoots = softwareRoots;
@@ -1420,6 +1559,7 @@ export class AgentOs {
1420
1559
  this._sidecarClient = sidecarClient;
1421
1560
  this._sidecarSession = sidecarSession;
1422
1561
  this._sidecarVm = sidecarVm;
1562
+ this._additionalInstructions = additionalInstructions;
1423
1563
  this._agentStderrHandler = agentStderrHandler;
1424
1564
  this._disposeSidecarEventListener = this._sidecarClient.onEvent((event) => {
1425
1565
  this._handleSidecarEvent(event);
@@ -1443,11 +1583,17 @@ export class AgentOs {
1443
1583
  ? (options.software ?? [])
1444
1584
  : [commonSoftware, ...(options?.software ?? [])];
1445
1585
  const processed = processSoftware(software);
1586
+ // Agent-SDK snapshot bundle (loaded once per sidecar into the V8 startup
1587
+ // snapshot, reused across sessions) for any snapshot-enabled agent.
1588
+ const snapshotUserlandCode = resolveAgentSnapshotBundle(software);
1446
1589
  const localMounts = await resolveCompatLocalMounts(options?.mounts);
1447
1590
  const toolKits = options?.toolKits;
1448
1591
  if (toolKits && toolKits.length > 0) {
1449
1592
  validateToolkits(toolKits);
1450
1593
  }
1594
+ // Resolve the sidecar handle up front so every VM created here leases the
1595
+ // one shared native sidecar process owned by that handle.
1596
+ const sidecar = resolveAgentOsSidecar(options?.sidecar);
1451
1597
  const createVmAdmin = async () => {
1452
1598
  const preparedCommandDirs = prepareCommandDirs(processed.commandPackages);
1453
1599
  const toolBootstrapCommands = collectToolkitBootstrapCommands(toolKits ?? []);
@@ -1460,6 +1606,8 @@ export class AgentOs {
1460
1606
  let rootBridge = null;
1461
1607
  let kernel = null;
1462
1608
  let client = null;
1609
+ let createdNativeVm = null;
1610
+ let nativeSession = null;
1463
1611
  let toolShimDir = null;
1464
1612
  let cleanedUp = false;
1465
1613
  const cleanup = async () => {
@@ -1498,16 +1646,15 @@ export class AgentOs {
1498
1646
  commandDirs: preparedCommandDirs.commandDirs,
1499
1647
  shimDir: toolShimDir,
1500
1648
  });
1501
- client = SidecarProcess.spawn({
1502
- cwd: REPO_ROOT,
1503
- command: ensureNativeSidecarBinary(),
1504
- args: [],
1505
- frameTimeoutMs: NATIVE_SIDECAR_FRAME_TIMEOUT_MS,
1506
- });
1507
- const session = await client.authenticateAndOpenSession();
1649
+ // Reuse the sidecar handle's single shared native process; this VM
1650
+ // becomes another tenant of it rather than spawning its own process.
1651
+ const shared = await ensureSharedSidecarNativeProcess(sidecar);
1652
+ client = shared.client;
1653
+ const session = shared.session;
1654
+ nativeSession = session;
1508
1655
  const hostPermissions = options?.permissions ?? {
1509
1656
  ...allowAll,
1510
- tool: "allow",
1657
+ binding: "allow",
1511
1658
  };
1512
1659
  const sidecarPermissions = serializePermissionsForSidecar(hostPermissions);
1513
1660
  const createVmConfig = {
@@ -1522,12 +1669,18 @@ export class AgentOs {
1522
1669
  // module resolution keep their engine defaults (full Node
1523
1670
  // emulation), matching the prior behavior where Agent OS only
1524
1671
  // constrained the builtin allow-list.
1525
- ...(options?.allowedNodeBuiltins !== undefined
1672
+ ...(options?.allowedNodeBuiltins !== undefined ||
1673
+ snapshotUserlandCode !== undefined
1526
1674
  ? {
1527
1675
  jsRuntime: {
1528
1676
  platform: "node",
1529
1677
  moduleResolution: "node",
1530
- allowedBuiltins: options.allowedNodeBuiltins,
1678
+ ...(options?.allowedNodeBuiltins !== undefined
1679
+ ? { allowedBuiltins: options.allowedNodeBuiltins }
1680
+ : {}),
1681
+ ...(snapshotUserlandCode !== undefined
1682
+ ? { snapshotUserlandCode }
1683
+ : {}),
1531
1684
  },
1532
1685
  }
1533
1686
  : {}),
@@ -1536,8 +1689,13 @@ export class AgentOs {
1536
1689
  runtime: "java_script",
1537
1690
  config: createVmConfig,
1538
1691
  });
1692
+ createdNativeVm = nativeVm;
1693
+ // Scope the readiness wait to THIS VM's ownership; on a shared process
1694
+ // other VMs are emitting their own lifecycle events concurrently.
1539
1695
  await client.waitForEvent((event) => event.payload.type === "vm_lifecycle" &&
1540
- event.payload.state === "ready", 10_000);
1696
+ event.payload.state === "ready" &&
1697
+ event.ownership.scope === "vm" &&
1698
+ event.ownership.vm_id === nativeVm.vmId, 10_000);
1541
1699
  await client.configureVm(session, nativeVm, {
1542
1700
  mounts: sidecarMounts,
1543
1701
  permissions: sidecarPermissions,
@@ -1558,8 +1716,15 @@ export class AgentOs {
1558
1716
  env,
1559
1717
  cwd: "/workspace",
1560
1718
  localMounts,
1719
+ sidecarMounts,
1720
+ permissions: sidecarPermissions,
1721
+ commandPermissions: processed.commandPermissions,
1722
+ loopbackExemptPorts: options?.loopbackExemptPorts,
1561
1723
  commandGuestPaths,
1562
1724
  onDispose: cleanup,
1725
+ // The native process is owned by the AgentOsSidecar handle and
1726
+ // shared across VMs; disposing this VM must not kill the process.
1727
+ ownsClient: false,
1563
1728
  });
1564
1729
  await bootstrapLiveBootstrapDirectories(client, session, nativeVm, options?.rootFilesystem);
1565
1730
  kernel = rootBridge;
@@ -1569,6 +1734,10 @@ export class AgentOs {
1569
1734
  hostMounts,
1570
1735
  kernel,
1571
1736
  rootView: rootBridge.createRootView(),
1737
+ sidecarMounts,
1738
+ sidecarPermissions,
1739
+ commandPermissions: processed.commandPermissions,
1740
+ loopbackExemptPorts: options?.loopbackExemptPorts,
1572
1741
  sidecarClient: client,
1573
1742
  sidecarSession: session,
1574
1743
  sidecarVm: nativeVm,
@@ -1593,6 +1762,8 @@ export class AgentOs {
1593
1762
  };
1594
1763
  }
1595
1764
  catch (error) {
1765
+ // The native process is shared and owned by the sidecar handle, so
1766
+ // never dispose the client here — only tear down this VM's resources.
1596
1767
  if (kernel) {
1597
1768
  await kernel.dispose().catch(() => { });
1598
1769
  }
@@ -1600,20 +1771,23 @@ export class AgentOs {
1600
1771
  await rootBridge.dispose().catch(() => { });
1601
1772
  }
1602
1773
  else {
1603
- await client?.dispose().catch(() => { });
1774
+ if (createdNativeVm && nativeSession && client) {
1775
+ await client
1776
+ .disposeVm(nativeSession, createdNativeVm)
1777
+ .catch(() => { });
1778
+ }
1604
1779
  await cleanup();
1605
1780
  }
1606
1781
  throw error;
1607
1782
  }
1608
1783
  };
1609
- const sidecar = resolveAgentOsSidecar(options?.sidecar);
1610
1784
  let sidecarLease = null;
1611
1785
  try {
1612
1786
  sidecarLease = await leaseAgentOsSidecarVm(sidecar, {
1613
1787
  createVm: async () => createVmAdmin(),
1614
1788
  });
1615
1789
  const vmAdmin = sidecarLease.admin;
1616
- const vm = new AgentOs(vmAdmin.kernel, sidecar, processed.softwareRoots, processed.agentConfigs, vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.onAgentStderr ?? defaultAgentStderrHandler);
1790
+ const vm = new AgentOs(vmAdmin.kernel, sidecar, processed.softwareRoots, processed.agentConfigs, vmAdmin.hostMounts, vmAdmin.env, vmAdmin.rootView, vmAdmin.sidecarClient, vmAdmin.sidecarSession, vmAdmin.sidecarVm, options?.additionalInstructions, options?.onAgentStderr ?? defaultAgentStderrHandler);
1617
1791
  vm._sidecarLease = sidecarLease;
1618
1792
  vm._toolKits = vmAdmin.toolKits;
1619
1793
  vm._toolReference = vmAdmin.toolReference;
@@ -1929,7 +2103,7 @@ export class AgentOs {
1929
2103
  }
1930
2104
  async delete(path, options) {
1931
2105
  this._assertSafeAbsolutePath(path);
1932
- const s = await this.#kernel.stat(path);
2106
+ const s = await this._vfs().lstat(path);
1933
2107
  if (s.isDirectory) {
1934
2108
  if (options?.recursive) {
1935
2109
  const entries = await this.#kernel.readdir(path);
@@ -2585,7 +2759,10 @@ export class AgentOs {
2585
2759
  mcpServers: JSON.stringify(options?.mcpServers ?? []),
2586
2760
  protocolVersion: ACP_PROTOCOL_VERSION,
2587
2761
  clientCapabilities: JSON.stringify(defaultAcpClientCapabilities()),
2588
- additionalInstructions: combineInstructions(options?.additionalInstructions, this._toolReference),
2762
+ additionalInstructions: combineInstructions([this._additionalInstructions, options?.additionalInstructions]
2763
+ .map((part) => part?.trim())
2764
+ .filter((part) => Boolean(part))
2765
+ .join("\n\n") || undefined, this._toolReference),
2589
2766
  skipOsInstructions: options?.skipOsInstructions ?? false,
2590
2767
  },
2591
2768
  });
@@ -2672,6 +2849,10 @@ export class AgentOs {
2672
2849
  }
2673
2850
  const { sessionId: liveSessionId, mode } = response.val;
2674
2851
  // Register + hydrate the live session so subsequent prompts route to it.
2852
+ const existing = this._sessions.get(liveSessionId);
2853
+ if (existing !== undefined && !existing.closed) {
2854
+ throw new Error(`session id collision: ${liveSessionId}`);
2855
+ }
2675
2856
  const session = sessionEntryFromInit(liveSessionId, String(agentType), {});
2676
2857
  this._closedSessionIds.delete(liveSessionId);
2677
2858
  this._sessions.set(liveSessionId, session);
@@ -2755,11 +2936,7 @@ export class AgentOs {
2755
2936
  case "host_callback":
2756
2937
  return handleHostCallback(request, context);
2757
2938
  case "js_bridge_call":
2758
- return Promise.resolve({
2759
- type: "js_bridge_result",
2760
- call_id: request.payload.call_id,
2761
- error: `unsupported sidecar request type: ${request.payload.type}`,
2762
- });
2939
+ return handleJsBridgeCall(request.payload, { filesystem: this.#kernel.vfs });
2763
2940
  case "ext":
2764
2941
  return this._handleAcpExtSidecarRequest(request.payload.envelope);
2765
2942
  }
@@ -3476,6 +3653,42 @@ function resolveAgentOsSidecar(config) {
3476
3653
  }
3477
3654
  const sidecarStates = new WeakMap();
3478
3655
  const sharedSidecars = new Map();
3656
+ /**
3657
+ * Spawn-once accessor for a sidecar handle's shared native process. Concurrent
3658
+ * callers await the same promise, so one `AgentOsSidecar` maps to exactly one
3659
+ * `agent-os-sidecar` OS process for its whole lifetime.
3660
+ */
3661
+ function ensureSharedSidecarNativeProcess(sidecar) {
3662
+ const state = getSidecarState(sidecar);
3663
+ if (!state.nativeProcess) {
3664
+ state.nativeProcess = (async () => {
3665
+ const client = SidecarProcess.spawn({
3666
+ cwd: REPO_ROOT,
3667
+ command: ensureNativeSidecarBinary(),
3668
+ args: [],
3669
+ frameTimeoutMs: NATIVE_SIDECAR_FRAME_TIMEOUT_MS,
3670
+ });
3671
+ const session = await client.authenticateAndOpenSession();
3672
+ return { client, session };
3673
+ })();
3674
+ }
3675
+ return state.nativeProcess;
3676
+ }
3677
+ /** Dispose a sidecar handle's shared native process, if one was spawned. */
3678
+ async function disposeSharedSidecarNativeProcess(state) {
3679
+ const pending = state.nativeProcess;
3680
+ if (!pending) {
3681
+ return;
3682
+ }
3683
+ state.nativeProcess = undefined;
3684
+ try {
3685
+ const { client } = await pending;
3686
+ await client.dispose();
3687
+ }
3688
+ catch {
3689
+ // Process may have already exited; nothing to reclaim.
3690
+ }
3691
+ }
3479
3692
  export class AgentOsSidecar {
3480
3693
  constructor(sidecarId, placement, sharedPool) {
3481
3694
  sidecarStates.set(this, {
@@ -3510,6 +3723,8 @@ export class AgentOsSidecar {
3510
3723
  }
3511
3724
  state.activeLeases.clear();
3512
3725
  state.description.activeVmCount = 0;
3726
+ // Tear down the shared native process after all leased VMs are gone.
3727
+ await disposeSharedSidecarNativeProcess(state);
3513
3728
  state.description.state = "disposed";
3514
3729
  if (state.sharedPool && sharedSidecars.get(state.sharedPool) === this) {
3515
3730
  sharedSidecars.delete(state.sharedPool);
@@ -8,16 +8,25 @@ export function hostTool(def) {
8
8
  export function toolKit(def) {
9
9
  return def;
10
10
  }
11
+ const TOOLKIT_COMMAND_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
12
+ function validateToolCommandName(kind, name) {
13
+ if (TOOLKIT_COMMAND_NAME_RE.test(name)) {
14
+ return;
15
+ }
16
+ throw new Error(`${kind} name "${name}" must be lowercase alphanumeric with optional single hyphen separators`);
17
+ }
11
18
  /**
12
19
  * Validate all description lengths in the given toolkits.
13
20
  * Throws if any toolkit or tool description exceeds MAX_TOOL_DESCRIPTION_LENGTH.
14
21
  */
15
22
  export function validateToolkits(toolKits) {
16
23
  for (const tk of toolKits) {
24
+ validateToolCommandName("Toolkit", tk.name);
17
25
  if (tk.description.length > MAX_TOOL_DESCRIPTION_LENGTH) {
18
26
  throw new Error(`Toolkit "${tk.name}" description is ${tk.description.length} characters, max is ${MAX_TOOL_DESCRIPTION_LENGTH}`);
19
27
  }
20
28
  for (const [toolName, tool] of Object.entries(tk.tools)) {
29
+ validateToolCommandName("Tool", toolName);
21
30
  if (tool.description.length > MAX_TOOL_DESCRIPTION_LENGTH) {
22
31
  throw new Error(`Tool "${tk.name}/${toolName}" description is ${tool.description.length} characters, max is ${MAX_TOOL_DESCRIPTION_LENGTH}`);
23
32
  }
@@ -87,7 +87,7 @@ export declare const permissionsSchema: z.ZodObject<{
87
87
  patterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
88
88
  }, z.core.$strict>>;
89
89
  }, z.core.$strict>]>>;
90
- tool: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
90
+ binding: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
91
91
  allow: "allow";
92
92
  deny: "deny";
93
93
  }>, z.ZodObject<{
@@ -344,7 +344,7 @@ export declare const agentOsOptionFieldSchemas: {
344
344
  patterns: z.ZodOptional<z.ZodArray<z.ZodString>>;
345
345
  }, z.core.$strict>>;
346
346
  }, z.core.$strict>]>>;
347
- tool: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
347
+ binding: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
348
348
  allow: "allow";
349
349
  deny: "deny";
350
350
  }>, z.ZodObject<{
@@ -42,7 +42,7 @@ export const permissionsSchema = z
42
42
  childProcess: patternPermissionsSchema.optional(),
43
43
  process: patternPermissionsSchema.optional(),
44
44
  env: patternPermissionsSchema.optional(),
45
- tool: patternPermissionsSchema.optional(),
45
+ binding: patternPermissionsSchema.optional(),
46
46
  })
47
47
  .strict();
48
48
  export const agentOsLimitsSchema = z
@@ -25,6 +25,19 @@ export interface AgentSoftwareDescriptor extends SoftwareDescriptor {
25
25
  acpAdapter: string;
26
26
  /** npm package name of the agent CLI. Must be in requires. */
27
27
  agentPackage: string;
28
+ /**
29
+ * Whether to evaluate this agent's SDK into the per-sidecar V8 heap
30
+ * snapshot so it is loaded once per sidecar and reused across sessions
31
+ * (instead of re-evaluated on every `createSession`). Opt-in: the agent's
32
+ * SDK must be **snapshot-safe** — its module-init must not create native
33
+ * (`.node` addon / WASM / External) handles, open fds/sockets, start
34
+ * timers, read per-session config, or leave pending promises. SDKs that
35
+ * are not snapshot-safe (or where snapshot creation fails) automatically
36
+ * fall back to the per-session dynamic-import path, so this flag is a
37
+ * safety/performance switch, not a correctness requirement. Defaults to
38
+ * `false`. See the Custom Agents / dependencies docs for the full rules.
39
+ */
40
+ snapshot?: boolean;
28
41
  /** Static env vars passed when spawning the adapter. */
29
42
  staticEnv?: Record<string, string>;
30
43
  /** Dynamic env vars computed at boot time. */
@@ -103,6 +116,15 @@ export interface CommandPackageMetadata {
103
116
  * validates the descriptor shape at compile time.
104
117
  */
105
118
  export declare function defineSoftware<T extends AnySoftwareDescriptor>(desc: T): T;
119
+ /**
120
+ * Resolve the agent-SDK snapshot bundle (an esbuild IIFE at
121
+ * `<packageDir>/dist/sdk-snapshot.js`) for the first snapshot-enabled agent in
122
+ * the software set. Returns its source so it can be evaluated once into the
123
+ * per-sidecar V8 startup snapshot (`jsRuntime.snapshotUserlandCode`) and reused
124
+ * across sessions. Returns `undefined` when no agent opts in (`agent.snapshot`)
125
+ * or the bundle is absent — the runtime then keeps the per-session import path.
126
+ */
127
+ export declare function resolveAgentSnapshotBundle(software: SoftwareInput[]): string | undefined;
106
128
  /** Result of processing all software descriptors at boot time. */
107
129
  export interface ProcessedSoftware {
108
130
  /** WASM command directories to pass to the WasmVM driver. */
package/dist/packages.js CHANGED
@@ -118,6 +118,29 @@ function createSoftwareContext(packageDir, requires) {
118
118
  export function defineSoftware(desc) {
119
119
  return desc;
120
120
  }
121
+ /**
122
+ * Resolve the agent-SDK snapshot bundle (an esbuild IIFE at
123
+ * `<packageDir>/dist/sdk-snapshot.js`) for the first snapshot-enabled agent in
124
+ * the software set. Returns its source so it can be evaluated once into the
125
+ * per-sidecar V8 startup snapshot (`jsRuntime.snapshotUserlandCode`) and reused
126
+ * across sessions. Returns `undefined` when no agent opts in (`agent.snapshot`)
127
+ * or the bundle is absent — the runtime then keeps the per-session import path.
128
+ */
129
+ export function resolveAgentSnapshotBundle(software) {
130
+ const descriptors = software.flat();
131
+ for (const desc of descriptors) {
132
+ if (desc.type !== "agent")
133
+ continue;
134
+ const agent = desc;
135
+ if (!agent.agent.snapshot)
136
+ continue;
137
+ const bundlePath = join(agent.packageDir, "dist", "sdk-snapshot.js");
138
+ if (existsSync(bundlePath)) {
139
+ return readFileSync(bundlePath, "utf-8");
140
+ }
141
+ }
142
+ return undefined;
143
+ }
121
144
  /** Check if a descriptor is a typed software descriptor (has a `type` field). */
122
145
  function isTypedDescriptor(desc) {
123
146
  return ("type" in desc && typeof desc.type === "string");
@@ -270,6 +293,10 @@ export function processSoftware(software) {
270
293
  if (!isTypedDescriptor(pkg)) {
271
294
  // Duck-typed: any object with commandDir is a WASM command source.
272
295
  const commandMetadata = collectCommandMetadata(pkg);
296
+ if (!existsSync(commandMetadata.commandDir)) {
297
+ console.warn(`[agentos] skipping WASM command source with missing commandDir: ${commandMetadata.commandDir} (build its wasm artifacts to enable these commands)`);
298
+ continue;
299
+ }
273
300
  commandDirs.push(commandMetadata.commandDir);
274
301
  commandPackages.push(commandMetadata);
275
302
  collectRegistryPackagePermissions(commandPermissions, pkg);
@@ -278,6 +305,10 @@ export function processSoftware(software) {
278
305
  switch (pkg.type) {
279
306
  case "wasm-commands": {
280
307
  const commandMetadata = collectCommandMetadata(pkg);
308
+ if (!existsSync(commandMetadata.commandDir)) {
309
+ console.warn(`[agentos] skipping WASM command source with missing commandDir: ${commandMetadata.commandDir} (build its wasm artifacts to enable these commands)`);
310
+ break;
311
+ }
281
312
  commandDirs.push(commandMetadata.commandDir);
282
313
  commandPackages.push(commandMetadata);
283
314
  collectTypedDescriptorPermissions(commandPermissions, pkg);
@@ -81,7 +81,7 @@ export type NetworkPermissions = PermissionMode | RulePermissions<PatternPermiss
81
81
  export type ChildProcessPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
82
82
  export type ProcessPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
83
83
  export type EnvPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
84
- export type ToolPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
84
+ export type BindingPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
85
85
  export interface ProcessInfo {
86
86
  pid: number;
87
87
  ppid: number;
@@ -168,7 +168,7 @@ export interface Permissions {
168
168
  childProcess?: ChildProcessPermissions;
169
169
  process?: ProcessPermissions;
170
170
  env?: EnvPermissions;
171
- tool?: ToolPermissions;
171
+ binding?: BindingPermissions;
172
172
  }
173
173
  export interface ResourceBudgets {
174
174
  maxOutputBytes?: number;
@@ -119,7 +119,7 @@ export class InMemoryFileSystem {
119
119
  throw errnoError("ENOENT", `open '${targetPath}'`);
120
120
  }
121
121
  entry.atimeMs = Date.now();
122
- return entry.data;
122
+ return new Uint8Array(entry.data);
123
123
  }
124
124
  async readTextFile(targetPath) {
125
125
  return new TextDecoder().decode(await this.readFile(targetPath));
@@ -152,7 +152,9 @@ export class InMemoryFileSystem {
152
152
  async writeFile(targetPath, content) {
153
153
  const normalized = normalizePath(targetPath);
154
154
  await this.mkdir(dirnameVirtual(normalized), { recursive: true });
155
- const data = typeof content === "string" ? new TextEncoder().encode(content) : content;
155
+ const data = typeof content === "string"
156
+ ? new TextEncoder().encode(content)
157
+ : new Uint8Array(content);
156
158
  const existing = this.entries.get(normalized);
157
159
  if (existing?.type === "file") {
158
160
  existing.data = data;
@@ -212,7 +214,7 @@ export class InMemoryFileSystem {
212
214
  return this.toStat(entry);
213
215
  }
214
216
  async removeFile(targetPath) {
215
- const resolved = this.resolvePath(targetPath);
217
+ const resolved = normalizePath(targetPath);
216
218
  const entry = this.entries.get(resolved);
217
219
  if (!entry || entry.type === "dir") {
218
220
  throw errnoError("ENOENT", `unlink '${targetPath}'`);
@@ -374,7 +376,7 @@ export class InMemoryFileSystem {
374
376
  const nextSize = Math.max(entry.data.length, offset + data.length);
375
377
  const updated = new Uint8Array(nextSize);
376
378
  updated.set(entry.data);
377
- updated.set(data, offset);
379
+ updated.set(new Uint8Array(data), offset);
378
380
  entry.data = updated;
379
381
  entry.mtimeMs = Date.now();
380
382
  entry.ctimeMs = Date.now();
@@ -1607,6 +1609,35 @@ function bindLiveFilesystem(target, getFilesystem) {
1607
1609
  },
1608
1610
  };
1609
1611
  }
1612
+ function serializeLocalCompatMountForSidecar(mount) {
1613
+ return (mount.sidecarMount ??
1614
+ (mount.fs instanceof NodeFileSystem
1615
+ ? serializeMountConfigForSidecar({
1616
+ path: mount.path,
1617
+ readOnly: mount.readOnly,
1618
+ plugin: {
1619
+ id: "host_dir",
1620
+ config: {
1621
+ hostPath: mount.fs.rootPath,
1622
+ readOnly: mount.readOnly,
1623
+ },
1624
+ },
1625
+ })
1626
+ : serializeMountConfigForSidecar({
1627
+ path: mount.path,
1628
+ driver: mount.fs,
1629
+ readOnly: mount.readOnly,
1630
+ })));
1631
+ }
1632
+ function makeLocalCompatMount(options) {
1633
+ const localMount = {
1634
+ path: normalizePath(options.path),
1635
+ fs: options.fs,
1636
+ readOnly: options.readOnly ?? false,
1637
+ };
1638
+ localMount.sidecarMount = serializeLocalCompatMountForSidecar(localMount);
1639
+ return localMount;
1640
+ }
1610
1641
  class NativeKernel {
1611
1642
  options;
1612
1643
  env;
@@ -1646,11 +1677,11 @@ class NativeKernel {
1646
1677
  };
1647
1678
  this.loopbackExemptPorts = [...(options.loopbackExemptPorts ?? [])];
1648
1679
  for (const mount of options.mounts ?? []) {
1649
- this.pendingLocalMounts.push({
1650
- path: normalizePath(mount.path),
1680
+ this.pendingLocalMounts.push(makeLocalCompatMount({
1681
+ path: mount.path,
1651
1682
  fs: mount.fs,
1652
- readOnly: mount.readOnly ?? false,
1653
- });
1683
+ readOnly: mount.readOnly,
1684
+ }));
1654
1685
  }
1655
1686
  this.vfs = new DeferredFileSystem(() => this.rootFilesystem);
1656
1687
  this.liveFilesystemBinding = bindLiveFilesystem(this.options.filesystem, () => this.rootFilesystem);
@@ -1696,23 +1727,7 @@ class NativeKernel {
1696
1727
  },
1697
1728
  },
1698
1729
  }));
1699
- const localMounts = this.pendingLocalMounts.map((mount) => mount.fs instanceof NodeFileSystem
1700
- ? serializeMountConfigForSidecar({
1701
- path: mount.path,
1702
- readOnly: mount.readOnly,
1703
- plugin: {
1704
- id: "host_dir",
1705
- config: {
1706
- hostPath: mount.fs.rootPath,
1707
- readOnly: mount.readOnly,
1708
- },
1709
- },
1710
- })
1711
- : serializeMountConfigForSidecar({
1712
- path: mount.path,
1713
- driver: mount.fs,
1714
- readOnly: mount.readOnly,
1715
- }));
1730
+ const localMounts = this.pendingLocalMounts.map((mount) => serializeLocalCompatMountForSidecar(mount));
1716
1731
  await this.client.configureVm(this.session, this.vm, {
1717
1732
  mounts: [...localMounts, ...sidecarMounts],
1718
1733
  loopbackExemptPorts: this.loopbackExemptPorts,
@@ -1818,17 +1833,27 @@ class NativeKernel {
1818
1833
  return this.proxy.connectTerminal(options);
1819
1834
  }
1820
1835
  mountFs(mountPath, filesystem, options) {
1836
+ const localMount = makeLocalCompatMount({
1837
+ path: mountPath,
1838
+ fs: filesystem,
1839
+ readOnly: options?.readOnly,
1840
+ });
1841
+ this.pendingLocalMounts.unshift(localMount);
1842
+ this.pendingLocalMounts.sort((left, right) => right.path.length - left.path.length);
1821
1843
  if (!this.proxy) {
1822
- this.pendingLocalMounts.push({
1823
- path: normalizePath(mountPath),
1824
- fs: filesystem,
1825
- readOnly: options?.readOnly ?? false,
1826
- });
1827
1844
  return;
1828
1845
  }
1829
- this.proxy.mountFs(mountPath, filesystem, options);
1846
+ this.proxy.mountFs(mountPath, filesystem, {
1847
+ readOnly: localMount.readOnly,
1848
+ sidecarMount: localMount.sidecarMount,
1849
+ });
1830
1850
  }
1831
1851
  unmountFs(mountPath) {
1852
+ const normalized = normalizePath(mountPath);
1853
+ const pendingIndex = this.pendingLocalMounts.findIndex((mount) => mount.path === normalized);
1854
+ if (pendingIndex >= 0) {
1855
+ this.pendingLocalMounts.splice(pendingIndex, 1);
1856
+ }
1832
1857
  this.proxy?.unmountFs(mountPath);
1833
1858
  }
1834
1859
  async readFile(targetPath) {
@@ -1955,33 +1980,23 @@ class NativeKernel {
1955
1980
  await materializeSnapshotEntriesIntoVm(client, session, vm, snapshotEntries);
1956
1981
  }
1957
1982
  if (rootPassthroughPlan.mounts.length > 0) {
1958
- this.pendingLocalMounts.push(...rootPassthroughPlan.mounts);
1983
+ this.pendingLocalMounts.push(...rootPassthroughPlan.mounts.map((mount) => makeLocalCompatMount({
1984
+ path: mount.path,
1985
+ fs: mount.fs,
1986
+ readOnly: mount.readOnly,
1987
+ })));
1959
1988
  }
1960
1989
  if (this.pendingLocalMounts.length > 0 ||
1961
1990
  this.loopbackExemptPorts.length > 0 ||
1962
1991
  requestedPermissions) {
1992
+ const sidecarMounts = this.pendingLocalMounts.map((mount) => serializeLocalCompatMountForSidecar(mount));
1963
1993
  await client.configureVm(session, vm, {
1964
- mounts: this.pendingLocalMounts.map((mount) => mount.fs instanceof NodeFileSystem
1965
- ? serializeMountConfigForSidecar({
1966
- path: mount.path,
1967
- readOnly: mount.readOnly,
1968
- plugin: {
1969
- id: "host_dir",
1970
- config: {
1971
- hostPath: mount.fs.rootPath,
1972
- readOnly: mount.readOnly,
1973
- },
1974
- },
1975
- })
1976
- : serializeMountConfigForSidecar({
1977
- path: mount.path,
1978
- driver: mount.fs,
1979
- readOnly: mount.readOnly,
1980
- })),
1994
+ mounts: sidecarMounts,
1981
1995
  permissions: requestedPermissions,
1982
1996
  loopbackExemptPorts: this.loopbackExemptPorts,
1983
1997
  });
1984
1998
  }
1999
+ const configuredSidecarMounts = this.pendingLocalMounts.map((mount) => serializeLocalCompatMountForSidecar(mount));
1985
2000
  const proxy = new NativeSidecarKernelProxy({
1986
2001
  client,
1987
2002
  session,
@@ -1990,6 +2005,9 @@ class NativeKernel {
1990
2005
  cwd: this.cwd,
1991
2006
  defaultExecCwd: this.options.cwd === undefined ? "/workspace" : this.cwd,
1992
2007
  localMounts: this.pendingLocalMounts,
2008
+ sidecarMounts: configuredSidecarMounts,
2009
+ permissions: requestedPermissions,
2010
+ loopbackExemptPorts: this.loopbackExemptPorts,
1993
2011
  commandGuestPaths: new Map(),
1994
2012
  onWasmCommandResolved: (command) => {
1995
2013
  this.recordModuleExecution(command);
package/dist/runtime.d.ts CHANGED
@@ -76,7 +76,7 @@ export type NetworkPermissions = PermissionMode | RulePermissions<PatternPermiss
76
76
  export type ChildProcessPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
77
77
  export type ProcessPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
78
78
  export type EnvPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
79
- export type ToolPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
79
+ export type BindingPermissions = PermissionMode | RulePermissions<PatternPermissionRule>;
80
80
  export interface ProcessInfo {
81
81
  pid: number;
82
82
  ppid: number;
@@ -163,7 +163,7 @@ export interface Permissions {
163
163
  childProcess?: ChildProcessPermissions;
164
164
  process?: ProcessPermissions;
165
165
  env?: EnvPermissions;
166
- tool?: ToolPermissions;
166
+ binding?: BindingPermissions;
167
167
  }
168
168
  export interface ResourceBudgets {
169
169
  maxOutputBytes?: number;
@@ -1,3 +1,5 @@
1
1
  import "@secure-exec/core/native-client";
2
- export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, SidecarProcess, SidecarProcess as NativeSidecarProcessClient, SidecarEventBufferOverflow, SidecarProcessError, SidecarProcessExited, } from "@secure-exec/core/sidecar-client";
2
+ import { SidecarProcess } from "@secure-exec/core/sidecar-client";
3
+ export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, SidecarEventBufferOverflow, SidecarProcess, SidecarProcessError, SidecarProcessExited, } from "@secure-exec/core/sidecar-client";
4
+ export declare const NativeSidecarProcessClient: typeof SidecarProcess;
3
5
  export type { AuthenticatedSession, CreatedVm, ExtEnvelope, GuestFilesystemStat, RootFilesystemEntry, RootFilesystemLowerDescriptor, SidecarEventSelector, SidecarFsPermissionRule, SidecarMountDescriptor, SidecarMountPluginDescriptor, SidecarPatternPermissionRule, SidecarPermissionMode, SidecarPermissionScope, SidecarPermissionsPolicy, SidecarProcessSnapshotEntry, SidecarProjectedModuleDescriptor, SidecarRegisteredHostCallbackDefinition, SidecarRegisteredHostCallbackExample, SidecarRequestFrame, SidecarRequestHandler, SidecarRequestPayload, SidecarResponseFrame, SidecarResponsePayload, SidecarRulePermissions, SidecarSessionState, SidecarSignalHandlerRegistration, SidecarSignalState, SidecarSocketStateEntry, SidecarSoftwareDescriptor, SidecarSpawnOptions, SidecarSpawnOptions as NativeSidecarSpawnOptions, SidecarZombieTimerCount, } from "@secure-exec/core/sidecar-client";
@@ -3,4 +3,6 @@
3
3
  // separately-registered factory; importing native-client wires it up so
4
4
  // SidecarProcess.spawn works in this native runtime.
5
5
  import "@secure-exec/core/native-client";
6
- export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, SidecarProcess, SidecarProcess as NativeSidecarProcessClient, SidecarEventBufferOverflow, SidecarProcessError, SidecarProcessExited, } from "@secure-exec/core/sidecar-client";
6
+ import { SidecarProcess } from "@secure-exec/core/sidecar-client";
7
+ export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, SidecarEventBufferOverflow, SidecarProcess, SidecarProcessError, SidecarProcessExited, } from "@secure-exec/core/sidecar-client";
8
+ export const NativeSidecarProcessClient = SidecarProcess;
@@ -47,8 +47,8 @@ export function serializePermissionsForSidecar(permissions) {
47
47
  env: typeof permissions.env === "string" || !permissions.env
48
48
  ? permissions.env
49
49
  : serializePatternScope(permissions.env),
50
- binding: typeof permissions.tool === "string" || !permissions.tool
51
- ? permissions.tool
52
- : serializePatternScope(permissions.tool),
50
+ binding: typeof permissions.binding === "string" || !permissions.binding
51
+ ? permissions.binding
52
+ : serializePatternScope(permissions.binding),
53
53
  };
54
54
  }
@@ -2,12 +2,13 @@ import type { RootFilesystemConfig as VmConfigRootFilesystemConfig, RootFilesyst
2
2
  import type { NativeMountConfig, PlainMountConfig, RootFilesystemConfig } from "../agent-os.js";
3
3
  import type { RootSnapshotExport } from "../layers.js";
4
4
  import type { ConnectTerminalOptions, KernelExecOptions, KernelExecResult, KernelSpawnOptions, ManagedProcess, OpenShellOptions, ProcessInfo, ShellHandle, VirtualFileSystem, VirtualStat } from "../runtime-compat.js";
5
- import type { AuthenticatedSession, CreatedVm, SidecarProcess, SidecarSignalHandlerRegistration } from "./native-process-client.js";
5
+ import type { AuthenticatedSession, CreatedVm, SidecarProcess, SidecarPermissionsPolicy, SidecarSignalHandlerRegistration } from "./native-process-client.js";
6
6
  export declare function toSidecarSignalName(signal: number): string;
7
7
  export interface LocalCompatMount {
8
8
  path: string;
9
9
  fs: VirtualFileSystem;
10
10
  readOnly: boolean;
11
+ sidecarMount?: SidecarMountDescriptor;
11
12
  }
12
13
  interface KernelSocketSnapshot {
13
14
  processId: string;
@@ -30,9 +31,21 @@ interface NativeSidecarKernelProxyOptions {
30
31
  cwd: string;
31
32
  defaultExecCwd?: string;
32
33
  localMounts: LocalCompatMount[];
34
+ sidecarMounts: SidecarMountDescriptor[];
35
+ permissions?: SidecarPermissionsPolicy;
36
+ commandPermissions?: Parameters<SidecarProcess["configureVm"]>[2]["commandPermissions"];
37
+ loopbackExemptPorts?: number[];
33
38
  commandGuestPaths: ReadonlyMap<string, string>;
34
39
  onWasmCommandResolved?: (command: string) => void;
35
40
  onDispose?: () => Promise<void>;
41
+ /**
42
+ * Whether this proxy owns the underlying sidecar process. When VMs share one
43
+ * sidecar process (the default for VMs leased from an `AgentOsSidecar`
44
+ * handle), each proxy tears down only its own VM on `dispose()`; the shared
45
+ * process is disposed when the sidecar handle is disposed. Defaults to true
46
+ * for the legacy one-process-per-VM path.
47
+ */
48
+ ownsClient?: boolean;
36
49
  }
37
50
  export declare class NativeSidecarKernelProxy {
38
51
  readonly env: Record<string, string>;
@@ -44,7 +57,12 @@ export declare class NativeSidecarKernelProxy {
44
57
  private readonly client;
45
58
  private readonly session;
46
59
  private readonly vm;
60
+ private readonly ownsClient;
47
61
  private readonly localMounts;
62
+ private readonly baseSidecarMounts;
63
+ private readonly permissions;
64
+ private readonly commandPermissions;
65
+ private readonly loopbackExemptPorts;
48
66
  private readonly commandDrivers;
49
67
  private readonly onWasmCommandResolved;
50
68
  private readonly onDispose;
@@ -62,6 +80,7 @@ export declare class NativeSidecarKernelProxy {
62
80
  private zombieTimerCountRefresh;
63
81
  private disposed;
64
82
  private pumpError;
83
+ private mountReconfigurePromise;
65
84
  private nextSyntheticPid;
66
85
  private readonly eventPumpAbortController;
67
86
  private readonly eventPump;
@@ -86,8 +105,12 @@ export declare class NativeSidecarKernelProxy {
86
105
  rename(oldPath: string, newPath: string): Promise<void>;
87
106
  mountFs(path: string, driver: VirtualFileSystem, options?: {
88
107
  readOnly?: boolean;
108
+ sidecarMount?: SidecarMountDescriptor;
89
109
  }): void;
90
110
  unmountFs(path: string): void;
111
+ private desiredSidecarMounts;
112
+ private reconfigureSidecarMounts;
113
+ private waitForMountReconfigure;
91
114
  snapshotProcesses(): ProcessInfo[];
92
115
  findListener(request: {
93
116
  host?: string;
@@ -125,8 +148,8 @@ export declare class NativeSidecarKernelProxy {
125
148
  private assertLocalWritable;
126
149
  private updateTrackedProcessSnapshot;
127
150
  }
128
- export type { AuthenticatedSession, CreatedVm, GuestFilesystemStat, SidecarSpawnOptions, RootFilesystemEntry, SidecarEventSelector, SidecarPermissionsPolicy, SidecarRegisteredHostCallbackDefinition, SidecarRequestFrame, SidecarResponsePayload, SidecarSessionState, SidecarSignalHandlerRegistration, SidecarSocketStateEntry, } from "./native-process-client.js";
129
- export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, SidecarProcess, SidecarEventBufferOverflow, SidecarProcessError, SidecarProcessExited, } from "./native-process-client.js";
151
+ export type { AuthenticatedSession, CreatedVm, GuestFilesystemStat, RootFilesystemEntry, SidecarEventSelector, SidecarPermissionsPolicy, SidecarRegisteredHostCallbackDefinition, SidecarRequestFrame, SidecarResponsePayload, SidecarSessionState, SidecarSignalHandlerRegistration, SidecarSocketStateEntry, SidecarSpawnOptions, } from "./native-process-client.js";
152
+ export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, NativeSidecarProcessClient, SidecarEventBufferOverflow, SidecarProcess, SidecarProcessError, SidecarProcessExited, } from "./native-process-client.js";
130
153
  export type AgentOsSidecarPlacement = {
131
154
  kind: "shared";
132
155
  pool?: string;
@@ -181,7 +181,12 @@ export class NativeSidecarKernelProxy {
181
181
  client;
182
182
  session;
183
183
  vm;
184
+ ownsClient;
184
185
  localMounts;
186
+ baseSidecarMounts;
187
+ permissions;
188
+ commandPermissions;
189
+ loopbackExemptPorts;
185
190
  commandDrivers;
186
191
  onWasmCommandResolved;
187
192
  onDispose;
@@ -199,6 +204,7 @@ export class NativeSidecarKernelProxy {
199
204
  zombieTimerCountRefresh = null;
200
205
  disposed = false;
201
206
  pumpError = null;
207
+ mountReconfigurePromise = null;
202
208
  nextSyntheticPid = SYNTHETIC_PID_BASE;
203
209
  eventPumpAbortController = new AbortController();
204
210
  eventPump;
@@ -206,10 +212,17 @@ export class NativeSidecarKernelProxy {
206
212
  this.client = options.client;
207
213
  this.session = options.session;
208
214
  this.vm = options.vm;
215
+ this.ownsClient = options.ownsClient ?? true;
209
216
  this.env = { ...options.env };
210
217
  this.cwd = options.cwd;
211
218
  this.defaultExecCwd = options.defaultExecCwd;
212
219
  this.localMounts = [...options.localMounts].sort((left, right) => right.path.length - left.path.length);
220
+ const localMountPaths = new Set(this.localMounts.map((mount) => mount.path));
221
+ this.baseSidecarMounts = options.sidecarMounts.filter((mount) => mount.plugin.id !== "js_bridge" ||
222
+ !localMountPaths.has(posixPath.normalize(mount.guestPath)));
223
+ this.permissions = options.permissions;
224
+ this.commandPermissions = options.commandPermissions;
225
+ this.loopbackExemptPorts = options.loopbackExemptPorts;
213
226
  this.commandDrivers = buildCommandMap(options.commandGuestPaths);
214
227
  this.onWasmCommandResolved = options.onWasmCommandResolved;
215
228
  this.onDispose = options.onDispose;
@@ -239,6 +252,7 @@ export class NativeSidecarKernelProxy {
239
252
  }
240
253
  this.disposed = true;
241
254
  this.eventPumpAbortController.abort();
255
+ await this.mountReconfigurePromise?.catch(() => { });
242
256
  const liveProcesses = [...this.trackedProcesses.values()].filter((entry) => entry.exitCode === null);
243
257
  await Promise.allSettled(liveProcesses.map((entry) => this.signalProcess(entry, 15)));
244
258
  await this.client.disposeVm(this.session, this.vm).catch(() => { });
@@ -251,7 +265,12 @@ export class NativeSidecarKernelProxy {
251
265
  this.finishProcess(entry, 143);
252
266
  }
253
267
  }
254
- await this.client.dispose().catch(() => { });
268
+ // Only tear down the shared sidecar process when this proxy owns it. VMs
269
+ // leased from an `AgentOsSidecar` handle share one process, which is
270
+ // disposed when the handle is disposed.
271
+ if (this.ownsClient) {
272
+ await this.client.dispose().catch(() => { });
273
+ }
255
274
  await this.eventPump.catch(() => { });
256
275
  await this.onDispose?.().catch(() => { });
257
276
  }
@@ -1011,14 +1030,57 @@ export class NativeSidecarKernelProxy {
1011
1030
  path: posixPath.normalize(path),
1012
1031
  fs: driver,
1013
1032
  readOnly: options?.readOnly ?? false,
1033
+ sidecarMount: options?.sidecarMount,
1014
1034
  });
1015
1035
  this.localMounts.sort((left, right) => right.path.length - left.path.length);
1036
+ void this.reconfigureSidecarMounts().catch(() => { });
1016
1037
  }
1017
1038
  unmountFs(path) {
1018
1039
  const normalized = posixPath.normalize(path);
1019
1040
  const index = this.localMounts.findIndex((mount) => mount.path === normalized);
1020
1041
  if (index >= 0) {
1021
1042
  this.localMounts.splice(index, 1);
1043
+ void this.reconfigureSidecarMounts().catch(() => { });
1044
+ }
1045
+ }
1046
+ desiredSidecarMounts() {
1047
+ return [
1048
+ ...this.baseSidecarMounts,
1049
+ ...this.localMounts.map((mount) => mount.sidecarMount ?? {
1050
+ guestPath: mount.path,
1051
+ readOnly: mount.readOnly,
1052
+ plugin: {
1053
+ id: "js_bridge",
1054
+ config: {},
1055
+ },
1056
+ }),
1057
+ ];
1058
+ }
1059
+ reconfigureSidecarMounts() {
1060
+ const run = async () => {
1061
+ if (this.disposed) {
1062
+ return;
1063
+ }
1064
+ await this.client.configureVm(this.session, this.vm, {
1065
+ mounts: this.desiredSidecarMounts(),
1066
+ permissions: this.permissions,
1067
+ commandPermissions: this.commandPermissions,
1068
+ loopbackExemptPorts: this.loopbackExemptPorts,
1069
+ });
1070
+ };
1071
+ const previous = this.mountReconfigurePromise ?? Promise.resolve();
1072
+ const next = previous.then(run, run);
1073
+ const tracked = next.finally(() => {
1074
+ if (this.mountReconfigurePromise === tracked) {
1075
+ this.mountReconfigurePromise = null;
1076
+ }
1077
+ });
1078
+ this.mountReconfigurePromise = tracked;
1079
+ return tracked;
1080
+ }
1081
+ async waitForMountReconfigure() {
1082
+ if (this.mountReconfigurePromise) {
1083
+ await this.mountReconfigurePromise;
1022
1084
  }
1023
1085
  }
1024
1086
  snapshotProcesses() {
@@ -1132,6 +1194,7 @@ export class NativeSidecarKernelProxy {
1132
1194
  }
1133
1195
  }
1134
1196
  async startTrackedProcess(entry) {
1197
+ await this.waitForMountReconfigure();
1135
1198
  const started = await this.client.execute(this.session, this.vm, {
1136
1199
  processId: entry.processId,
1137
1200
  command: entry.command,
@@ -1157,11 +1220,14 @@ export class NativeSidecarKernelProxy {
1157
1220
  }
1158
1221
  }
1159
1222
  async runEventPump() {
1223
+ // Scope the pump to THIS VM's ownership so multiple proxies can share one
1224
+ // sidecar process: events for other VMs stay buffered for their own pumps
1225
+ // rather than being consumed (and dropped) here.
1226
+ const vmId = this.vm.vmId;
1160
1227
  while (!this.disposed) {
1161
1228
  try {
1162
- const event = await this.client.waitForEvent({ any: true }, undefined, {
1163
- signal: this.eventPumpAbortController.signal,
1164
- });
1229
+ const event = await this.client.waitForEvent((frame) => frame.ownership.scope === "vm" &&
1230
+ frame.ownership.vm_id === vmId, undefined, { signal: this.eventPumpAbortController.signal });
1165
1231
  if (event.payload.type === "process_output") {
1166
1232
  const entry = this.trackedProcessesById.get(event.payload.process_id);
1167
1233
  if (!entry) {
@@ -1580,7 +1646,8 @@ export class NativeSidecarKernelProxy {
1580
1646
  }
1581
1647
  return this.dispatchNativeRead(path);
1582
1648
  }
1583
- dispatchNativeRead(path) {
1649
+ async dispatchNativeRead(path) {
1650
+ await this.waitForMountReconfigure();
1584
1651
  return this.client.readFile(this.session, this.vm, path);
1585
1652
  }
1586
1653
  async dispatchWrite(path, handler, nativeHandler, includeLocalMounts = true) {
@@ -1591,6 +1658,7 @@ export class NativeSidecarKernelProxy {
1591
1658
  await handler(local.mount, local.relativePath);
1592
1659
  return;
1593
1660
  }
1661
+ await this.waitForMountReconfigure();
1594
1662
  await nativeHandler();
1595
1663
  }
1596
1664
  resolveLocalMount(path) {
@@ -1750,7 +1818,7 @@ function socketLookupKey(kind, request) {
1750
1818
  path: request.path ?? null,
1751
1819
  });
1752
1820
  }
1753
- export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, SidecarProcess, SidecarEventBufferOverflow, SidecarProcessError, SidecarProcessExited, } from "./native-process-client.js";
1821
+ export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, NativeSidecarProcessClient, SidecarEventBufferOverflow, SidecarProcess, SidecarProcessError, SidecarProcessExited, } from "./native-process-client.js";
1754
1822
  export class AgentOsSidecarVmHandle {
1755
1823
  client;
1756
1824
  sessionId;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rivet-dev/agentos-core",
3
- "version": "0.0.0-nathan-agentos-combined.e833bb7",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./dist/index.js",
@@ -56,9 +56,9 @@
56
56
  "dependencies": {
57
57
  "@agentos-software/common": "0.3.0-rc.2",
58
58
  "@aws-sdk/client-s3": "^3.1019.0",
59
- "@rivet-dev/agentos-sidecar": "0.0.0-nathan-agentos-combined.e833bb7",
59
+ "@rivet-dev/agentos-sidecar": "0.2.0",
60
60
  "@rivetkit/bare-ts": "^0.6.2",
61
- "@secure-exec/core": "0.0.0-codex-native-sidecar-platform-ci.117d645",
61
+ "@secure-exec/core": "0.3.1",
62
62
  "@xterm/headless": "^6.0.0",
63
63
  "better-sqlite3": "^12.8.0",
64
64
  "croner": "^10.0.1",
@@ -79,11 +79,11 @@
79
79
  "@copilotkit/llmock": "^1.6.0",
80
80
  "@rivet-dev/agentos-sandbox": "link:../agentos-sandbox",
81
81
  "@agentos-software/git": "0.3.0-rc.2",
82
- "@secure-exec/google-drive": "0.0.0-codex-native-sidecar-platform-ci.117d645",
83
- "@secure-exec/s3": "0.0.0-codex-native-sidecar-platform-ci.117d645",
82
+ "@secure-exec/google-drive": "0.3.1",
83
+ "@secure-exec/s3": "0.3.1",
84
84
  "@mariozechner/pi-coding-agent": "^0.60.0",
85
85
  "@agentos-software/claude-code": "0.0.0-nathan-binding-workspace.9be0a88",
86
- "@agentos-software/codex-cli": "0.3.0-rc.2",
86
+ "@agentos-software/codex-cli": "0.0.0-codex-claude-runtime-fixes.9cbef3a",
87
87
  "@agentos-software/codex": "0.3.0-rc.2",
88
88
  "@agentos-software/coreutils": "0.3.0-rc.2",
89
89
  "@agentos-software/curl": "0.3.0-rc.2",