@rivet-dev/agentos-core 0.0.0-agentos-dylib-plugin.bbfdf95 → 0.0.0-chdir-fix.f899dc6

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 (103) hide show
  1. package/README.md +8 -11
  2. package/dist/agent-os.d.ts +185 -43
  3. package/dist/agent-os.js +834 -536
  4. package/dist/agentos-package.d.ts +29 -0
  5. package/dist/agentos-package.js +88 -0
  6. package/dist/base-filesystem.d.ts +2 -15
  7. package/dist/base-filesystem.js +23 -171
  8. package/dist/cron/cron-manager.js +1 -4
  9. package/dist/cron/types.d.ts +1 -1
  10. package/dist/default-software.d.ts +8 -0
  11. package/dist/default-software.js +10 -0
  12. package/dist/generated/AcpLimitsConfig.d.ts +4 -0
  13. package/dist/generated/AcpLimitsConfig.js +2 -0
  14. package/dist/generated/CreateVmConfig.d.ts +26 -0
  15. package/dist/generated/CreateVmConfig.js +1 -0
  16. package/dist/generated/FsPermissionRule.d.ts +6 -0
  17. package/dist/generated/FsPermissionRule.js +1 -0
  18. package/dist/generated/FsPermissionRuleSet.d.ts +6 -0
  19. package/dist/generated/FsPermissionRuleSet.js +1 -0
  20. package/dist/generated/FsPermissionScope.d.ts +3 -0
  21. package/dist/generated/FsPermissionScope.js +1 -0
  22. package/dist/generated/HttpLimitsConfig.d.ts +3 -0
  23. package/dist/generated/HttpLimitsConfig.js +2 -0
  24. package/dist/generated/JsModuleResolution.d.ts +1 -0
  25. package/dist/generated/JsModuleResolution.js +2 -0
  26. package/dist/generated/JsRuntimeConfig.d.ts +31 -0
  27. package/dist/generated/JsRuntimeConfig.js +1 -0
  28. package/dist/generated/JsRuntimeLimitsConfig.d.ts +11 -0
  29. package/dist/generated/JsRuntimeLimitsConfig.js +2 -0
  30. package/dist/generated/JsRuntimePlatform.d.ts +1 -0
  31. package/dist/generated/JsRuntimePlatform.js +2 -0
  32. package/dist/generated/MountPluginDescriptor.d.ts +4 -0
  33. package/dist/generated/MountPluginDescriptor.js +2 -0
  34. package/dist/generated/NativeRootFilesystemConfig.d.ts +5 -0
  35. package/dist/generated/NativeRootFilesystemConfig.js +1 -0
  36. package/dist/generated/PatternPermissionRule.d.ts +6 -0
  37. package/dist/generated/PatternPermissionRule.js +1 -0
  38. package/dist/generated/PatternPermissionRuleSet.d.ts +6 -0
  39. package/dist/generated/PatternPermissionRuleSet.js +1 -0
  40. package/dist/generated/PatternPermissionScope.d.ts +3 -0
  41. package/dist/generated/PatternPermissionScope.js +1 -0
  42. package/dist/generated/PermissionMode.d.ts +1 -0
  43. package/dist/generated/PermissionMode.js +2 -0
  44. package/dist/generated/PermissionsPolicy.d.ts +10 -0
  45. package/dist/generated/PermissionsPolicy.js +1 -0
  46. package/dist/generated/PluginLimitsConfig.d.ts +4 -0
  47. package/dist/generated/PluginLimitsConfig.js +2 -0
  48. package/dist/generated/PythonLimitsConfig.d.ts +6 -0
  49. package/dist/generated/PythonLimitsConfig.js +2 -0
  50. package/dist/generated/ResourceLimitsConfig.d.ts +24 -0
  51. package/dist/generated/ResourceLimitsConfig.js +2 -0
  52. package/dist/generated/RootFilesystemConfig.d.ts +9 -0
  53. package/dist/generated/RootFilesystemConfig.js +1 -0
  54. package/dist/generated/RootFilesystemEntry.d.ts +13 -0
  55. package/dist/generated/RootFilesystemEntry.js +1 -0
  56. package/dist/generated/RootFilesystemEntryEncoding.d.ts +1 -0
  57. package/dist/generated/RootFilesystemEntryEncoding.js +2 -0
  58. package/dist/generated/RootFilesystemEntryKind.d.ts +1 -0
  59. package/dist/generated/RootFilesystemEntryKind.js +2 -0
  60. package/dist/generated/RootFilesystemLowerDescriptor.d.ts +7 -0
  61. package/dist/generated/RootFilesystemLowerDescriptor.js +1 -0
  62. package/dist/generated/RootFilesystemMode.d.ts +1 -0
  63. package/dist/generated/RootFilesystemMode.js +2 -0
  64. package/dist/generated/ToolLimitsConfig.d.ts +10 -0
  65. package/dist/generated/ToolLimitsConfig.js +2 -0
  66. package/dist/generated/VmDnsConfig.d.ts +6 -0
  67. package/dist/generated/VmDnsConfig.js +2 -0
  68. package/dist/generated/VmLimitsConfig.d.ts +18 -0
  69. package/dist/generated/VmLimitsConfig.js +1 -0
  70. package/dist/generated/VmListenPolicyConfig.d.ts +5 -0
  71. package/dist/generated/VmListenPolicyConfig.js +2 -0
  72. package/dist/generated/WasmLimitsConfig.d.ts +7 -0
  73. package/dist/generated/WasmLimitsConfig.js +2 -0
  74. package/dist/host-dir-mount.d.ts +1 -1
  75. package/dist/host-tools-zod.js +27 -7
  76. package/dist/host-tools.js +9 -0
  77. package/dist/index.d.ts +3 -1
  78. package/dist/index.js +3 -2
  79. package/dist/layers.d.ts +3 -3
  80. package/dist/layers.js +22 -13
  81. package/dist/options-schema.d.ts +453 -0
  82. package/dist/options-schema.js +268 -0
  83. package/dist/packages.d.ts +17 -116
  84. package/dist/packages.js +35 -322
  85. package/dist/runtime-compat.d.ts +27 -7
  86. package/dist/runtime-compat.js +133 -147
  87. package/dist/runtime.d.ts +37 -7
  88. package/dist/sandbox.d.ts +112 -0
  89. package/dist/sandbox.js +264 -0
  90. package/dist/sidecar/{agent-os-protocol.d.ts → agentos-protocol.d.ts} +99 -1
  91. package/dist/sidecar/{agent-os-protocol.js → agentos-protocol.js} +156 -19
  92. package/dist/sidecar/native-process-client.d.ts +6 -2
  93. package/dist/sidecar/native-process-client.js +8 -1
  94. package/dist/sidecar/permissions.d.ts +1 -1
  95. package/dist/sidecar/permissions.js +4 -4
  96. package/dist/sidecar/rpc-client.d.ts +87 -6
  97. package/dist/sidecar/rpc-client.js +433 -93
  98. package/dist/test/sandbox-agent.js +1 -1
  99. package/dist/types.d.ts +10 -3
  100. package/package.json +42 -35
  101. package/dist/agents.d.ts +0 -63
  102. package/dist/agents.js +0 -42
  103. package/fixtures/AGENTOS_SYSTEM_PROMPT.md +0 -21
@@ -7,6 +7,30 @@ const PROTECTED_READ_ONLY_GUEST_ROOTS = ["/etc/agentos"];
7
7
  const TRAILING_OUTPUT_DRAIN_INTERVAL_MS = 10;
8
8
  const TRAILING_OUTPUT_DRAIN_MAX_MS = 250;
9
9
  const TRAILING_OUTPUT_DRAIN_QUIET_TURNS = 2;
10
+ function shouldLogStructuredSidecarEvent(name) {
11
+ const normalized = name.toLowerCase();
12
+ return (normalized === "limit_warning" ||
13
+ normalized.startsWith("security.") ||
14
+ normalized.includes("warning") ||
15
+ normalized.includes("failed") ||
16
+ normalized.includes("error"));
17
+ }
18
+ function formatStructuredSidecarDetail(detail) {
19
+ const entries = Object.entries(detail);
20
+ if (entries.length === 0) {
21
+ return "";
22
+ }
23
+ return entries.map(([key, value]) => `${key}=${JSON.stringify(value)}`).join(" ");
24
+ }
25
+ function logStructuredSidecarEvent(name, detail) {
26
+ if (!shouldLogStructuredSidecarEvent(name)) {
27
+ return;
28
+ }
29
+ const formatted = formatStructuredSidecarDetail(detail);
30
+ console.warn(formatted
31
+ ? `[agent-os] sidecar ${name}: ${formatted}`
32
+ : `[agent-os] sidecar ${name}`);
33
+ }
10
34
  async function drainTrailingProcessOutputTurn(delayMs = 0) {
11
35
  // Native-sidecar `process_output` events can lag one macrotask behind the
12
36
  // terminal `process_exited` notification for very short-lived processes, and
@@ -142,7 +166,10 @@ function parseSimpleExecCommand(command) {
142
166
  return tokens;
143
167
  }
144
168
  function canUseDirectExec(driver, commandName) {
145
- return driver === "wasmvm" || (driver === "node" && commandName === "node");
169
+ return (driver === "wasmvm" ||
170
+ (driver === "node" && commandName === "node") ||
171
+ (driver === "python" &&
172
+ (commandName === "python" || commandName === "python3")));
146
173
  }
147
174
  function shellSingleQuote(value) {
148
175
  if (value.length === 0) {
@@ -181,7 +208,17 @@ export class NativeSidecarKernelProxy {
181
208
  client;
182
209
  session;
183
210
  vm;
211
+ ownsClient;
184
212
  localMounts;
213
+ baseSidecarMounts;
214
+ permissions;
215
+ commandPermissions;
216
+ loopbackExemptPorts;
217
+ // Mutable: runtime `linkSoftware` appends via `registerLinkedPackage` so
218
+ // later mount reconfigures resend linked packages too, not just boot ones.
219
+ packages;
220
+ packagesMountAt;
221
+ toolShimCommands;
185
222
  commandDrivers;
186
223
  onWasmCommandResolved;
187
224
  onDispose;
@@ -199,6 +236,7 @@ export class NativeSidecarKernelProxy {
199
236
  zombieTimerCountRefresh = null;
200
237
  disposed = false;
201
238
  pumpError = null;
239
+ mountReconfigurePromise = null;
202
240
  nextSyntheticPid = SYNTHETIC_PID_BASE;
203
241
  eventPumpAbortController = new AbortController();
204
242
  eventPump;
@@ -206,10 +244,20 @@ export class NativeSidecarKernelProxy {
206
244
  this.client = options.client;
207
245
  this.session = options.session;
208
246
  this.vm = options.vm;
247
+ this.ownsClient = options.ownsClient ?? true;
209
248
  this.env = { ...options.env };
210
249
  this.cwd = options.cwd;
211
250
  this.defaultExecCwd = options.defaultExecCwd;
212
251
  this.localMounts = [...options.localMounts].sort((left, right) => right.path.length - left.path.length);
252
+ const localMountPaths = new Set(this.localMounts.map((mount) => mount.path));
253
+ this.baseSidecarMounts = options.sidecarMounts.filter((mount) => mount.plugin.id !== "js_bridge" ||
254
+ !localMountPaths.has(posixPath.normalize(mount.guestPath)));
255
+ this.permissions = options.permissions;
256
+ this.commandPermissions = options.commandPermissions;
257
+ this.loopbackExemptPorts = options.loopbackExemptPorts;
258
+ this.packages = options.packages ? [...options.packages] : [];
259
+ this.packagesMountAt = options.packagesMountAt;
260
+ this.toolShimCommands = options.toolShimCommands;
213
261
  this.commandDrivers = buildCommandMap(options.commandGuestPaths);
214
262
  this.onWasmCommandResolved = options.onWasmCommandResolved;
215
263
  this.onDispose = options.onDispose;
@@ -233,12 +281,24 @@ export class NativeSidecarKernelProxy {
233
281
  this.commandDrivers.set(name, "wasmvm");
234
282
  }
235
283
  }
284
+ /**
285
+ * Record a runtime-linked package (`linkSoftware`) so mount reconfigures
286
+ * resend it. Rust `configure_vm` rebuilds the whole VM configuration from
287
+ * each payload, so a linked package omitted here would be silently
288
+ * unprojected from `/opt/agentos` by the next `mountFs`/`unmountFs`.
289
+ */
290
+ registerLinkedPackage(descriptor) {
291
+ if (!this.packages.some((pkg) => pkg.path === descriptor.path)) {
292
+ this.packages.push({ path: descriptor.path });
293
+ }
294
+ }
236
295
  async dispose() {
237
296
  if (this.disposed) {
238
297
  return;
239
298
  }
240
299
  this.disposed = true;
241
300
  this.eventPumpAbortController.abort();
301
+ await this.mountReconfigurePromise?.catch(() => { });
242
302
  const liveProcesses = [...this.trackedProcesses.values()].filter((entry) => entry.exitCode === null);
243
303
  await Promise.allSettled(liveProcesses.map((entry) => this.signalProcess(entry, 15)));
244
304
  await this.client.disposeVm(this.session, this.vm).catch(() => { });
@@ -251,9 +311,42 @@ export class NativeSidecarKernelProxy {
251
311
  this.finishProcess(entry, 143);
252
312
  }
253
313
  }
254
- await this.client.dispose().catch(() => { });
314
+ // Only tear down the shared sidecar process when this proxy owns it. VMs
315
+ // leased from an `AgentOsSidecar` handle share one process, which is
316
+ // disposed when the handle is disposed.
317
+ if (this.ownsClient) {
318
+ await this.client.dispose().catch(() => { });
319
+ }
255
320
  await this.eventPump.catch(() => { });
256
321
  await this.onDispose?.().catch(() => { });
322
+ // Drop all per-VM tracking state so a disposed proxy retains nothing.
323
+ for (const entry of this.trackedProcesses.values()) {
324
+ entry.onStdout.clear();
325
+ entry.onStderr.clear();
326
+ }
327
+ this.trackedProcesses.clear();
328
+ this.trackedProcessesById.clear();
329
+ this.signalStates.clear();
330
+ this.signalRefreshes.clear();
331
+ this.localMounts.length = 0;
332
+ }
333
+ /** Test-only snapshot of the per-VM tracking collection sizes. */
334
+ __trackingSizesForTest() {
335
+ return {
336
+ trackedProcesses: this.trackedProcesses.size,
337
+ trackedProcessesById: this.trackedProcessesById.size,
338
+ signalStates: this.signalStates.size,
339
+ signalRefreshes: this.signalRefreshes.size,
340
+ localMounts: this.localMounts.length,
341
+ };
342
+ }
343
+ /** Test-only handle to a tracked entry so its listener Sets can be inspected. */
344
+ __trackedEntryForTest(pid) {
345
+ return this.trackedProcesses.get(pid);
346
+ }
347
+ /** Test-only join point for in-flight signal-state refreshes. */
348
+ async __awaitSignalRefreshesForTest() {
349
+ await Promise.allSettled([...this.signalRefreshes.values()]);
257
350
  }
258
351
  async exec(command, options) {
259
352
  if (!this.commands.has("sh")) {
@@ -263,9 +356,6 @@ export class NativeSidecarKernelProxy {
263
356
  const stderrChunks = [];
264
357
  const effectiveCwd = options?.cwd ?? this.defaultExecCwd ?? this.cwd;
265
358
  const parsedCommand = parseSimpleExecCommand(command);
266
- const resolveExecPath = (targetPath) => targetPath.startsWith("/")
267
- ? posixPath.normalize(targetPath)
268
- : posixPath.normalize(posixPath.join(effectiveCwd, targetPath));
269
359
  const runAndCapture = async (proc, stdinOverride, readExitCode) => {
270
360
  if (stdinOverride !== undefined) {
271
361
  proc.writeStdin(stdinOverride);
@@ -301,43 +391,6 @@ export class NativeSidecarKernelProxy {
301
391
  stderr: Buffer.concat(stderrChunks.map((chunk) => Buffer.from(chunk))).toString("utf8"),
302
392
  };
303
393
  };
304
- if (parsedCommand &&
305
- (parsedCommand[0] === "sh" || parsedCommand[0] === "/bin/sh") &&
306
- parsedCommand[1] === "-c" &&
307
- parsedCommand.length === 3) {
308
- const shellScript = parsedCommand[2].trim();
309
- const exitMatch = shellScript.match(/^exit(?:\s+(-?\d+))?$/);
310
- if (exitMatch) {
311
- return {
312
- exitCode: Number.parseInt(exitMatch[1] ?? "0", 10),
313
- stdout: "",
314
- stderr: "",
315
- };
316
- }
317
- return this.exec(parsedCommand[2], options);
318
- }
319
- if (parsedCommand &&
320
- parsedCommand[0] === "chmod" &&
321
- parsedCommand.length >= 3 &&
322
- /^[0-7]{3,4}$/.test(parsedCommand[1] ?? "")) {
323
- const mode = Number.parseInt(parsedCommand[1], 8);
324
- for (const target of parsedCommand.slice(2)) {
325
- await this.client.chmod(this.session, this.vm, resolveExecPath(target), mode);
326
- }
327
- return { exitCode: 0, stdout: "", stderr: "" };
328
- }
329
- if (parsedCommand &&
330
- parsedCommand[0] === "stat" &&
331
- parsedCommand.length === 4 &&
332
- parsedCommand[1] === "-c" &&
333
- parsedCommand[2] === "%a") {
334
- const stat = await this.stat(resolveExecPath(parsedCommand[3]));
335
- return {
336
- exitCode: 0,
337
- stdout: `${(stat.mode & 0o777).toString(8)}\n`,
338
- stderr: "",
339
- };
340
- }
341
394
  const parsedCommandDriver = parsedCommand
342
395
  ? this.commands.get(parsedCommand[0])
343
396
  : undefined;
@@ -376,6 +429,51 @@ export class NativeSidecarKernelProxy {
376
429
  });
377
430
  return runAndCapture(proc);
378
431
  }
432
+ async execArgv(command, args = [], options) {
433
+ const stdoutChunks = [];
434
+ const stderrChunks = [];
435
+ const effectiveCwd = options?.cwd ?? this.defaultExecCwd ?? this.cwd;
436
+ const runAndCapture = async (proc) => {
437
+ if (options?.stdin !== undefined) {
438
+ proc.writeStdin(options.stdin);
439
+ }
440
+ proc.closeStdin();
441
+ const waitPromise = proc.wait();
442
+ const exitCode = typeof options?.timeout === "number"
443
+ ? await new Promise((resolve) => {
444
+ const timer = setTimeout(() => {
445
+ proc.kill(9);
446
+ void proc.wait().then(resolve);
447
+ }, options.timeout);
448
+ void waitPromise.then((code) => {
449
+ clearTimeout(timer);
450
+ resolve(code);
451
+ });
452
+ })
453
+ : await waitPromise;
454
+ await drainTrailingProcessOutputTurn();
455
+ return {
456
+ exitCode,
457
+ stdout: Buffer.concat(stdoutChunks.map((chunk) => Buffer.from(chunk))).toString("utf8"),
458
+ stderr: Buffer.concat(stderrChunks.map((chunk) => Buffer.from(chunk))).toString("utf8"),
459
+ };
460
+ };
461
+ if (this.commands.get(command) === "wasmvm") {
462
+ this.onWasmCommandResolved?.(command);
463
+ }
464
+ return runAndCapture(this.spawn(command, [...args], {
465
+ ...options,
466
+ cwd: effectiveCwd,
467
+ onStdout: (chunk) => {
468
+ stdoutChunks.push(chunk);
469
+ options?.onStdout?.(chunk);
470
+ },
471
+ onStderr: (chunk) => {
472
+ stderrChunks.push(chunk);
473
+ options?.onStderr?.(chunk);
474
+ },
475
+ }));
476
+ }
379
477
  spawn(command, args, options) {
380
478
  let spawnCommand = command;
381
479
  let spawnArgs = [...args];
@@ -402,11 +500,15 @@ export class NativeSidecarKernelProxy {
402
500
  processId,
403
501
  command: spawnCommand,
404
502
  args: spawnArgs,
405
- driver: spawnCommand === "node" ? "node" : "wasmvm",
503
+ driver: spawnCommand === "node"
504
+ ? "node"
505
+ : spawnCommand === "python" || spawnCommand === "python3"
506
+ ? "python"
507
+ : "wasmvm",
406
508
  cwd: options?.cwd ?? this.cwd,
407
509
  env: {
408
510
  ...(options?.env ?? {}),
409
- ...(options?.streamStdin ? { SECURE_EXEC_KEEP_STDIN_OPEN: "1" } : {}),
511
+ ...(options?.streamStdin ? { AGENTOS_KEEP_STDIN_OPEN: "1" } : {}),
410
512
  },
411
513
  startTime: Date.now(),
412
514
  exitTime: null,
@@ -434,16 +536,16 @@ export class NativeSidecarKernelProxy {
434
536
  pid,
435
537
  writeStdin: (data) => {
436
538
  if (entry.exitCode !== null) {
437
- return;
539
+ return Promise.resolve();
438
540
  }
439
541
  entry.pendingStdin.push(data);
440
- void this.flushPendingStdin(entry).catch((error) => {
542
+ return this.flushPendingStdin(entry).catch((error) => {
441
543
  this.handleBackgroundProcessError(entry, error);
442
544
  });
443
545
  },
444
546
  closeStdin: () => {
445
547
  entry.pendingCloseStdin = true;
446
- void this.closeTrackedStdin(entry).catch((error) => {
548
+ return this.closeTrackedStdin(entry).catch((error) => {
447
549
  this.handleBackgroundProcessError(entry, error);
448
550
  });
449
551
  },
@@ -487,7 +589,6 @@ export class NativeSidecarKernelProxy {
487
589
  const args = options?.args ??
488
590
  (command === "sh" || command === "/bin/sh" ? ["-i"] : []);
489
591
  const synthesizePrompt = !options?.command && !options?.args;
490
- const autoCloseExplicitCommandStdin = Boolean(options?.command) && !["sh", "/bin/sh", "bash"].includes(command);
491
592
  const promptText = "sh-0.4$ ";
492
593
  const textEncoder = new TextEncoder();
493
594
  const textDecoder = new TextDecoder();
@@ -497,14 +598,18 @@ export class NativeSidecarKernelProxy {
497
598
  .replace(/\u001b\[[0-9;]*m/g, "")
498
599
  .replace(/^.*WARN could not retrieve pid for child process\n?/gm, "")
499
600
  .replace(/^ProcessExitError:.*\n(?:\s+at .*\n)*/gm, "");
601
+ const sanitizeNativeShellOutput = (chunk) => {
602
+ const text = textDecoder.decode(chunk);
603
+ const sanitized = text.replace(/^.*WARN could not retrieve pid for child process\n?/gm, "");
604
+ return sanitized.length > 0 ? textEncoder.encode(sanitized) : null;
605
+ };
500
606
  let bufferedInput = "";
501
607
  let bufferedCommand = "";
502
608
  let activeForegroundProcess = null;
503
- let shellEnv = { ...(options?.env ?? {}) };
609
+ let shellEnv = { ...(options?.env ?? {}), AGENTOS_EXEC_TTY: "1" };
504
610
  let shellCwd = options?.cwd ?? this.cwd;
505
611
  let syntheticCommandQueue = Promise.resolve();
506
612
  let promptTimer = null;
507
- let closeStdinTimer = null;
508
613
  let commandInFlight = false;
509
614
  let syntheticCursorAtLineStart = true;
510
615
  const syntheticPid = this.nextSyntheticPid++;
@@ -519,12 +624,6 @@ export class NativeSidecarKernelProxy {
519
624
  promptTimer = null;
520
625
  }
521
626
  };
522
- const clearCloseStdinTimer = () => {
523
- if (closeStdinTimer !== null) {
524
- clearTimeout(closeStdinTimer);
525
- closeStdinTimer = null;
526
- }
527
- };
528
627
  const normalizeSyntheticTerminalText = (text) => text.replace(/\r?\n/g, "\r\n");
529
628
  const updateSyntheticCursor = (text) => {
530
629
  if (!text) {
@@ -620,15 +719,36 @@ export class NativeSidecarKernelProxy {
620
719
  }
621
720
  return parsed;
622
721
  };
623
- const writeForegroundInput = (proc, data) => {
722
+ const writeForegroundInput = async (proc, data) => {
624
723
  if (typeof data === "string") {
625
724
  for (const character of data) {
626
- proc.writeStdin(character);
725
+ await proc.writeStdin(character);
627
726
  }
628
727
  return;
629
728
  }
630
729
  for (const byte of data) {
631
- proc.writeStdin(new Uint8Array([byte]));
730
+ await proc.writeStdin(new Uint8Array([byte]));
731
+ }
732
+ };
733
+ const appendSyntheticInput = (input) => {
734
+ for (const character of input) {
735
+ if (character === "\u0004") {
736
+ continue;
737
+ }
738
+ if (character === "\b" || character === "\u007f") {
739
+ if (bufferedInput.length > 0) {
740
+ bufferedInput = bufferedInput.slice(0, -1);
741
+ emitSyntheticTerminal("\b \b");
742
+ }
743
+ continue;
744
+ }
745
+ bufferedInput += character;
746
+ if (character === "\n") {
747
+ emitSyntheticTerminal("\n");
748
+ }
749
+ else if (character >= " ") {
750
+ emitSyntheticTerminal(character);
751
+ }
632
752
  }
633
753
  };
634
754
  let onData = null;
@@ -640,7 +760,7 @@ export class NativeSidecarKernelProxy {
640
760
  schedulePrompt(0);
641
761
  return {
642
762
  pid: syntheticPid,
643
- write(data) {
763
+ async write(data) {
644
764
  if (syntheticExitCode !== null) {
645
765
  return;
646
766
  }
@@ -651,13 +771,13 @@ export class NativeSidecarKernelProxy {
651
771
  if (rawText.includes("\u0003")) {
652
772
  const [beforeInterrupt] = rawText.split("\u0003");
653
773
  if (beforeInterrupt) {
654
- writeForegroundInput(activeForegroundProcess, beforeInterrupt);
774
+ await writeForegroundInput(activeForegroundProcess, beforeInterrupt);
655
775
  }
656
776
  emitSyntheticTerminal("^C\n");
657
777
  activeForegroundProcess.kill(2);
658
778
  return;
659
779
  }
660
- writeForegroundInput(activeForegroundProcess, data);
780
+ await writeForegroundInput(activeForegroundProcess, data);
661
781
  return;
662
782
  }
663
783
  const rawText = typeof data === "string"
@@ -680,7 +800,7 @@ export class NativeSidecarKernelProxy {
680
800
  finishSyntheticShell(0);
681
801
  return;
682
802
  }
683
- bufferedInput += text.replace(/\u0004/g, "");
803
+ appendSyntheticInput(text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"));
684
804
  while (true) {
685
805
  const newlineIndex = bufferedInput.indexOf("\n");
686
806
  if (newlineIndex < 0) {
@@ -690,7 +810,6 @@ export class NativeSidecarKernelProxy {
690
810
  .slice(0, newlineIndex)
691
811
  .replace(/\r$/, "");
692
812
  bufferedInput = bufferedInput.slice(newlineIndex + 1);
693
- emitSyntheticStdout(`${line}\n`);
694
813
  const nextCommand = bufferedCommand
695
814
  ? `${bufferedCommand}\n${line}`
696
815
  : line;
@@ -789,20 +908,33 @@ export class NativeSidecarKernelProxy {
789
908
  };
790
909
  }
791
910
  const proc = this.spawn(command, args, {
792
- env: options?.env,
911
+ env: {
912
+ ...(options?.env ?? {}),
913
+ ...(options?.cols ? { COLUMNS: String(Math.trunc(options.cols)) } : {}),
914
+ ...(options?.rows ? { LINES: String(Math.trunc(options.rows)) } : {}),
915
+ AGENTOS_EXEC_TTY: "1",
916
+ },
793
917
  cwd: options?.cwd,
794
918
  streamStdin: true,
795
919
  onStdout: (chunk) => {
920
+ const sanitized = sanitizeNativeShellOutput(chunk);
921
+ if (!sanitized) {
922
+ return;
923
+ }
796
924
  for (const handler of stdoutHandlers) {
797
- handler(chunk);
925
+ handler(sanitized);
798
926
  }
799
927
  if (commandInFlight) {
800
928
  schedulePrompt(120);
801
929
  }
802
930
  },
803
931
  onStderr: (chunk) => {
932
+ const sanitized = sanitizeNativeShellOutput(chunk);
933
+ if (!sanitized) {
934
+ return;
935
+ }
804
936
  for (const handler of stderrHandlers) {
805
- handler(chunk);
937
+ handler(sanitized);
806
938
  }
807
939
  if (commandInFlight) {
808
940
  schedulePrompt(120);
@@ -811,18 +943,11 @@ export class NativeSidecarKernelProxy {
811
943
  });
812
944
  return {
813
945
  pid: proc.pid,
814
- write(data) {
946
+ async write(data) {
815
947
  if (synthesizePrompt) {
816
948
  return;
817
949
  }
818
- proc.writeStdin(data);
819
- if (autoCloseExplicitCommandStdin) {
820
- clearCloseStdinTimer();
821
- closeStdinTimer = setTimeout(() => {
822
- closeStdinTimer = null;
823
- proc.closeStdin();
824
- }, 100);
825
- }
950
+ await proc.writeStdin(data);
826
951
  if (synthesizePrompt &&
827
952
  typeof data === "string" &&
828
953
  (data.includes("\n") || data.includes("\r"))) {
@@ -836,11 +961,18 @@ export class NativeSidecarKernelProxy {
836
961
  set onData(handler) {
837
962
  onData = handler;
838
963
  },
839
- resize() {
840
- // The current stdio-native path is process-backed rather than PTY-backed.
964
+ resize: (cols, rows) => {
965
+ const entry = this.trackedProcesses.get(proc.pid);
966
+ if (!entry || entry.exitCode !== null) {
967
+ return;
968
+ }
969
+ void entry.startPromise
970
+ .then(() => this.client.resizePty(this.session, this.vm, entry.processId, Math.trunc(cols), Math.trunc(rows)))
971
+ .catch((error) => {
972
+ this.handleBackgroundProcessError(entry, error);
973
+ });
841
974
  },
842
975
  kill(signal) {
843
- clearCloseStdinTimer();
844
976
  clearPromptTimer();
845
977
  proc.kill(signal);
846
978
  },
@@ -940,12 +1072,41 @@ export class NativeSidecarKernelProxy {
940
1072
  const entries = await this.client.readdir(this.session, this.vm, path);
941
1073
  return [...new Set([...entries, ...this.mountedChildNames(path)])].sort((a, b) => a.localeCompare(b));
942
1074
  }
1075
+ async readdirRecursive(path, options) {
1076
+ const local = this.resolveLocalMount(path);
1077
+ if (local) {
1078
+ return this.readdirRecursiveLocal(local.mount.fs, local.relativePath, options?.maxDepth);
1079
+ }
1080
+ return (await this.client.readdirRecursive(this.session, this.vm, path, options)).map((entry) => ({ ...entry, size: Number(entry.size) }));
1081
+ }
943
1082
  async removeFile(path) {
944
1083
  return this.dispatchWrite(path, (mount, relativePath) => mount.fs.removeFile(relativePath), () => this.client.removeFile(this.session, this.vm, path));
945
1084
  }
946
1085
  async removeDir(path) {
947
1086
  return this.dispatchWrite(path, (mount, relativePath) => mount.fs.removeDir(relativePath), () => this.client.removeDir(this.session, this.vm, path));
948
1087
  }
1088
+ async removePath(path, options) {
1089
+ return this.dispatchWrite(path, (mount, relativePath) => this.removePathLocal(mount.fs, relativePath, options?.recursive ?? false), () => this.client.removePath(this.session, this.vm, path, {
1090
+ recursive: options?.recursive ?? false,
1091
+ }));
1092
+ }
1093
+ async copyPath(fromPath, toPath, options) {
1094
+ const from = this.resolveLocalMount(fromPath);
1095
+ const to = this.resolveLocalMount(toPath);
1096
+ if (!!from !== !!to) {
1097
+ throw errnoError("EXDEV", "cross-device link not permitted");
1098
+ }
1099
+ if (from && to) {
1100
+ if (from.mount.path !== to.mount.path) {
1101
+ throw errnoError("EXDEV", "cross-device link not permitted");
1102
+ }
1103
+ this.assertLocalWritable(to.mount);
1104
+ return this.copyPathLocal(from.mount.fs, from.relativePath, to.relativePath, options?.recursive ?? false);
1105
+ }
1106
+ return this.client.copyPath(this.session, this.vm, fromPath, toPath, {
1107
+ recursive: options?.recursive ?? false,
1108
+ });
1109
+ }
949
1110
  async rename(oldPath, newPath) {
950
1111
  const from = this.resolveLocalMount(oldPath);
951
1112
  const to = this.resolveLocalMount(newPath);
@@ -961,19 +1122,93 @@ export class NativeSidecarKernelProxy {
961
1122
  }
962
1123
  return this.client.rename(this.session, this.vm, oldPath, newPath);
963
1124
  }
1125
+ async movePath(oldPath, newPath) {
1126
+ const from = this.resolveLocalMount(oldPath);
1127
+ const to = this.resolveLocalMount(newPath);
1128
+ if (!!from !== !!to) {
1129
+ throw errnoError("EXDEV", "cross-device link not permitted");
1130
+ }
1131
+ if (from && to) {
1132
+ if (from.mount.path !== to.mount.path) {
1133
+ throw errnoError("EXDEV", "cross-device link not permitted");
1134
+ }
1135
+ this.assertLocalWritable(from.mount);
1136
+ return from.mount.fs.rename(from.relativePath, to.relativePath);
1137
+ }
1138
+ return this.client.movePath(this.session, this.vm, oldPath, newPath);
1139
+ }
964
1140
  mountFs(path, driver, options) {
965
1141
  this.localMounts.unshift({
966
1142
  path: posixPath.normalize(path),
967
1143
  fs: driver,
968
1144
  readOnly: options?.readOnly ?? false,
1145
+ sidecarMount: options?.sidecarMount,
969
1146
  });
970
1147
  this.localMounts.sort((left, right) => right.path.length - left.path.length);
1148
+ // Resolves once the sidecar has the mount; a swallowed rejection here
1149
+ // used to turn reconfigure failures into silently missing guest mounts.
1150
+ // The local catch only guards callers that drop the promise — awaiting
1151
+ // callers still observe the rejection.
1152
+ const applied = this.reconfigureSidecarMounts();
1153
+ applied.catch(() => { });
1154
+ return applied;
971
1155
  }
972
1156
  unmountFs(path) {
973
1157
  const normalized = posixPath.normalize(path);
974
1158
  const index = this.localMounts.findIndex((mount) => mount.path === normalized);
975
- if (index >= 0) {
976
- this.localMounts.splice(index, 1);
1159
+ if (index < 0) {
1160
+ return Promise.resolve();
1161
+ }
1162
+ this.localMounts.splice(index, 1);
1163
+ const applied = this.reconfigureSidecarMounts();
1164
+ applied.catch(() => { });
1165
+ return applied;
1166
+ }
1167
+ desiredSidecarMounts() {
1168
+ return [
1169
+ ...this.baseSidecarMounts,
1170
+ ...this.localMounts.map((mount) => mount.sidecarMount ?? {
1171
+ guestPath: mount.path,
1172
+ readOnly: mount.readOnly,
1173
+ plugin: {
1174
+ id: "js_bridge",
1175
+ config: {},
1176
+ },
1177
+ }),
1178
+ ];
1179
+ }
1180
+ reconfigureSidecarMounts() {
1181
+ const run = async () => {
1182
+ if (this.disposed) {
1183
+ return;
1184
+ }
1185
+ // Rust `configure_vm` rebuilds the whole VM configuration from this
1186
+ // payload, so resend the boot packages / tool shim commands too —
1187
+ // omitting them here strips the `/opt/agentos` projections and tool
1188
+ // shims from the VM as a side effect of a runtime mount change.
1189
+ await this.client.configureVm(this.session, this.vm, {
1190
+ mounts: this.desiredSidecarMounts(),
1191
+ permissions: this.permissions,
1192
+ commandPermissions: this.commandPermissions,
1193
+ loopbackExemptPorts: this.loopbackExemptPorts,
1194
+ packages: this.packages,
1195
+ packagesMountAt: this.packagesMountAt,
1196
+ toolShimCommands: this.toolShimCommands,
1197
+ });
1198
+ };
1199
+ const previous = this.mountReconfigurePromise ?? Promise.resolve();
1200
+ const next = previous.then(run, run);
1201
+ const tracked = next.finally(() => {
1202
+ if (this.mountReconfigurePromise === tracked) {
1203
+ this.mountReconfigurePromise = null;
1204
+ }
1205
+ });
1206
+ this.mountReconfigurePromise = tracked;
1207
+ return tracked;
1208
+ }
1209
+ async waitForMountReconfigure() {
1210
+ if (this.mountReconfigurePromise) {
1211
+ await this.mountReconfigurePromise;
977
1212
  }
978
1213
  }
979
1214
  snapshotProcesses() {
@@ -1087,6 +1322,7 @@ export class NativeSidecarKernelProxy {
1087
1322
  }
1088
1323
  }
1089
1324
  async startTrackedProcess(entry) {
1325
+ await this.waitForMountReconfigure();
1090
1326
  const started = await this.client.execute(this.session, this.vm, {
1091
1327
  processId: entry.processId,
1092
1328
  command: entry.command,
@@ -1112,11 +1348,13 @@ export class NativeSidecarKernelProxy {
1112
1348
  }
1113
1349
  }
1114
1350
  async runEventPump() {
1351
+ // Scope the pump to THIS VM's ownership so multiple proxies can share one
1352
+ // sidecar process: events for other VMs stay buffered for their own pumps
1353
+ // rather than being consumed (and dropped) here.
1354
+ const vmId = this.vm.vmId;
1115
1355
  while (!this.disposed) {
1116
1356
  try {
1117
- const event = await this.client.waitForEvent({ any: true }, undefined, {
1118
- signal: this.eventPumpAbortController.signal,
1119
- });
1357
+ const event = await this.client.waitForEvent((frame) => frame.ownership.scope === "vm" && frame.ownership.vm_id === vmId, undefined, { signal: this.eventPumpAbortController.signal });
1120
1358
  if (event.payload.type === "process_output") {
1121
1359
  const entry = this.trackedProcessesById.get(event.payload.process_id);
1122
1360
  if (!entry) {
@@ -1143,8 +1381,11 @@ export class NativeSidecarKernelProxy {
1143
1381
  continue;
1144
1382
  }
1145
1383
  void this.refreshProcessSnapshot().catch(() => { });
1146
- this.signalRefreshes.delete(entry.pid);
1147
1384
  this.finishProcess(entry, event.payload.exit_code);
1385
+ continue;
1386
+ }
1387
+ if (event.payload.type === "structured") {
1388
+ logStructuredSidecarEvent(event.payload.name, event.payload.detail);
1148
1389
  }
1149
1390
  }
1150
1391
  catch (error) {
@@ -1175,6 +1416,25 @@ export class NativeSidecarKernelProxy {
1175
1416
  entry.exitTime = Date.now();
1176
1417
  this.updateTrackedProcessSnapshot(entry);
1177
1418
  entry.resolveWait(exitCode);
1419
+ // Release per-process tracking now that the process has terminated so these
1420
+ // maps/Sets don't grow without bound. Defer the release until trailing
1421
+ // output has drained: `wait()`'s drain and late `process_output` events
1422
+ // still need the entry + its listeners during the drain window. The exited
1423
+ // record lives on in `processes` for listing.
1424
+ void this.releaseProcessTrackingAfterDrain(entry);
1425
+ }
1426
+ async releaseProcessTrackingAfterDrain(entry) {
1427
+ try {
1428
+ await this.drainTrailingProcessOutput(entry);
1429
+ }
1430
+ finally {
1431
+ this.trackedProcesses.delete(entry.pid);
1432
+ this.trackedProcessesById.delete(entry.processId);
1433
+ this.signalRefreshes.delete(entry.pid);
1434
+ this.signalStates.delete(entry.pid);
1435
+ entry.onStdout.clear();
1436
+ entry.onStderr.clear();
1437
+ }
1178
1438
  }
1179
1439
  waitForTrackedProcess(entry) {
1180
1440
  if (entry.exitCode !== null) {
@@ -1325,6 +1585,74 @@ export class NativeSidecarKernelProxy {
1325
1585
  handler(stderr);
1326
1586
  }
1327
1587
  }
1588
+ async readdirRecursiveLocal(fs, path, maxDepth) {
1589
+ const results = [];
1590
+ const queue = [{ path, depth: 0 }];
1591
+ while (queue.length > 0) {
1592
+ const current = queue.shift();
1593
+ if (!current)
1594
+ break;
1595
+ for (const name of await fs.readDir(current.path)) {
1596
+ if (name === "." || name === "..")
1597
+ continue;
1598
+ const child = posixPath.join(current.path, name);
1599
+ const stat = await fs.lstat(child);
1600
+ results.push({
1601
+ name,
1602
+ path: child,
1603
+ isDirectory: stat.isDirectory,
1604
+ isSymbolicLink: stat.isSymbolicLink,
1605
+ size: stat.size,
1606
+ });
1607
+ if (stat.isDirectory &&
1608
+ !stat.isSymbolicLink &&
1609
+ (maxDepth === undefined || current.depth < maxDepth)) {
1610
+ queue.push({ path: child, depth: current.depth + 1 });
1611
+ }
1612
+ }
1613
+ }
1614
+ return results;
1615
+ }
1616
+ async removePathLocal(fs, path, recursive) {
1617
+ const stat = await fs.lstat(path);
1618
+ if (stat.isDirectory && !stat.isSymbolicLink) {
1619
+ if (recursive) {
1620
+ for (const name of await fs.readDir(path)) {
1621
+ if (name === "." || name === "..")
1622
+ continue;
1623
+ await this.removePathLocal(fs, posixPath.join(path, name), true);
1624
+ }
1625
+ }
1626
+ return fs.removeDir(path);
1627
+ }
1628
+ return fs.removeFile(path);
1629
+ }
1630
+ async copyPathLocal(fs, fromPath, toPath, recursive) {
1631
+ const stat = await fs.lstat(fromPath);
1632
+ if (stat.isSymbolicLink) {
1633
+ return fs.symlink(await fs.readlink(fromPath), toPath);
1634
+ }
1635
+ if (stat.isDirectory) {
1636
+ if (!recursive) {
1637
+ throw errnoError("EISDIR", "illegal operation on a directory");
1638
+ }
1639
+ await fs.mkdir(posixPath.dirname(toPath), { recursive: true });
1640
+ if (!(await fs.exists(toPath))) {
1641
+ await fs.createDir(toPath);
1642
+ }
1643
+ await fs.chmod(toPath, stat.mode);
1644
+ await fs.chown(toPath, stat.uid, stat.gid);
1645
+ for (const name of await fs.readDir(fromPath)) {
1646
+ if (name === "." || name === "..")
1647
+ continue;
1648
+ await this.copyPathLocal(fs, posixPath.join(fromPath, name), posixPath.join(toPath, name), true);
1649
+ }
1650
+ return;
1651
+ }
1652
+ await fs.writeFile(toPath, await fs.readFile(fromPath));
1653
+ await fs.chmod(toPath, stat.mode);
1654
+ await fs.chown(toPath, stat.uid, stat.gid);
1655
+ }
1328
1656
  createFilesystemView(includeLocalMounts) {
1329
1657
  return {
1330
1658
  readFile: (path) => this.dispatchRead(path, (mount, relativePath) => mount.fs.readFile(relativePath), includeLocalMounts),
@@ -1535,7 +1863,8 @@ export class NativeSidecarKernelProxy {
1535
1863
  }
1536
1864
  return this.dispatchNativeRead(path);
1537
1865
  }
1538
- dispatchNativeRead(path) {
1866
+ async dispatchNativeRead(path) {
1867
+ await this.waitForMountReconfigure();
1539
1868
  return this.client.readFile(this.session, this.vm, path);
1540
1869
  }
1541
1870
  async dispatchWrite(path, handler, nativeHandler, includeLocalMounts = true) {
@@ -1546,6 +1875,7 @@ export class NativeSidecarKernelProxy {
1546
1875
  await handler(local.mount, local.relativePath);
1547
1876
  return;
1548
1877
  }
1878
+ await this.waitForMountReconfigure();
1549
1879
  await nativeHandler();
1550
1880
  }
1551
1881
  resolveLocalMount(path) {
@@ -1621,6 +1951,10 @@ function buildCommandMap(commandGuestPaths) {
1621
1951
  ["node", "node"],
1622
1952
  ["npm", "node"],
1623
1953
  ["npx", "node"],
1954
+ // `python` / `python3` are served by the embedded Pyodide runtime,
1955
+ // mirroring how `node` is served by the embedded V8 runtime.
1956
+ ["python", "python"],
1957
+ ["python3", "python"],
1624
1958
  ]);
1625
1959
  for (const name of commandGuestPaths.keys()) {
1626
1960
  commands.set(name, "wasmvm");
@@ -1658,21 +1992,27 @@ function isMissingHostProcessError(error) {
1658
1992
  function errnoError(code, message) {
1659
1993
  return Object.assign(new Error(`${code}: ${message}`), { code });
1660
1994
  }
1995
+ // VirtualStat is a numeric, Node-default-shaped view: u64 fields above
1996
+ // Number.MAX_SAFE_INTEGER lose precision here, same as Node's non-bigint
1997
+ // fs.stat on the host.
1661
1998
  function toVirtualStat(stat) {
1662
1999
  return {
1663
2000
  mode: stat.mode,
1664
- size: stat.size,
1665
- blocks: stat.blocks,
1666
- dev: stat.dev,
1667
- rdev: stat.rdev,
2001
+ size: Number(stat.size),
2002
+ sizeExact: stat.size,
2003
+ blocks: Number(stat.blocks),
2004
+ dev: Number(stat.dev),
2005
+ rdev: Number(stat.rdev),
1668
2006
  isDirectory: stat.is_directory,
1669
2007
  isSymbolicLink: stat.is_symbolic_link,
1670
2008
  atimeMs: stat.atime_ms,
1671
2009
  mtimeMs: stat.mtime_ms,
1672
2010
  ctimeMs: stat.ctime_ms,
1673
2011
  birthtimeMs: stat.birthtime_ms,
1674
- ino: stat.ino,
1675
- nlink: stat.nlink,
2012
+ ino: Number(stat.ino),
2013
+ inoExact: stat.ino,
2014
+ nlink: Number(stat.nlink),
2015
+ nlinkExact: stat.nlink,
1676
2016
  uid: stat.uid,
1677
2017
  gid: stat.gid,
1678
2018
  };
@@ -1705,7 +2045,7 @@ function socketLookupKey(kind, request) {
1705
2045
  path: request.path ?? null,
1706
2046
  });
1707
2047
  }
1708
- export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, SidecarProcess, SidecarEventBufferOverflow, SidecarProcessError, SidecarProcessExited, } from "./native-process-client.js";
2048
+ export { NativeSidecarProcessClient, SidecarEventBufferOverflow, SidecarProcess, SidecarProcessError, SidecarProcessExited, } from "./native-process-client.js";
1709
2049
  export class AgentOsSidecarVmHandle {
1710
2050
  client;
1711
2051
  sessionId;