@rivet-dev/agentos-core 0.0.0-abc-fixes-1.d8c4d95 → 0.0.0-agent-acp-codex-session-benchmark.62b1f46

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 (128) hide show
  1. package/README.md +48 -48
  2. package/dist/agent-os.d.ts +215 -161
  3. package/dist/agent-os.js +1300 -2576
  4. package/dist/agentos-package.d.ts +27 -0
  5. package/dist/agentos-package.js +21 -0
  6. package/dist/base-filesystem.d.ts +1 -16
  7. package/dist/base-filesystem.js +1 -173
  8. package/dist/cron/cron-manager.d.ts +39 -14
  9. package/dist/cron/cron-manager.js +289 -128
  10. package/dist/cron/errors.d.ts +8 -0
  11. package/dist/cron/errors.js +16 -0
  12. package/dist/cron/index.d.ts +1 -3
  13. package/dist/cron/index.js +1 -2
  14. package/dist/cron/types.d.ts +2 -2
  15. package/dist/default-software.d.ts +8 -0
  16. package/dist/default-software.js +10 -0
  17. package/dist/filesystem-snapshot.d.ts +1 -1
  18. package/dist/filesystem-snapshot.js +1 -1
  19. package/dist/generated/AcpLimitsConfig.d.ts +4 -0
  20. package/dist/generated/AcpLimitsConfig.js +2 -0
  21. package/dist/generated/CreateVmConfig.d.ts +29 -0
  22. package/dist/generated/FsPermissionRule.d.ts +6 -0
  23. package/dist/generated/FsPermissionRule.js +1 -0
  24. package/dist/generated/FsPermissionRuleSet.d.ts +6 -0
  25. package/dist/generated/FsPermissionRuleSet.js +1 -0
  26. package/dist/generated/FsPermissionScope.d.ts +3 -0
  27. package/dist/generated/FsPermissionScope.js +1 -0
  28. package/dist/generated/HttpLimitsConfig.d.ts +3 -0
  29. package/dist/generated/HttpLimitsConfig.js +2 -0
  30. package/dist/generated/JsModuleResolution.d.ts +1 -0
  31. package/dist/generated/JsModuleResolution.js +2 -0
  32. package/dist/generated/JsRuntimeConfig.d.ts +31 -0
  33. package/dist/generated/JsRuntimeConfig.js +1 -0
  34. package/dist/generated/JsRuntimeLimitsConfig.d.ts +8 -0
  35. package/dist/generated/JsRuntimeLimitsConfig.js +2 -0
  36. package/dist/generated/JsRuntimePlatform.d.ts +1 -0
  37. package/dist/generated/JsRuntimePlatform.js +2 -0
  38. package/dist/generated/MountPluginDescriptor.d.ts +4 -0
  39. package/dist/generated/MountPluginDescriptor.js +2 -0
  40. package/dist/generated/NativeRootFilesystemConfig.d.ts +5 -0
  41. package/dist/generated/NativeRootFilesystemConfig.js +1 -0
  42. package/dist/generated/PatternPermissionRule.d.ts +6 -0
  43. package/dist/generated/PatternPermissionRule.js +1 -0
  44. package/dist/generated/PatternPermissionRuleSet.d.ts +6 -0
  45. package/dist/generated/PatternPermissionRuleSet.js +1 -0
  46. package/dist/generated/PatternPermissionScope.d.ts +3 -0
  47. package/dist/generated/PatternPermissionScope.js +1 -0
  48. package/dist/generated/PermissionMode.d.ts +1 -0
  49. package/dist/generated/PermissionMode.js +2 -0
  50. package/dist/generated/PermissionsPolicy.d.ts +10 -0
  51. package/dist/generated/PermissionsPolicy.js +1 -0
  52. package/dist/generated/PluginLimitsConfig.d.ts +4 -0
  53. package/dist/generated/PluginLimitsConfig.js +2 -0
  54. package/dist/generated/PythonLimitsConfig.d.ts +6 -0
  55. package/dist/generated/PythonLimitsConfig.js +2 -0
  56. package/dist/generated/ResourceLimitsConfig.d.ts +25 -0
  57. package/dist/generated/ResourceLimitsConfig.js +2 -0
  58. package/dist/generated/RootFilesystemConfig.d.ts +9 -0
  59. package/dist/generated/RootFilesystemConfig.js +1 -0
  60. package/dist/generated/RootFilesystemEntry.d.ts +13 -0
  61. package/dist/generated/RootFilesystemEntry.js +1 -0
  62. package/dist/generated/RootFilesystemEntryEncoding.d.ts +1 -0
  63. package/dist/generated/RootFilesystemEntryEncoding.js +2 -0
  64. package/dist/generated/RootFilesystemEntryKind.d.ts +1 -0
  65. package/dist/generated/RootFilesystemEntryKind.js +2 -0
  66. package/dist/generated/RootFilesystemLowerDescriptor.d.ts +7 -0
  67. package/dist/generated/RootFilesystemLowerDescriptor.js +1 -0
  68. package/dist/generated/RootFilesystemMode.d.ts +1 -0
  69. package/dist/generated/RootFilesystemMode.js +2 -0
  70. package/dist/generated/ToolLimitsConfig.d.ts +10 -0
  71. package/dist/generated/ToolLimitsConfig.js +2 -0
  72. package/dist/generated/VmDnsConfig.d.ts +6 -0
  73. package/dist/generated/VmDnsConfig.js +2 -0
  74. package/dist/generated/VmLimitsConfig.d.ts +18 -0
  75. package/dist/generated/VmLimitsConfig.js +1 -0
  76. package/dist/generated/VmListenPolicyConfig.d.ts +5 -0
  77. package/dist/generated/VmListenPolicyConfig.js +2 -0
  78. package/dist/generated/WasmLimitsConfig.d.ts +7 -0
  79. package/dist/generated/WasmLimitsConfig.js +2 -0
  80. package/dist/host-dir-mount.d.ts +5 -5
  81. package/dist/host-dir-mount.js +4 -4
  82. package/dist/host-tools-zod.js +27 -7
  83. package/dist/host-tools.d.ts +2 -9
  84. package/dist/host-tools.js +0 -18
  85. package/dist/index.d.ts +7 -4
  86. package/dist/index.js +6 -4
  87. package/dist/layers.d.ts +3 -3
  88. package/dist/layers.js +19 -10
  89. package/dist/memory-filesystem.d.ts +39 -0
  90. package/dist/memory-filesystem.js +375 -0
  91. package/dist/options-schema.d.ts +452 -0
  92. package/dist/options-schema.js +269 -0
  93. package/dist/overlay-filesystem.d.ts +1 -1
  94. package/dist/overlay-filesystem.js +3 -3
  95. package/dist/packages.d.ts +10 -123
  96. package/dist/packages.js +3 -329
  97. package/dist/runtime.d.ts +41 -184
  98. package/dist/sidecar/agentos-protocol.d.ts +232 -29
  99. package/dist/sidecar/agentos-protocol.js +456 -120
  100. package/dist/sidecar/binary.d.ts +2 -2
  101. package/dist/sidecar/binary.js +2 -2
  102. package/dist/sidecar/native-process-client.d.ts +5 -3
  103. package/dist/sidecar/native-process-client.js +5 -3
  104. package/dist/sidecar/permissions.d.ts +4 -3
  105. package/dist/sidecar/permissions.js +6 -23
  106. package/dist/sidecar/rpc-client.d.ts +74 -86
  107. package/dist/sidecar/rpc-client.js +422 -1418
  108. package/dist/test/file-system.d.ts +1 -1
  109. package/dist/test/file-system.js +3 -1
  110. package/dist/test/runtime.d.ts +2 -3
  111. package/dist/test/runtime.js +1 -1
  112. package/dist/test/terminal-harness.d.ts +5 -4
  113. package/dist/test/terminal-harness.js +12 -7
  114. package/dist/types.d.ts +12 -5
  115. package/package.json +37 -41
  116. package/dist/agents.d.ts +0 -63
  117. package/dist/agents.js +0 -42
  118. package/dist/cron/parse-schedule.d.ts +0 -25
  119. package/dist/cron/parse-schedule.js +0 -66
  120. package/dist/cron/schedule-driver.d.ts +0 -19
  121. package/dist/cron/timer-driver.d.ts +0 -17
  122. package/dist/cron/timer-driver.js +0 -61
  123. package/dist/runtime-compat.d.ts +0 -475
  124. package/dist/runtime-compat.js +0 -2010
  125. package/dist/sidecar/cargo.d.ts +0 -2
  126. package/dist/sidecar/cargo.js +0 -142
  127. package/fixtures/AGENTOS_SYSTEM_PROMPT.md +0 -21
  128. /package/dist/{cron/schedule-driver.js → generated/CreateVmConfig.js} +0 -0
@@ -1,20 +1,34 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { constants as osConstants } from "node:os";
3
3
  import { posix as posixPath } from "node:path";
4
- const SYNTHETIC_PID_BASE = 1_000_000;
5
- const MISSING_EXIT_EVENT_GRACE_MS = 500;
6
- const PROTECTED_READ_ONLY_GUEST_ROOTS = ["/etc/agentos"];
7
- const TRAILING_OUTPUT_DRAIN_INTERVAL_MS = 10;
8
- const TRAILING_OUTPUT_DRAIN_MAX_MS = 250;
9
- const TRAILING_OUTPUT_DRAIN_QUIET_TURNS = 2;
10
- async function drainTrailingProcessOutputTurn(delayMs = 0) {
11
- // Native-sidecar `process_output` events can lag one macrotask behind the
12
- // terminal `process_exited` notification for very short-lived processes, and
13
- // under suite load the sidecar event pump can need a little extra time to
14
- // flush delayed output through its listener callbacks.
15
- await new Promise((resolve) => {
16
- setTimeout(resolve, delayMs);
17
- });
4
+ function shouldLogStructuredSidecarEvent(name) {
5
+ const normalized = name.toLowerCase();
6
+ return (normalized === "limit_warning" ||
7
+ normalized.startsWith("security.") ||
8
+ normalized.includes("warning") ||
9
+ normalized.includes("failed") ||
10
+ normalized.includes("error"));
11
+ }
12
+ function decodeUtf8(data) {
13
+ return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
14
+ }
15
+ function formatStructuredSidecarDetail(detail) {
16
+ const entries = Object.entries(detail);
17
+ if (entries.length === 0) {
18
+ return "";
19
+ }
20
+ return entries
21
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`)
22
+ .join(" ");
23
+ }
24
+ function logStructuredSidecarEvent(name, detail) {
25
+ if (!shouldLogStructuredSidecarEvent(name)) {
26
+ return;
27
+ }
28
+ const formatted = formatStructuredSidecarDetail(detail);
29
+ console.warn(formatted
30
+ ? `[agent-os] sidecar ${name}: ${formatted}`
31
+ : `[agent-os] sidecar ${name}`);
18
32
  }
19
33
  const PREFERRED_SIGNAL_NAMES = [
20
34
  "SIGHUP",
@@ -58,98 +72,6 @@ const NON_CANONICAL_SIGNAL_NAMES = new Set([
58
72
  "SIGUNUSED",
59
73
  ]);
60
74
  const SIGNAL_NAME_BY_NUMBER = buildSignalNameByNumber();
61
- const DOUBLE_QUOTE_ESCAPABLE_CHARACTERS = new Set(['"', "\\", "$", "`"]);
62
- function appendDoubleQuotedEscape(current, character) {
63
- if (DOUBLE_QUOTE_ESCAPABLE_CHARACTERS.has(character)) {
64
- return current + character;
65
- }
66
- if (character === "\n") {
67
- return current;
68
- }
69
- return `${current}\\${character}`;
70
- }
71
- function parseSimpleExecCommand(command) {
72
- const tokens = [];
73
- let current = "";
74
- let quote = null;
75
- let escaped = false;
76
- for (const character of command) {
77
- if (quote === null) {
78
- if (escaped) {
79
- current += character;
80
- escaped = false;
81
- continue;
82
- }
83
- if (character === "\\") {
84
- escaped = true;
85
- continue;
86
- }
87
- if (character === "'" || character === '"') {
88
- quote = character;
89
- continue;
90
- }
91
- if (/\s/.test(character)) {
92
- if (current) {
93
- tokens.push(current);
94
- current = "";
95
- }
96
- continue;
97
- }
98
- if ("|&;<>()$`*?[]{}~!".includes(character)) {
99
- return null;
100
- }
101
- current += character;
102
- continue;
103
- }
104
- if (quote === "'") {
105
- if (character === "'") {
106
- quote = null;
107
- continue;
108
- }
109
- current += character;
110
- continue;
111
- }
112
- if (escaped) {
113
- current = appendDoubleQuotedEscape(current, character);
114
- escaped = false;
115
- continue;
116
- }
117
- if (character === "\\") {
118
- escaped = true;
119
- continue;
120
- }
121
- if (character === '"') {
122
- quote = null;
123
- continue;
124
- }
125
- if (character === "$" || character === "`") {
126
- return null;
127
- }
128
- current += character;
129
- }
130
- if (quote !== null || escaped) {
131
- return null;
132
- }
133
- if (current) {
134
- tokens.push(current);
135
- }
136
- if (tokens.length === 0) {
137
- return null;
138
- }
139
- if (tokens.some((token) => token.length === 0)) {
140
- return null;
141
- }
142
- return tokens;
143
- }
144
- function canUseDirectExec(driver, commandName) {
145
- return driver === "wasmvm" || (driver === "node" && commandName === "node");
146
- }
147
- function shellSingleQuote(value) {
148
- if (value.length === 0) {
149
- return "''";
150
- }
151
- return `'${value.replace(/'/g, `'"'"'`)}'`;
152
- }
153
75
  function buildSignalNameByNumber() {
154
76
  const signals = osConstants.signals;
155
77
  const names = new Map();
@@ -171,79 +93,110 @@ function buildSignalNameByNumber() {
171
93
  export function toSidecarSignalName(signal) {
172
94
  return SIGNAL_NAME_BY_NUMBER.get(signal) ?? String(signal);
173
95
  }
96
+ function toSidecarTimeoutMs(timeout) {
97
+ if (timeout === undefined) {
98
+ return undefined;
99
+ }
100
+ if (!Number.isFinite(timeout) || timeout < 0) {
101
+ throw new RangeError("process timeout must be a finite non-negative number");
102
+ }
103
+ return Math.trunc(timeout);
104
+ }
105
+ const processCompletion = Symbol("agentos.processCompletion");
174
106
  export class NativeSidecarKernelProxy {
175
107
  env;
176
108
  cwd;
177
- commands;
178
109
  vfs;
179
110
  processes = new Map();
180
- defaultExecCwd;
181
111
  client;
182
112
  session;
183
113
  vm;
114
+ ownsClient;
184
115
  localMounts;
185
- commandDrivers;
186
- onWasmCommandResolved;
116
+ baseSidecarMounts;
187
117
  onDispose;
188
118
  trackedProcesses = new Map();
189
119
  trackedProcessesById = new Map();
190
- listenerLookups = new Map();
191
- boundUdpLookups = new Map();
192
- signalStates = new Map();
193
- signalRefreshes = new Map();
194
- sidecarProcessSnapshot = [];
195
120
  processSnapshotRefresh = null;
196
- observedProcessStartTimes = new Map();
197
121
  rootView;
198
- zombieTimerCountValue = 0;
199
- zombieTimerCountRefresh = null;
200
122
  disposed = false;
123
+ disposePromise = null;
201
124
  pumpError = null;
202
- nextSyntheticPid = SYNTHETIC_PID_BASE;
125
+ mountReconfigurePromise = null;
203
126
  eventPumpAbortController = new AbortController();
204
127
  eventPump;
205
128
  constructor(options) {
206
129
  this.client = options.client;
207
130
  this.session = options.session;
208
131
  this.vm = options.vm;
132
+ this.ownsClient = options.ownsClient ?? true;
209
133
  this.env = { ...options.env };
210
134
  this.cwd = options.cwd;
211
- this.defaultExecCwd = options.defaultExecCwd;
212
135
  this.localMounts = [...options.localMounts].sort((left, right) => right.path.length - left.path.length);
213
- this.commandDrivers = buildCommandMap(options.commandGuestPaths);
214
- this.onWasmCommandResolved = options.onWasmCommandResolved;
136
+ const localMountPaths = new Set(this.localMounts.map((mount) => mount.path));
137
+ this.baseSidecarMounts = options.sidecarMounts.filter((mount) => mount.plugin.id !== "js_bridge" ||
138
+ !localMountPaths.has(posixPath.normalize(mount.guestPath)));
215
139
  this.onDispose = options.onDispose;
216
- this.commands = this.commandDrivers;
217
- this.vfs = this.createFilesystemView(true);
218
- this.rootView = this.createFilesystemView(false);
140
+ this.vfs = this.createFilesystemView();
141
+ this.rootView = this.vfs;
219
142
  this.eventPump = this.runEventPump();
220
- void this.eventPump.catch(() => { });
143
+ void this.eventPump;
221
144
  }
222
145
  createRootView() {
223
146
  return this.rootView;
224
147
  }
225
- get zombieTimerCount() {
226
- if (!this.zombieTimerCountRefresh) {
227
- this.zombieTimerCountRefresh = this.refreshZombieTimerCount();
228
- }
229
- return this.zombieTimerCountValue;
230
- }
231
- registerCommandGuestPaths(commandGuestPaths) {
232
- for (const name of commandGuestPaths.keys()) {
233
- this.commandDrivers.set(name, "wasmvm");
234
- }
148
+ /** Resolve the host-only backing store for an exact js_bridge mount id. */
149
+ hostFilesystemForMount(mountId) {
150
+ const normalized = posixPath.normalize(mountId);
151
+ return this.localMounts.find((mount) => mount.path === normalized)?.fs;
235
152
  }
236
153
  async dispose() {
237
154
  if (this.disposed) {
238
155
  return;
239
156
  }
157
+ if (this.disposePromise) {
158
+ return this.disposePromise;
159
+ }
160
+ const attempt = this.disposeOnce();
161
+ this.disposePromise = attempt;
162
+ try {
163
+ await attempt;
164
+ }
165
+ finally {
166
+ if (this.disposePromise === attempt) {
167
+ this.disposePromise = null;
168
+ }
169
+ }
170
+ }
171
+ async disposeOnce() {
172
+ const errors = [];
173
+ try {
174
+ await this.mountReconfigurePromise;
175
+ }
176
+ catch (error) {
177
+ errors.push(toError(error));
178
+ }
179
+ const liveProcesses = [...this.trackedProcessesById.values()].filter((entry) => !entry.settled);
180
+ const signalResults = await Promise.allSettled(liveProcesses.map((entry) => this.signalProcess(entry, 15)));
181
+ for (const result of signalResults) {
182
+ if (result.status === "rejected") {
183
+ errors.push(toError(result.reason));
184
+ }
185
+ }
186
+ try {
187
+ await this.client.disposeVm(this.session, this.vm);
188
+ }
189
+ catch (error) {
190
+ errors.push(toError(error));
191
+ throw new AggregateError(errors, "failed to dispose sidecar VM");
192
+ }
193
+ // Remote disposal is authoritative. Only now make local teardown
194
+ // irreversible; a rejected/failed request leaves the route and lease live so
195
+ // the caller can retry without manufacturing local success.
240
196
  this.disposed = true;
241
197
  this.eventPumpAbortController.abort();
242
- const liveProcesses = [...this.trackedProcesses.values()].filter((entry) => entry.exitCode === null);
243
- await Promise.allSettled(liveProcesses.map((entry) => this.signalProcess(entry, 15)));
244
- await this.client.disposeVm(this.session, this.vm).catch(() => { });
245
198
  for (const entry of liveProcesses) {
246
- if (entry.exitCode === null) {
199
+ if (!entry.settled) {
247
200
  // The sidecar dispose path already performs TERM/KILL escalation for any
248
201
  // guest executions that are still live. Resolve local waiters eagerly so
249
202
  // VM teardown does not hang on killed ACP adapter processes that never
@@ -251,191 +204,107 @@ export class NativeSidecarKernelProxy {
251
204
  this.finishProcess(entry, 143);
252
205
  }
253
206
  }
254
- await this.client.dispose().catch(() => { });
255
- await this.eventPump.catch(() => { });
256
- await this.onDispose?.().catch(() => { });
257
- }
258
- async exec(command, options) {
259
- if (!this.commands.has("sh")) {
260
- throw new Error(`native sidecar exec requires guest shell command 'sh': ${command}`);
261
- }
262
- const stdoutChunks = [];
263
- const stderrChunks = [];
264
- const effectiveCwd = options?.cwd ?? this.defaultExecCwd ?? this.cwd;
265
- const parsedCommand = parseSimpleExecCommand(command);
266
- const resolveExecPath = (targetPath) => targetPath.startsWith("/")
267
- ? posixPath.normalize(targetPath)
268
- : posixPath.normalize(posixPath.join(effectiveCwd, targetPath));
269
- const runAndCapture = async (proc, stdinOverride, readExitCode) => {
270
- if (stdinOverride !== undefined) {
271
- proc.writeStdin(stdinOverride);
272
- }
273
- else if (options?.stdin !== undefined) {
274
- proc.writeStdin(options.stdin);
207
+ // Only tear down the shared sidecar process when this proxy owns it. VMs
208
+ // leased from an `AgentOsSidecar` handle share one process, which is
209
+ // disposed when the handle is disposed.
210
+ if (this.ownsClient) {
211
+ try {
212
+ await this.client.dispose();
275
213
  }
276
- // `kernel.exec()` is a non-interactive run-to-completion API: when the
277
- // caller does not opt into a streaming stdin handle, the guest process
278
- // should observe EOF after any provided input so commands like
279
- // `node -e ...` do not linger behind an inherited open stdin pipe.
280
- proc.closeStdin();
281
- const waitPromise = proc.wait();
282
- const shellExitCode = typeof options?.timeout === "number"
283
- ? await new Promise((resolve) => {
284
- const timer = setTimeout(() => {
285
- proc.kill(9);
286
- void proc.wait().then(resolve);
287
- }, options.timeout);
288
- void waitPromise.then((code) => {
289
- clearTimeout(timer);
290
- resolve(code);
291
- });
292
- })
293
- : await waitPromise;
294
- const exitCode = readExitCode
295
- ? await readExitCode().catch(() => shellExitCode)
296
- : shellExitCode;
297
- await drainTrailingProcessOutputTurn();
298
- return {
299
- exitCode,
300
- stdout: Buffer.concat(stdoutChunks.map((chunk) => Buffer.from(chunk))).toString("utf8"),
301
- stderr: Buffer.concat(stderrChunks.map((chunk) => Buffer.from(chunk))).toString("utf8"),
302
- };
303
- };
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
- };
214
+ catch (error) {
215
+ errors.push(toError(error));
316
216
  }
317
- return this.exec(parsedCommand[2], options);
318
217
  }
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: "" };
218
+ try {
219
+ await this.eventPump;
328
220
  }
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
- };
221
+ catch (error) {
222
+ errors.push(toError(error));
340
223
  }
341
- const parsedCommandDriver = parsedCommand
342
- ? this.commands.get(parsedCommand[0])
343
- : undefined;
344
- const requiresShellWrappedWasmCwd = parsedCommandDriver === "wasmvm" && parsedCommand?.[0] === "pwd";
345
- if (parsedCommand &&
346
- parsedCommandDriver &&
347
- canUseDirectExec(parsedCommandDriver, parsedCommand[0]) &&
348
- !requiresShellWrappedWasmCwd) {
349
- if (parsedCommandDriver === "wasmvm") {
350
- this.onWasmCommandResolved?.(parsedCommand[0]);
351
- }
352
- return runAndCapture(this.spawn(parsedCommand[0], parsedCommand.slice(1), {
353
- ...options,
354
- cwd: effectiveCwd,
355
- onStdout: (chunk) => {
356
- stdoutChunks.push(chunk);
357
- options?.onStdout?.(chunk);
358
- },
359
- onStderr: (chunk) => {
360
- stderrChunks.push(chunk);
361
- options?.onStderr?.(chunk);
362
- },
363
- }));
224
+ try {
225
+ await this.onDispose?.();
226
+ }
227
+ catch (error) {
228
+ errors.push(toError(error));
229
+ }
230
+ // Drop all per-VM tracking state so a disposed proxy retains nothing.
231
+ for (const entry of this.trackedProcesses.values()) {
232
+ entry.onStdout.clear();
233
+ entry.onStderr.clear();
234
+ }
235
+ this.trackedProcesses.clear();
236
+ this.trackedProcessesById.clear();
237
+ this.localMounts.length = 0;
238
+ if (errors.length > 0) {
239
+ throw new AggregateError(errors, "failed to dispose sidecar VM");
364
240
  }
365
- const proc = this.spawn("sh", ["-c", command], {
366
- ...options,
367
- cwd: effectiveCwd,
368
- onStdout: (chunk) => {
369
- stdoutChunks.push(chunk);
370
- options?.onStdout?.(chunk);
371
- },
372
- onStderr: (chunk) => {
373
- stderrChunks.push(chunk);
374
- options?.onStderr?.(chunk);
375
- },
376
- });
377
- return runAndCapture(proc);
241
+ }
242
+ /** Test-only snapshot of the per-VM tracking collection sizes. */
243
+ __trackingSizesForTest() {
244
+ return {
245
+ trackedProcesses: this.trackedProcesses.size,
246
+ trackedProcessesById: this.trackedProcessesById.size,
247
+ localMounts: this.localMounts.length,
248
+ };
249
+ }
250
+ /** Test-only handle to a tracked entry so its listener Sets can be inspected. */
251
+ __trackedEntryForTest(pid) {
252
+ return this.trackedProcesses.get(pid);
253
+ }
254
+ async exec(command, options) {
255
+ const captureOutput = options?.captureStdio !== false;
256
+ const proc = (await this.spawnTracked("sh", [], {
257
+ env: options?.env,
258
+ cwd: options?.cwd,
259
+ timeout: options?.timeout,
260
+ onStdout: options?.onStdout,
261
+ onStderr: options?.onStderr,
262
+ shellCommand: command,
263
+ captureOutput,
264
+ }));
265
+ if (options?.stdin !== undefined) {
266
+ await proc.writeStdin(options.stdin);
267
+ }
268
+ await proc.closeStdin();
269
+ const completion = await proc[processCompletion];
270
+ return {
271
+ exitCode: completion.exitCode,
272
+ stdout: decodeUtf8(completion.stdout),
273
+ stderr: decodeUtf8(completion.stderr),
274
+ };
378
275
  }
379
276
  async execArgv(command, args = [], options) {
380
- const stdoutChunks = [];
381
- const stderrChunks = [];
382
- const effectiveCwd = options?.cwd ?? this.defaultExecCwd ?? this.cwd;
277
+ const captureOutput = options?.captureStdio !== false;
278
+ const requestedCwd = options?.cwd;
383
279
  const runAndCapture = async (proc) => {
384
280
  if (options?.stdin !== undefined) {
385
- proc.writeStdin(options.stdin);
281
+ await proc.writeStdin(options.stdin);
386
282
  }
387
- proc.closeStdin();
388
- const waitPromise = proc.wait();
389
- const exitCode = typeof options?.timeout === "number"
390
- ? await new Promise((resolve) => {
391
- const timer = setTimeout(() => {
392
- proc.kill(9);
393
- void proc.wait().then(resolve);
394
- }, options.timeout);
395
- void waitPromise.then((code) => {
396
- clearTimeout(timer);
397
- resolve(code);
398
- });
399
- })
400
- : await waitPromise;
401
- await drainTrailingProcessOutputTurn();
283
+ await proc.closeStdin();
284
+ const completion = await proc[processCompletion];
402
285
  return {
403
- exitCode,
404
- stdout: Buffer.concat(stdoutChunks.map((chunk) => Buffer.from(chunk))).toString("utf8"),
405
- stderr: Buffer.concat(stderrChunks.map((chunk) => Buffer.from(chunk))).toString("utf8"),
286
+ exitCode: completion.exitCode,
287
+ stdout: decodeUtf8(completion.stdout),
288
+ stderr: decodeUtf8(completion.stderr),
406
289
  };
407
290
  };
408
- if (this.commands.get(command) === "wasmvm") {
409
- this.onWasmCommandResolved?.(command);
410
- }
411
- return runAndCapture(this.spawn(command, [...args], {
412
- ...options,
413
- cwd: effectiveCwd,
414
- onStdout: (chunk) => {
415
- stdoutChunks.push(chunk);
416
- options?.onStdout?.(chunk);
417
- },
418
- onStderr: (chunk) => {
419
- stderrChunks.push(chunk);
420
- options?.onStderr?.(chunk);
421
- },
291
+ const proc = (await this.spawnTracked(command, [...args], {
292
+ env: options?.env,
293
+ cwd: requestedCwd,
294
+ timeout: options?.timeout,
295
+ onStdout: options?.onStdout,
296
+ onStderr: options?.onStderr,
297
+ captureOutput,
422
298
  }));
299
+ return runAndCapture(proc);
423
300
  }
424
- spawn(command, args, options) {
425
- let spawnCommand = command;
426
- let spawnArgs = [...args];
427
- const shellOption = options?.shell;
428
- if (shellOption === true || typeof shellOption === "string") {
429
- // Node's shell mode hands the raw command line to the shell. Shell
430
- // grammar belongs to the guest shell, so the bridge never parses it.
431
- if (!this.commands.has("sh")) {
432
- throw new Error(`native sidecar shell-mode spawn requires guest shell command 'sh': ${command}`);
433
- }
434
- spawnCommand = "sh";
435
- spawnArgs = ["-c", [command, ...args].join(" ")];
436
- }
437
- const pid = this.nextSyntheticPid++;
438
- const processId = `proc-${pid}`;
301
+ async spawn(command, args, options) {
302
+ return this.spawnTracked(command, args, options);
303
+ }
304
+ async spawnTracked(command, args, options) {
305
+ const spawnCommand = command;
306
+ const spawnArgs = [...args];
307
+ const shellCommand = options?.shellCommand;
439
308
  let resolveWait;
440
309
  let rejectWait;
441
310
  const waitPromise = new Promise((resolve, reject) => {
@@ -443,437 +312,85 @@ export class NativeSidecarKernelProxy {
443
312
  rejectWait = reject;
444
313
  });
445
314
  const entry = {
446
- pid,
447
- processId,
448
- command: spawnCommand,
315
+ pid: null,
316
+ processId: null,
317
+ command: shellCommand === undefined ? spawnCommand : undefined,
318
+ shellCommand,
449
319
  args: spawnArgs,
450
- driver: spawnCommand === "node" ? "node" : "wasmvm",
451
- cwd: options?.cwd ?? this.cwd,
452
- env: {
453
- ...(options?.env ?? {}),
454
- ...(options?.streamStdin ? { SECURE_EXEC_KEEP_STDIN_OPEN: "1" } : {}),
455
- },
456
- startTime: Date.now(),
457
- exitTime: null,
458
- hostPid: null,
320
+ requestedCwd: options?.cwd,
321
+ pty: options?.pty,
322
+ env: options?.env ? { ...options.env } : undefined,
323
+ keepStdinOpen: options?.streamStdin,
324
+ timeoutMs: toSidecarTimeoutMs(options?.timeout),
325
+ captureOutput: options?.captureOutput,
459
326
  exitCode: null,
460
- started: false,
461
- startPromise: Promise.resolve(),
462
327
  waitPromise,
463
328
  resolveWait,
464
329
  rejectWait,
330
+ settled: false,
465
331
  onStdout: new Set(options?.onStdout ? [options.onStdout] : []),
466
332
  onStderr: new Set(options?.onStderr ? [options.onStderr] : []),
467
- pendingStdin: [],
468
- stdinFlushPromise: null,
469
- pendingCloseStdin: false,
470
- pendingKillSignal: null,
471
- waitWithFallbackPromise: null,
472
- hostExitObservedAt: null,
473
- outputGeneration: 0,
474
333
  };
475
- this.trackedProcesses.set(pid, entry);
476
- this.trackedProcessesById.set(processId, entry);
477
- this.updateTrackedProcessSnapshot(entry);
334
+ await this.startTrackedProcess(entry);
335
+ if (entry.pid === null) {
336
+ throw new Error("sidecar did not return a kernel pid for the process");
337
+ }
338
+ const pid = entry.pid;
478
339
  const proc = {
340
+ [processCompletion]: waitPromise,
479
341
  pid,
480
- writeStdin: (data) => {
342
+ writeStdin: async (data) => {
481
343
  if (entry.exitCode !== null) {
482
344
  return;
483
345
  }
484
- entry.pendingStdin.push(data);
485
- void this.flushPendingStdin(entry).catch((error) => {
486
- this.handleBackgroundProcessError(entry, error);
487
- });
346
+ await this.client.writeStdin(this.session, this.vm, this.processIdFor(entry), data);
488
347
  },
489
- closeStdin: () => {
490
- entry.pendingCloseStdin = true;
491
- void this.closeTrackedStdin(entry).catch((error) => {
492
- this.handleBackgroundProcessError(entry, error);
493
- });
494
- },
495
- kill: (signal = 15) => {
348
+ closeStdin: async () => {
496
349
  if (entry.exitCode !== null) {
497
350
  return;
498
351
  }
499
- entry.pendingKillSignal = signal;
500
- void entry.startPromise.then(async () => {
501
- if (entry.exitCode !== null || entry.pendingKillSignal === null) {
502
- return;
503
- }
504
- const pendingSignal = entry.pendingKillSignal;
505
- entry.pendingKillSignal = null;
506
- await this.signalProcess(entry, pendingSignal);
507
- });
508
- },
509
- wait: async () => {
510
- const exitCode = await this.waitForTrackedProcess(entry);
511
- await this.drainTrailingProcessOutput(entry);
512
- return exitCode;
352
+ await this.client.closeStdin(this.session, this.vm, this.processIdFor(entry));
513
353
  },
354
+ kill: (signal = 15) => this.signalProcess(entry, signal),
355
+ wait: () => this.waitForTrackedProcess(entry),
514
356
  get exitCode() {
515
357
  return entry.exitCode;
516
358
  },
517
359
  };
518
- entry.startPromise = this.startTrackedProcess(entry).catch((error) => {
519
- const normalized = error instanceof Error ? error : new Error(String(error));
520
- const stderr = new TextEncoder().encode(`${normalized.message}\n`);
521
- for (const handler of entry.onStderr) {
522
- handler(stderr);
523
- }
524
- this.finishProcess(entry, 1);
525
- });
526
360
  return proc;
527
361
  }
528
- openShell(options) {
362
+ async openShell(options) {
529
363
  const stdoutHandlers = new Set();
530
364
  const stderrHandlers = new Set();
531
- const command = options?.command ?? "sh";
532
- const args = options?.args ??
533
- (command === "sh" || command === "/bin/sh" ? ["-i"] : []);
534
- const synthesizePrompt = !options?.command && !options?.args;
535
- const autoCloseExplicitCommandStdin = Boolean(options?.command) && !["sh", "/bin/sh", "bash"].includes(command);
536
- const promptText = "sh-0.4$ ";
537
- const textEncoder = new TextEncoder();
538
- const textDecoder = new TextDecoder();
539
- const execCommand = this.exec.bind(this);
540
- const spawnCommand = this.spawn.bind(this);
541
- const sanitizeSyntheticShellText = (value) => value
542
- .replace(/\u001b\[[0-9;]*m/g, "")
543
- .replace(/^.*WARN could not retrieve pid for child process\n?/gm, "")
544
- .replace(/^ProcessExitError:.*\n(?:\s+at .*\n)*/gm, "");
545
- let bufferedInput = "";
546
- let bufferedCommand = "";
547
- let activeForegroundProcess = null;
548
- let shellEnv = { ...(options?.env ?? {}) };
549
- let shellCwd = options?.cwd ?? this.cwd;
550
- let syntheticCommandQueue = Promise.resolve();
551
- let promptTimer = null;
552
- let closeStdinTimer = null;
553
- let commandInFlight = false;
554
- let syntheticCursorAtLineStart = true;
555
- const syntheticPid = this.nextSyntheticPid++;
556
- let syntheticExitCode = null;
557
- let resolveSyntheticWait;
558
- const syntheticWaitPromise = new Promise((resolve) => {
559
- resolveSyntheticWait = resolve;
560
- });
561
- const clearPromptTimer = () => {
562
- if (promptTimer !== null) {
563
- clearTimeout(promptTimer);
564
- promptTimer = null;
565
- }
566
- };
567
- const clearCloseStdinTimer = () => {
568
- if (closeStdinTimer !== null) {
569
- clearTimeout(closeStdinTimer);
570
- closeStdinTimer = null;
571
- }
572
- };
573
- const normalizeSyntheticTerminalText = (text) => text.replace(/\r?\n/g, "\r\n");
574
- const updateSyntheticCursor = (text) => {
575
- if (!text) {
576
- return;
577
- }
578
- syntheticCursorAtLineStart = /(?:\r\n)$/.test(text);
579
- };
580
- const emitSyntheticStdout = (text) => {
581
- if (!text) {
582
- return;
583
- }
584
- const normalized = normalizeSyntheticTerminalText(text);
585
- updateSyntheticCursor(normalized);
586
- const chunk = textEncoder.encode(normalized);
587
- for (const handler of stdoutHandlers) {
588
- handler(chunk);
589
- }
590
- };
591
- const emitSyntheticTerminal = (text) => {
592
- if (!text) {
593
- return;
594
- }
595
- const normalized = normalizeSyntheticTerminalText(text);
596
- updateSyntheticCursor(normalized);
597
- const chunk = textEncoder.encode(normalized);
598
- for (const handler of stdoutHandlers) {
599
- handler(chunk);
600
- }
601
- };
602
- const finishSyntheticShell = (exitCode) => {
603
- if (syntheticExitCode !== null) {
604
- return;
605
- }
606
- syntheticExitCode = exitCode;
607
- clearPromptTimer();
608
- resolveSyntheticWait(exitCode);
609
- };
610
- const commandNeedsContinuation = (source) => {
611
- let singleQuoted = false;
612
- let doubleQuoted = false;
613
- let escaped = false;
614
- for (const character of source) {
615
- if (escaped) {
616
- escaped = false;
617
- continue;
618
- }
619
- if (character === "\\") {
620
- escaped = true;
621
- continue;
622
- }
623
- if (!doubleQuoted && character === "'") {
624
- singleQuoted = !singleQuoted;
625
- continue;
626
- }
627
- if (!singleQuoted && character === '"') {
628
- doubleQuoted = !doubleQuoted;
629
- }
630
- }
631
- return singleQuoted || doubleQuoted || escaped;
632
- };
633
- const emitPrompt = () => {
634
- if (!synthesizePrompt) {
635
- return;
636
- }
637
- if (syntheticExitCode !== null) {
638
- return;
639
- }
640
- commandInFlight = false;
641
- const promptPrefix = syntheticCursorAtLineStart ? "" : "\r\n";
642
- const promptChunk = textEncoder.encode(`${promptPrefix}${promptText}`);
643
- for (const handler of stdoutHandlers) {
644
- handler(promptChunk);
645
- }
646
- syntheticCursorAtLineStart = false;
647
- };
648
- const schedulePrompt = (delayMs) => {
649
- if (!synthesizePrompt) {
650
- return;
651
- }
652
- clearPromptTimer();
653
- promptTimer = setTimeout(() => {
654
- promptTimer = null;
655
- emitPrompt();
656
- }, delayMs);
657
- };
658
- const parseForegroundCommand = (source) => {
659
- const parsed = parseSimpleExecCommand(source);
660
- const driver = parsed ? this.commands.get(parsed[0]) : undefined;
661
- if (!parsed ||
662
- !canUseDirectExec(driver, parsed[0]) ||
663
- (driver === "wasmvm" && parsed[0] === "pwd")) {
664
- return null;
665
- }
666
- return parsed;
667
- };
668
- const writeForegroundInput = (proc, data) => {
669
- if (typeof data === "string") {
670
- for (const character of data) {
671
- proc.writeStdin(character);
672
- }
673
- return;
674
- }
675
- for (const byte of data) {
676
- proc.writeStdin(new Uint8Array([byte]));
677
- }
678
- };
679
365
  let onData = null;
680
366
  stdoutHandlers.add((data) => onData?.(data));
681
367
  if (options?.onStderr) {
682
368
  stderrHandlers.add(options.onStderr);
683
369
  }
684
- if (synthesizePrompt) {
685
- schedulePrompt(0);
686
- return {
687
- pid: syntheticPid,
688
- write(data) {
689
- if (syntheticExitCode !== null) {
690
- return;
691
- }
692
- if (activeForegroundProcess) {
693
- const rawText = typeof data === "string"
694
- ? data
695
- : Buffer.from(data).toString("utf8");
696
- if (rawText.includes("\u0003")) {
697
- const [beforeInterrupt] = rawText.split("\u0003");
698
- if (beforeInterrupt) {
699
- writeForegroundInput(activeForegroundProcess, beforeInterrupt);
700
- }
701
- emitSyntheticTerminal("^C\n");
702
- activeForegroundProcess.kill(2);
703
- return;
704
- }
705
- writeForegroundInput(activeForegroundProcess, data);
706
- return;
707
- }
708
- const rawText = typeof data === "string"
709
- ? data
710
- : Buffer.from(data).toString("utf8");
711
- let text = rawText;
712
- if (rawText.includes("\u0003")) {
713
- const segments = rawText.split("\u0003");
714
- bufferedInput = "";
715
- bufferedCommand = "";
716
- for (let index = 0; index < segments.length - 1; index += 1) {
717
- emitSyntheticTerminal("^C\n");
718
- emitPrompt();
719
- }
720
- text = segments[segments.length - 1] ?? "";
721
- }
722
- if (text.includes("\u0004") &&
723
- bufferedInput.length === 0 &&
724
- bufferedCommand.length === 0) {
725
- finishSyntheticShell(0);
726
- return;
727
- }
728
- bufferedInput += text.replace(/\u0004/g, "");
729
- while (true) {
730
- const newlineIndex = bufferedInput.indexOf("\n");
731
- if (newlineIndex < 0) {
732
- break;
733
- }
734
- const line = bufferedInput
735
- .slice(0, newlineIndex)
736
- .replace(/\r$/, "");
737
- bufferedInput = bufferedInput.slice(newlineIndex + 1);
738
- emitSyntheticStdout(`${line}\n`);
739
- const nextCommand = bufferedCommand
740
- ? `${bufferedCommand}\n${line}`
741
- : line;
742
- if (commandNeedsContinuation(nextCommand)) {
743
- bufferedCommand = nextCommand;
744
- continue;
745
- }
746
- bufferedCommand = "";
747
- syntheticCommandQueue = syntheticCommandQueue
748
- .then(async () => {
749
- const trimmed = nextCommand.trim();
750
- if (!trimmed) {
751
- emitPrompt();
752
- return;
753
- }
754
- const exitMatch = trimmed.match(/^exit(?:\s+(-?\d+))?$/);
755
- if (exitMatch) {
756
- finishSyntheticShell(Number.parseInt(exitMatch[1] ?? "0", 10));
757
- return;
758
- }
759
- const exportMatch = trimmed.match(/^export\s+([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
760
- if (exportMatch) {
761
- shellEnv = {
762
- ...shellEnv,
763
- [exportMatch[1]]: exportMatch[2],
764
- };
765
- emitPrompt();
766
- return;
767
- }
768
- const cdMatch = trimmed.match(/^cd(?:\s+(.*))?$/);
769
- if (cdMatch) {
770
- const target = cdMatch[1]?.trim() || "/";
771
- shellCwd = target.startsWith("/")
772
- ? posixPath.normalize(target)
773
- : posixPath.normalize(posixPath.join(shellCwd, target));
774
- emitPrompt();
775
- return;
776
- }
777
- const foregroundCommand = parseForegroundCommand(trimmed);
778
- if (foregroundCommand) {
779
- const proc = spawnCommand(foregroundCommand[0], foregroundCommand.slice(1), {
780
- env: shellEnv,
781
- cwd: shellCwd,
782
- streamStdin: true,
783
- onStdout: (chunk) => emitSyntheticTerminal(textDecoder.decode(chunk)),
784
- onStderr: (chunk) => emitSyntheticTerminal(textDecoder.decode(chunk)),
785
- });
786
- activeForegroundProcess = proc;
787
- try {
788
- await proc.wait();
789
- }
790
- finally {
791
- if (activeForegroundProcess === proc) {
792
- activeForegroundProcess = null;
793
- }
794
- }
795
- emitPrompt();
796
- return;
797
- }
798
- const result = await execCommand(nextCommand, {
799
- env: shellEnv,
800
- cwd: shellCwd,
801
- });
802
- const sanitizedStdout = sanitizeSyntheticShellText(result.stdout);
803
- if (sanitizedStdout) {
804
- emitSyntheticStdout(sanitizedStdout);
805
- }
806
- const sanitizedStderr = sanitizeSyntheticShellText(result.stderr).replace(/^error: failed to execute command '([^']+)': .*$/gm, "error: command not found: $1");
807
- if (sanitizedStderr) {
808
- emitSyntheticTerminal(sanitizedStderr);
809
- }
810
- emitPrompt();
811
- })
812
- .catch((error) => {
813
- const message = error instanceof Error ? error.message : String(error);
814
- emitSyntheticTerminal(`${message}\n`);
815
- emitPrompt();
816
- });
817
- }
818
- },
819
- get onData() {
820
- return onData;
821
- },
822
- set onData(handler) {
823
- onData = handler;
824
- },
825
- resize() {
826
- // Synthetic shells are terminal-less.
827
- },
828
- kill(signal = 15) {
829
- finishSyntheticShell(128 + signal);
830
- },
831
- wait() {
832
- return syntheticWaitPromise;
833
- },
834
- };
835
- }
836
- const proc = this.spawn(command, args, {
370
+ const proc = await this.spawnTracked(options?.command, options?.args ?? [], {
837
371
  env: options?.env,
838
372
  cwd: options?.cwd,
839
- streamStdin: true,
373
+ pty: { cols: options?.cols, rows: options?.rows },
840
374
  onStdout: (chunk) => {
841
375
  for (const handler of stdoutHandlers) {
842
376
  handler(chunk);
843
377
  }
844
- if (commandInFlight) {
845
- schedulePrompt(120);
846
- }
847
378
  },
848
379
  onStderr: (chunk) => {
849
380
  for (const handler of stderrHandlers) {
850
381
  handler(chunk);
851
382
  }
852
- if (commandInFlight) {
853
- schedulePrompt(120);
854
- }
855
383
  },
856
384
  });
385
+ const entry = this.trackedProcesses.get(proc.pid);
386
+ if (!entry) {
387
+ throw new Error(`sidecar shell process ${proc.pid} is not tracked`);
388
+ }
857
389
  return {
858
390
  pid: proc.pid,
391
+ processId: this.processIdFor(entry),
859
392
  write(data) {
860
- if (synthesizePrompt) {
861
- return;
862
- }
863
- proc.writeStdin(data);
864
- if (autoCloseExplicitCommandStdin) {
865
- clearCloseStdinTimer();
866
- closeStdinTimer = setTimeout(() => {
867
- closeStdinTimer = null;
868
- proc.closeStdin();
869
- }, 100);
870
- }
871
- if (synthesizePrompt &&
872
- typeof data === "string" &&
873
- (data.includes("\n") || data.includes("\r"))) {
874
- commandInFlight = true;
875
- schedulePrompt(120);
876
- }
393
+ return proc.writeStdin(data);
877
394
  },
878
395
  get onData() {
879
396
  return onData;
@@ -881,298 +398,177 @@ export class NativeSidecarKernelProxy {
881
398
  set onData(handler) {
882
399
  onData = handler;
883
400
  },
884
- resize() {
885
- // The current stdio-native path is process-backed rather than PTY-backed.
886
- },
887
- kill(signal) {
888
- clearCloseStdinTimer();
889
- clearPromptTimer();
890
- proc.kill(signal);
401
+ resize: (cols, rows) => {
402
+ const entry = this.trackedProcesses.get(proc.pid);
403
+ if (!entry || entry.exitCode !== null) {
404
+ return;
405
+ }
406
+ return this.client.resizePty(this.session, this.vm, this.processIdFor(entry), Math.trunc(cols), Math.trunc(rows));
891
407
  },
408
+ kill: (signal) => proc.kill(signal),
892
409
  wait() {
893
- clearPromptTimer();
894
410
  return proc.wait();
895
411
  },
896
412
  };
897
413
  }
898
- async connectTerminal(options) {
899
- const stdin = process.stdin;
900
- const stdout = process.stdout;
901
- const { onData, ...shellOptions } = options ?? {};
902
- const shell = this.openShell({
903
- ...shellOptions,
904
- onStderr: shellOptions.onStderr ??
905
- ((data) => {
906
- process.stderr.write(data);
907
- }),
908
- });
909
- const outputHandler = onData ??
910
- ((data) => {
911
- stdout.write(data);
912
- });
913
- const restoreRawMode = stdin.isTTY && typeof stdin.setRawMode === "function";
914
- const onStdinData = (data) => {
915
- shell.write(data);
916
- };
917
- const onResize = () => {
918
- shell.resize(stdout.columns, stdout.rows);
919
- };
920
- let cleanedUp = false;
921
- const cleanup = () => {
922
- if (cleanedUp) {
923
- return;
924
- }
925
- cleanedUp = true;
926
- stdin.removeListener("data", onStdinData);
927
- stdin.pause();
928
- if (restoreRawMode) {
929
- stdin.setRawMode(false);
930
- }
931
- if (stdout.isTTY) {
932
- stdout.removeListener("resize", onResize);
933
- }
934
- };
935
- try {
936
- if (restoreRawMode) {
937
- stdin.setRawMode(true);
938
- }
939
- stdin.on("data", onStdinData);
940
- stdin.resume();
941
- shell.onData = outputHandler;
942
- if (stdout.isTTY) {
943
- stdout.on("resize", onResize);
944
- shell.resize(stdout.columns, stdout.rows);
945
- }
946
- }
947
- catch (error) {
948
- cleanup();
949
- shell.kill();
950
- throw error;
951
- }
952
- void shell.wait().finally(() => {
953
- cleanup();
954
- });
955
- return shell.pid;
956
- }
957
414
  readFile(path) {
958
- return this.dispatchRead(path, (mount, relativePath) => mount.fs.readFile(relativePath));
415
+ return this.dispatchNativeRead(path);
959
416
  }
960
- writeFile(path, content) {
961
- return this.dispatchWrite(path, (mount, relativePath) => mount.fs.writeFile(relativePath, content), () => this.client.writeFile(this.session, this.vm, path, content));
417
+ async writeFile(path, content) {
418
+ await this.waitForMountReconfigure();
419
+ await this.client.writeFile(this.session, this.vm, path, content);
962
420
  }
963
- async mkdir(path, recursive = true) {
964
- return this.dispatchWrite(path, (mount, relativePath) => mount.fs.mkdir(relativePath, { recursive }), () => this.client.mkdir(this.session, this.vm, path, { recursive }));
421
+ async mkdir(path, options) {
422
+ await this.waitForMountReconfigure();
423
+ await this.client.mkdir(this.session, this.vm, path, options);
965
424
  }
966
425
  async exists(path) {
967
- const local = this.resolveLocalMount(path);
968
- if (local) {
969
- return local.mount.fs.exists(local.relativePath);
970
- }
971
426
  return this.client.exists(this.session, this.vm, path);
972
427
  }
973
428
  async stat(path) {
974
- const local = this.resolveLocalMount(path);
975
- if (local) {
976
- return local.mount.fs.stat(local.relativePath);
977
- }
978
429
  return toVirtualStat(await this.client.stat(this.session, this.vm, path));
979
430
  }
980
431
  async readdir(path) {
981
- const local = this.resolveLocalMount(path);
982
- if (local) {
983
- return local.mount.fs.readDir(local.relativePath);
984
- }
985
- const entries = await this.client.readdir(this.session, this.vm, path);
986
- return [...new Set([...entries, ...this.mountedChildNames(path)])].sort((a, b) => a.localeCompare(b));
432
+ return this.client.readdir(this.session, this.vm, path);
433
+ }
434
+ async readdirRecursive(path, options) {
435
+ return (await this.client.readdirRecursive(this.session, this.vm, path, options)).map((entry) => ({ ...entry, size: Number(entry.size) }));
987
436
  }
988
437
  async removeFile(path) {
989
- return this.dispatchWrite(path, (mount, relativePath) => mount.fs.removeFile(relativePath), () => this.client.removeFile(this.session, this.vm, path));
438
+ await this.client.removeFile(this.session, this.vm, path);
990
439
  }
991
440
  async removeDir(path) {
992
- return this.dispatchWrite(path, (mount, relativePath) => mount.fs.removeDir(relativePath), () => this.client.removeDir(this.session, this.vm, path));
441
+ await this.client.removeDir(this.session, this.vm, path);
442
+ }
443
+ async removePath(path, options) {
444
+ await this.client.removePath(this.session, this.vm, path, options);
445
+ }
446
+ async copyPath(fromPath, toPath, options) {
447
+ return this.client.copyPath(this.session, this.vm, fromPath, toPath, options);
993
448
  }
994
449
  async rename(oldPath, newPath) {
995
- const from = this.resolveLocalMount(oldPath);
996
- const to = this.resolveLocalMount(newPath);
997
- if (!!from !== !!to) {
998
- throw errnoError("EXDEV", "cross-device link not permitted");
999
- }
1000
- if (from && to) {
1001
- if (from.mount.path !== to.mount.path) {
1002
- throw errnoError("EXDEV", "cross-device link not permitted");
1003
- }
1004
- this.assertLocalWritable(from.mount);
1005
- return from.mount.fs.rename(from.relativePath, to.relativePath);
1006
- }
1007
450
  return this.client.rename(this.session, this.vm, oldPath, newPath);
1008
451
  }
452
+ async movePath(oldPath, newPath) {
453
+ return this.client.movePath(this.session, this.vm, oldPath, newPath);
454
+ }
1009
455
  mountFs(path, driver, options) {
1010
456
  this.localMounts.unshift({
1011
457
  path: posixPath.normalize(path),
1012
458
  fs: driver,
1013
- readOnly: options?.readOnly ?? false,
459
+ readOnly: options?.readOnly,
460
+ sidecarMount: options?.sidecarMount,
1014
461
  });
1015
462
  this.localMounts.sort((left, right) => right.path.length - left.path.length);
463
+ return this.reconfigureSidecarMounts();
1016
464
  }
1017
465
  unmountFs(path) {
1018
466
  const normalized = posixPath.normalize(path);
1019
467
  const index = this.localMounts.findIndex((mount) => mount.path === normalized);
1020
- if (index >= 0) {
1021
- this.localMounts.splice(index, 1);
1022
- }
1023
- }
1024
- snapshotProcesses() {
1025
- return this.buildProcessSnapshot();
468
+ if (index < 0) {
469
+ return Promise.resolve();
470
+ }
471
+ this.localMounts.splice(index, 1);
472
+ return this.reconfigureSidecarMounts();
473
+ }
474
+ desiredSidecarMounts() {
475
+ return [
476
+ ...this.baseSidecarMounts,
477
+ ...this.localMounts.map((mount) => mount.sidecarMount ?? {
478
+ guestPath: mount.path,
479
+ readOnly: mount.readOnly,
480
+ plugin: {
481
+ id: "js_bridge",
482
+ },
483
+ }),
484
+ ];
1026
485
  }
1027
- findListener(request) {
1028
- const key = socketLookupKey("listener", request);
1029
- const cached = this.listenerLookups.get(key);
1030
- if (!cached?.pending) {
1031
- this.listenerLookups.set(key, {
1032
- value: cached?.value ?? null,
1033
- pending: this.refreshSocketLookup(this.listenerLookups, key, () => this.client.findListener(this.session, this.vm, request)),
1034
- });
1035
- }
1036
- return this.listenerLookups.get(key)?.value ?? null;
1037
- }
1038
- findBoundUdp(request) {
1039
- const key = socketLookupKey("udp", request);
1040
- const cached = this.boundUdpLookups.get(key);
1041
- if (!cached?.pending) {
1042
- this.boundUdpLookups.set(key, {
1043
- value: cached?.value ?? null,
1044
- pending: this.refreshSocketLookup(this.boundUdpLookups, key, () => this.client.findBoundUdp(this.session, this.vm, request)),
486
+ reconfigureSidecarMounts() {
487
+ const run = async () => {
488
+ if (this.disposed) {
489
+ return;
490
+ }
491
+ // Package projections are sidecar-owned and survive mount reconfiguration.
492
+ await this.client.configureVm(this.session, this.vm, {
493
+ mounts: this.desiredSidecarMounts(),
1045
494
  });
1046
- }
1047
- return this.boundUdpLookups.get(key)?.value ?? null;
495
+ };
496
+ const previous = this.mountReconfigurePromise ?? Promise.resolve();
497
+ const next = previous.then(run, run);
498
+ const tracked = next.finally(() => {
499
+ if (this.mountReconfigurePromise === tracked) {
500
+ this.mountReconfigurePromise = null;
501
+ }
502
+ });
503
+ this.mountReconfigurePromise = tracked;
504
+ return tracked;
1048
505
  }
1049
- getSignalState(pid) {
1050
- const entry = this.trackedProcesses.get(pid);
1051
- if (entry && !this.signalRefreshes.has(pid)) {
1052
- this.signalRefreshes.set(pid, this.refreshSignalState(entry));
506
+ async waitForMountReconfigure() {
507
+ if (this.mountReconfigurePromise) {
508
+ await this.mountReconfigurePromise;
1053
509
  }
1054
- return this.signalStates.get(pid) ?? { handlers: new Map() };
1055
510
  }
1056
- async refreshSocketLookup(cache, key, lookup) {
1057
- try {
1058
- const socket = await lookup();
1059
- cache.set(key, {
1060
- value: socket ? toKernelSocketSnapshot(socket) : null,
1061
- pending: null,
1062
- });
1063
- }
1064
- catch {
1065
- cache.set(key, {
1066
- value: cache.get(key)?.value ?? null,
1067
- pending: null,
1068
- });
1069
- }
511
+ async snapshotProcesses() {
512
+ return this.buildProcessSnapshot(await this.refreshProcessSnapshot());
1070
513
  }
1071
- async refreshSignalState(entry) {
1072
- try {
1073
- const signalState = await this.client.getSignalState(this.session, this.vm, entry.processId);
1074
- this.signalStates.set(entry.pid, toKernelSignalState(signalState.handlers));
1075
- }
1076
- catch {
1077
- this.signalStates.set(entry.pid, this.signalStates.get(entry.pid) ?? { handlers: new Map() });
1078
- }
1079
- finally {
1080
- this.signalRefreshes.delete(entry.pid);
1081
- }
514
+ async processSnapshotById(processId) {
515
+ return (await this.refreshProcessSnapshot()).find((process) => process.processId === processId);
1082
516
  }
1083
517
  async refreshProcessSnapshot() {
1084
518
  if (this.processSnapshotRefresh) {
1085
- await this.processSnapshotRefresh;
1086
- return;
519
+ return this.processSnapshotRefresh;
1087
520
  }
1088
521
  this.processSnapshotRefresh = (async () => {
1089
522
  try {
1090
- this.sidecarProcessSnapshot = await this.client.getProcessSnapshot(this.session, this.vm);
523
+ return await this.client.getProcessSnapshot(this.session, this.vm);
1091
524
  }
1092
525
  finally {
1093
526
  this.processSnapshotRefresh = null;
1094
527
  }
1095
528
  })();
1096
- await this.processSnapshotRefresh;
1097
- }
1098
- async refreshZombieTimerCount() {
1099
- try {
1100
- const snapshot = await this.client.getZombieTimerCount(this.session, this.vm);
1101
- this.zombieTimerCountValue = snapshot.count;
1102
- }
1103
- catch {
1104
- // Keep the last known value if the sidecar query fails.
1105
- }
1106
- finally {
1107
- this.zombieTimerCountRefresh = null;
1108
- }
1109
- }
1110
- async drainTrailingProcessOutput(entry) {
1111
- if (entry.onStdout.size === 0 && entry.onStderr.size === 0) {
1112
- return;
1113
- }
1114
- let observedGeneration = entry.outputGeneration;
1115
- let quietTurns = 0;
1116
- let delayMs = 0;
1117
- const deadline = Date.now() + TRAILING_OUTPUT_DRAIN_MAX_MS;
1118
- while (quietTurns < TRAILING_OUTPUT_DRAIN_QUIET_TURNS) {
1119
- const remainingMs = deadline - Date.now();
1120
- if (remainingMs <= 0) {
1121
- return;
1122
- }
1123
- await drainTrailingProcessOutputTurn(Math.min(delayMs, remainingMs));
1124
- if (entry.outputGeneration === observedGeneration) {
1125
- quietTurns += 1;
1126
- }
1127
- else {
1128
- observedGeneration = entry.outputGeneration;
1129
- quietTurns = 0;
1130
- }
1131
- delayMs = TRAILING_OUTPUT_DRAIN_INTERVAL_MS;
1132
- }
529
+ return this.processSnapshotRefresh;
1133
530
  }
1134
531
  async startTrackedProcess(entry) {
532
+ await this.waitForMountReconfigure();
1135
533
  const started = await this.client.execute(this.session, this.vm, {
1136
- processId: entry.processId,
1137
- command: entry.command,
534
+ ...(entry.shellCommand !== undefined
535
+ ? { shellCommand: entry.shellCommand }
536
+ : entry.command !== undefined
537
+ ? { command: entry.command }
538
+ : {}),
1138
539
  args: entry.args,
1139
- env: entry.env,
1140
- cwd: entry.cwd,
1141
- });
1142
- entry.hostPid = started.pid;
1143
- entry.started = true;
1144
- this.updateTrackedProcessSnapshot(entry);
1145
- void this.refreshProcessSnapshot().catch(() => { });
1146
- await this.refreshSignalState(entry);
1147
- void this.flushPendingStdin(entry).catch((error) => {
1148
- this.handleBackgroundProcessError(entry, error);
1149
- });
1150
- void this.closeTrackedStdin(entry).catch((error) => {
1151
- this.handleBackgroundProcessError(entry, error);
540
+ ...(entry.env !== undefined ? { env: entry.env } : {}),
541
+ ...(entry.requestedCwd !== undefined ? { cwd: entry.requestedCwd } : {}),
542
+ ...(entry.pty ? { pty: entry.pty } : {}),
543
+ ...(entry.keepStdinOpen !== undefined
544
+ ? { keepStdinOpen: entry.keepStdinOpen }
545
+ : {}),
546
+ ...(entry.timeoutMs !== undefined ? { timeoutMs: entry.timeoutMs } : {}),
547
+ ...(entry.captureOutput !== undefined
548
+ ? { captureOutput: entry.captureOutput }
549
+ : {}),
1152
550
  });
1153
- if (entry.pendingKillSignal !== null) {
1154
- const signal = entry.pendingKillSignal;
1155
- entry.pendingKillSignal = null;
1156
- await this.signalProcess(entry, signal);
551
+ if (started.pid === null) {
552
+ throw new Error("sidecar did not return a kernel pid for the process");
1157
553
  }
554
+ entry.processId = started.processId;
555
+ entry.pid = started.pid;
556
+ this.trackedProcessesById.set(entry.processId, entry);
557
+ this.trackedProcesses.set(entry.pid, entry);
1158
558
  }
1159
559
  async runEventPump() {
560
+ // Scope the pump to THIS VM's ownership so multiple proxies can share one
561
+ // sidecar process: events for other VMs stay buffered for their own pumps
562
+ // rather than being consumed (and dropped) here.
563
+ const vmId = this.vm.vmId;
1160
564
  while (!this.disposed) {
1161
565
  try {
1162
- const event = await this.client.waitForEvent({ any: true }, undefined, {
1163
- signal: this.eventPumpAbortController.signal,
1164
- });
566
+ const event = await this.client.waitForEvent((frame) => frame.ownership.scope === "vm" && frame.ownership.vm_id === vmId, undefined, { signal: this.eventPumpAbortController.signal });
1165
567
  if (event.payload.type === "process_output") {
1166
568
  const entry = this.trackedProcessesById.get(event.payload.process_id);
1167
569
  if (!entry) {
1168
570
  continue;
1169
571
  }
1170
- entry.outputGeneration += 1;
1171
- void this.refreshProcessSnapshot().catch(() => { });
1172
- if (!this.signalRefreshes.has(entry.pid)) {
1173
- this.signalRefreshes.set(entry.pid, this.refreshSignalState(entry));
1174
- await this.signalRefreshes.get(entry.pid);
1175
- }
1176
572
  const chunk = event.payload.chunk;
1177
573
  const listeners = event.payload.channel === "stdout"
1178
574
  ? entry.onStdout
@@ -1187,9 +583,17 @@ export class NativeSidecarKernelProxy {
1187
583
  if (!entry) {
1188
584
  continue;
1189
585
  }
1190
- void this.refreshProcessSnapshot().catch(() => { });
1191
- this.signalRefreshes.delete(entry.pid);
1192
- this.finishProcess(entry, event.payload.exit_code);
586
+ if (event.payload.error_code !== undefined) {
587
+ const error = new Error(event.payload.error_message ?? event.payload.error_code);
588
+ error.code = event.payload.error_code;
589
+ this.failProcess(entry, error);
590
+ continue;
591
+ }
592
+ this.finishProcess(entry, event.payload.exit_code, event.payload.stdout, event.payload.stderr);
593
+ continue;
594
+ }
595
+ if (event.payload.type === "structured") {
596
+ logStructuredSidecarEvent(event.payload.name, event.payload.detail);
1193
597
  }
1194
598
  }
1195
599
  catch (error) {
@@ -1206,362 +610,113 @@ export class NativeSidecarKernelProxy {
1206
610
  for (const listener of entry.onStderr) {
1207
611
  listener(stderr);
1208
612
  }
1209
- this.finishProcess(entry, 1);
613
+ this.failProcess(entry, this.pumpError);
1210
614
  }
1211
615
  return;
1212
616
  }
1213
617
  }
1214
618
  }
1215
- finishProcess(entry, exitCode) {
1216
- if (entry.exitCode !== null) {
619
+ finishProcess(entry, exitCode, stdout = new Uint8Array(), stderr = new Uint8Array()) {
620
+ if (entry.settled) {
1217
621
  return;
1218
622
  }
623
+ entry.settled = true;
1219
624
  entry.exitCode = exitCode;
1220
- entry.exitTime = Date.now();
1221
- this.updateTrackedProcessSnapshot(entry);
1222
- entry.resolveWait(exitCode);
1223
- }
1224
- waitForTrackedProcess(entry) {
1225
- if (entry.exitCode !== null) {
1226
- return Promise.resolve(entry.exitCode);
1227
- }
1228
- if (entry.waitWithFallbackPromise !== null) {
1229
- return entry.waitWithFallbackPromise;
625
+ entry.resolveWait({ exitCode, stdout, stderr });
626
+ // The sidecar guarantees that all process_output events precede the terminal
627
+ // process_exited event. Release client routing immediately after exit; the
628
+ // exited record lives on in `processes` for listing.
629
+ this.releaseProcessTracking(entry);
630
+ }
631
+ failProcess(entry, error) {
632
+ if (entry.settled) {
633
+ return;
1230
634
  }
1231
- entry.waitWithFallbackPromise = (async () => {
1232
- await entry.startPromise.catch(() => { });
1233
- while (entry.exitCode === null && !this.disposed) {
1234
- const maybeExit = await Promise.race([
1235
- entry.waitPromise.then((exitCode) => exitCode),
1236
- new Promise((resolve) => setTimeout(() => resolve(null), 50)),
1237
- ]);
1238
- if (maybeExit !== null) {
1239
- return maybeExit;
1240
- }
1241
- try {
1242
- await this.refreshProcessSnapshot();
1243
- const snapshot = this.sidecarProcessSnapshot.find((candidate) => candidate.processId === entry.processId);
1244
- if (snapshot?.status === "exited") {
1245
- this.finishProcess(entry, snapshot.exitCode ?? 0);
1246
- break;
1247
- }
1248
- if (snapshot) {
1249
- entry.hostExitObservedAt = null;
1250
- continue;
1251
- }
1252
- // Fast guest processes can exit before the sidecar emits a
1253
- // `process_exited` event. Once a started process disappears from the
1254
- // authoritative VM snapshot for a full grace window, treat it as
1255
- // reaped even if the `pid` returned at launch was only a kernel/shared
1256
- // runtime identifier rather than a probeable host PID.
1257
- if (!snapshot) {
1258
- const now = Date.now();
1259
- if (entry.hostExitObservedAt === null) {
1260
- entry.hostExitObservedAt = now;
1261
- continue;
1262
- }
1263
- if (now - entry.hostExitObservedAt >= MISSING_EXIT_EVENT_GRACE_MS) {
1264
- this.finishProcess(entry, 0);
1265
- break;
1266
- }
1267
- }
1268
- }
1269
- catch {
1270
- // Fall back to the next wait interval if the sidecar snapshot query fails.
1271
- }
1272
- }
1273
- return entry.waitPromise;
1274
- })().finally(() => {
1275
- entry.waitWithFallbackPromise = null;
1276
- });
1277
- return entry.waitWithFallbackPromise;
635
+ entry.settled = true;
636
+ entry.rejectWait(error);
637
+ this.releaseProcessTracking(entry);
1278
638
  }
1279
- async signalProcess(entry, signal) {
1280
- try {
1281
- await this.client.killProcess(this.session, this.vm, entry.processId, toSidecarSignalName(signal));
639
+ releaseProcessTracking(entry) {
640
+ if (entry.pid !== null) {
641
+ this.trackedProcesses.delete(entry.pid);
1282
642
  }
1283
- catch (error) {
1284
- if (isNoSuchProcessError(error) || isUnknownVmError(error)) {
1285
- return;
1286
- }
1287
- throw error;
643
+ if (entry.processId !== null) {
644
+ this.trackedProcessesById.delete(entry.processId);
1288
645
  }
646
+ entry.onStdout.clear();
647
+ entry.onStderr.clear();
1289
648
  }
1290
- flushPendingStdin(entry) {
1291
- if (entry.stdinFlushPromise !== null) {
1292
- return entry.stdinFlushPromise;
1293
- }
1294
- entry.stdinFlushPromise = entry.startPromise
1295
- .then(async () => {
1296
- if (entry.exitCode !== null) {
1297
- return;
1298
- }
1299
- while (entry.pendingStdin.length > 0) {
1300
- const chunk = entry.pendingStdin.shift();
1301
- if (chunk === undefined) {
1302
- break;
1303
- }
1304
- await this.client.writeStdin(this.session, this.vm, entry.processId, chunk);
1305
- }
1306
- })
1307
- .catch((error) => {
1308
- if (isNoSuchProcessError(error) || isUnknownVmError(error)) {
1309
- return;
1310
- }
1311
- throw error;
1312
- })
1313
- .finally(() => {
1314
- entry.stdinFlushPromise = null;
1315
- if (entry.pendingStdin.length > 0 && entry.exitCode === null) {
1316
- void this.flushPendingStdin(entry).catch((error) => {
1317
- this.handleBackgroundProcessError(entry, error);
1318
- });
1319
- }
1320
- });
1321
- return entry.stdinFlushPromise;
649
+ waitForTrackedProcess(entry) {
650
+ return entry.waitPromise.then((completion) => completion.exitCode);
1322
651
  }
1323
- async closeTrackedStdin(entry) {
1324
- await entry.startPromise;
1325
- await this.flushPendingStdin(entry);
1326
- if (entry.exitCode !== null || !entry.pendingCloseStdin) {
1327
- return;
1328
- }
1329
- entry.pendingCloseStdin = false;
1330
- try {
1331
- await this.client.closeStdin(this.session, this.vm, entry.processId);
1332
- }
1333
- catch (error) {
1334
- if (isNoSuchProcessError(error) || isUnknownVmError(error)) {
1335
- return;
1336
- }
1337
- throw error;
1338
- }
652
+ async signalProcess(entry, signal) {
653
+ await this.client.killProcess(this.session, this.vm, this.processIdFor(entry), toSidecarSignalName(signal));
1339
654
  }
1340
- handleBackgroundProcessError(entry, error) {
1341
- if (this.disposed ||
1342
- isNoSuchProcessError(error) ||
1343
- isUnknownVmError(error)) {
1344
- return;
1345
- }
1346
- if (entry.exitCode !== null) {
1347
- this.recordCompletedProcessError(entry, error);
1348
- return;
1349
- }
1350
- this.emitBackgroundProcessError(entry, error);
1351
- this.finishProcess(entry, 1);
1352
- }
1353
- recordCompletedProcessError(entry, error) {
1354
- if (this.disposed ||
1355
- isNoSuchProcessError(error) ||
1356
- isUnknownVmError(error)) {
1357
- return entry.exitCode ?? 1;
1358
- }
1359
- this.emitBackgroundProcessError(entry, error);
1360
- entry.exitCode =
1361
- entry.exitCode === null || entry.exitCode === 0 ? 1 : entry.exitCode;
1362
- entry.exitTime ??= Date.now();
1363
- this.updateTrackedProcessSnapshot(entry);
1364
- return entry.exitCode;
1365
- }
1366
- emitBackgroundProcessError(entry, error) {
1367
- const normalized = error instanceof Error ? error : new Error(String(error));
1368
- const stderr = new TextEncoder().encode(`${normalized.message}\n`);
1369
- for (const handler of entry.onStderr) {
1370
- handler(stderr);
655
+ processIdFor(entry) {
656
+ if (entry.processId === null) {
657
+ throw new Error("sidecar process has not started");
1371
658
  }
659
+ return entry.processId;
1372
660
  }
1373
- createFilesystemView(includeLocalMounts) {
661
+ createFilesystemView() {
1374
662
  return {
1375
- readFile: (path) => this.dispatchRead(path, (mount, relativePath) => mount.fs.readFile(relativePath), includeLocalMounts),
1376
- readTextFile: async (path) => new TextDecoder().decode(await this.dispatchRead(path, (mount, relativePath) => mount.fs.readFile(relativePath), includeLocalMounts)),
1377
- readDir: async (path) => {
1378
- const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1379
- if (local) {
1380
- return local.mount.fs.readDir(local.relativePath);
1381
- }
1382
- const entries = await this.client.readdir(this.session, this.vm, path);
1383
- return includeLocalMounts
1384
- ? [...new Set([...entries, ...this.mountedChildNames(path)])].sort((a, b) => a.localeCompare(b))
1385
- : entries;
1386
- },
663
+ readFile: (path) => this.readFile(path),
664
+ readTextFile: async (path) => new TextDecoder().decode(await this.readFile(path)),
665
+ readDir: (path) => this.readdir(path),
1387
666
  readDirWithTypes: async (path) => {
1388
- const entries = await this.createFilesystemView(includeLocalMounts).readDir(path);
667
+ const entries = await this.readdir(path);
1389
668
  return Promise.all(entries.map(async (name) => {
1390
- const stat = await this.createFilesystemView(includeLocalMounts).lstat(posixPath.join(path, name));
669
+ const stat = await this.client.lstat(this.session, this.vm, posixPath.join(path, name));
1391
670
  return {
1392
671
  name,
1393
- isDirectory: stat.isDirectory,
1394
- isSymbolicLink: stat.isSymbolicLink,
672
+ isDirectory: stat.is_directory,
673
+ isSymbolicLink: stat.is_symbolic_link,
1395
674
  };
1396
675
  }));
1397
676
  },
1398
- writeFile: (path, content) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.writeFile(relativePath, content), () => this.client.writeFile(this.session, this.vm, path, content), includeLocalMounts),
1399
- createDir: (path) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.createDir(relativePath), async () => {
677
+ writeFile: (path, content) => this.writeFile(path, content),
678
+ createDir: async (path) => {
1400
679
  try {
1401
- await this.client.mkdir(this.session, this.vm, path, {
1402
- recursive: false,
1403
- });
680
+ await this.client.mkdir(this.session, this.vm, path);
1404
681
  }
1405
682
  catch (error) {
1406
683
  if (!isAlreadyExistsError(error)) {
1407
684
  throw error;
1408
685
  }
1409
686
  }
1410
- }, includeLocalMounts),
1411
- mkdir: (path, options) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.mkdir(relativePath, {
1412
- recursive: options?.recursive ?? true,
1413
- }), () => this.client.mkdir(this.session, this.vm, path, {
1414
- recursive: options?.recursive ?? true,
1415
- }), includeLocalMounts),
1416
- exists: async (path) => {
1417
- const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1418
- if (local) {
1419
- return local.mount.fs.exists(local.relativePath);
1420
- }
1421
- return this.client.exists(this.session, this.vm, path);
1422
- },
1423
- stat: async (path) => {
1424
- const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1425
- if (local) {
1426
- return local.mount.fs.stat(local.relativePath);
1427
- }
1428
- return toVirtualStat(await this.client.stat(this.session, this.vm, path));
1429
- },
1430
- removeFile: (path) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.removeFile(relativePath), () => this.client.removeFile(this.session, this.vm, path), includeLocalMounts),
1431
- removeDir: (path) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.removeDir(relativePath), () => this.client.removeDir(this.session, this.vm, path), includeLocalMounts),
1432
- rename: async (oldPath, newPath) => {
1433
- const from = includeLocalMounts
1434
- ? this.resolveLocalMount(oldPath)
1435
- : null;
1436
- const to = includeLocalMounts ? this.resolveLocalMount(newPath) : null;
1437
- if (!!from !== !!to) {
1438
- throw errnoError("EXDEV", "cross-device link not permitted");
1439
- }
1440
- if (from && to) {
1441
- if (from.mount.path !== to.mount.path) {
1442
- throw errnoError("EXDEV", "cross-device link not permitted");
1443
- }
1444
- this.assertLocalWritable(from.mount);
1445
- return from.mount.fs.rename(from.relativePath, to.relativePath);
1446
- }
1447
- return this.client.rename(this.session, this.vm, oldPath, newPath);
1448
- },
1449
- realpath: async (path) => {
1450
- const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1451
- if (local) {
1452
- return local.mount.fs.realpath(local.relativePath);
1453
- }
1454
- return this.client.realpath(this.session, this.vm, path);
1455
- },
1456
- symlink: (target, linkPath) => this.dispatchWrite(linkPath, (mount, relativePath) => mount.fs.symlink(target, relativePath), () => this.client.symlink(this.session, this.vm, target, linkPath), includeLocalMounts),
1457
- readlink: async (path) => {
1458
- const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1459
- if (local) {
1460
- return local.mount.fs.readlink(local.relativePath);
1461
- }
1462
- return this.client.readLink(this.session, this.vm, path);
1463
- },
1464
- lstat: async (path) => {
1465
- const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1466
- if (local) {
1467
- return local.mount.fs.lstat(local.relativePath);
1468
- }
1469
- return toVirtualStat(await this.client.lstat(this.session, this.vm, path));
1470
- },
1471
- link: async (oldPath, newPath) => {
1472
- const from = includeLocalMounts
1473
- ? this.resolveLocalMount(oldPath)
1474
- : null;
1475
- const to = includeLocalMounts ? this.resolveLocalMount(newPath) : null;
1476
- if (!!from !== !!to) {
1477
- throw errnoError("EXDEV", "cross-device link not permitted");
1478
- }
1479
- if (from && to) {
1480
- if (from.mount.path !== to.mount.path) {
1481
- throw errnoError("EXDEV", "cross-device link not permitted");
1482
- }
1483
- this.assertLocalWritable(from.mount);
1484
- return from.mount.fs.link(from.relativePath, to.relativePath);
1485
- }
1486
- return this.client.link(this.session, this.vm, oldPath, newPath);
1487
- },
1488
- chmod: (path, mode) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.chmod(relativePath, mode), () => this.client.chmod(this.session, this.vm, path, mode), includeLocalMounts),
1489
- chown: (path, uid, gid) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.chown(relativePath, uid, gid), () => this.client.chown(this.session, this.vm, path, uid, gid), includeLocalMounts),
1490
- utimes: (path, atimeMs, mtimeMs) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.utimes(relativePath, atimeMs, mtimeMs), () => this.client.utimes(this.session, this.vm, path, atimeMs, mtimeMs), includeLocalMounts),
1491
- truncate: (path, length) => this.dispatchWrite(path, (mount, relativePath) => mount.fs.truncate(relativePath, length), () => this.client.truncate(this.session, this.vm, path, length), includeLocalMounts),
1492
- pread: async (path, offset, length) => {
1493
- const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1494
- if (local) {
1495
- return local.mount.fs.pread(local.relativePath, offset, length);
1496
- }
1497
- return this.client.pread(this.session, this.vm, path, offset, length);
1498
- },
1499
- pwrite: async (path, offset, data) => {
1500
- const bytes = await this.createFilesystemView(includeLocalMounts).readFile(path);
1501
- const nextSize = Math.max(bytes.length, offset + data.length);
1502
- const updated = new Uint8Array(nextSize);
1503
- updated.set(bytes);
1504
- updated.set(data, offset);
1505
- await this.createFilesystemView(includeLocalMounts).writeFile(path, updated);
1506
687
  },
688
+ mkdir: (path, options) => this.mkdir(path, options),
689
+ exists: (path) => this.exists(path),
690
+ stat: (path) => this.stat(path),
691
+ removeFile: (path) => this.removeFile(path),
692
+ removeDir: (path) => this.removeDir(path),
693
+ rename: (oldPath, newPath) => this.client.rename(this.session, this.vm, oldPath, newPath),
694
+ realpath: (path) => this.client.realpath(this.session, this.vm, path),
695
+ symlink: (target, linkPath) => this.client.symlink(this.session, this.vm, target, linkPath),
696
+ readlink: (path) => this.client.readLink(this.session, this.vm, path),
697
+ lstat: async (path) => toVirtualStat(await this.client.lstat(this.session, this.vm, path)),
698
+ link: (oldPath, newPath) => this.client.link(this.session, this.vm, oldPath, newPath),
699
+ chmod: (path, mode) => this.client.chmod(this.session, this.vm, path, mode),
700
+ chown: (path, uid, gid) => this.client.chown(this.session, this.vm, path, uid, gid),
701
+ utimes: (path, atimeMs, mtimeMs) => this.client.utimes(this.session, this.vm, path, atimeMs, mtimeMs),
702
+ truncate: (path, length) => this.client.truncate(this.session, this.vm, path, length),
703
+ pread: (path, offset, length) => this.client.pread(this.session, this.vm, path, offset, length),
704
+ pwrite: (path, offset, data) => this.client.pwrite(this.session, this.vm, path, offset, data),
1507
705
  };
1508
706
  }
1509
- buildProcessSnapshot() {
1510
- void this.refreshProcessSnapshot().catch(() => { });
707
+ buildProcessSnapshot(snapshot) {
1511
708
  const processMap = new Map();
1512
- const displayPidByKernelPid = new Map();
1513
- for (const entry of this.sidecarProcessSnapshot) {
1514
- const tracked = this.trackedProcessesById.get(entry.processId);
1515
- if (tracked) {
1516
- displayPidByKernelPid.set(entry.pid, tracked.pid);
1517
- }
1518
- }
1519
- for (const entry of this.sidecarProcessSnapshot) {
1520
- const tracked = this.trackedProcessesById.get(entry.processId);
1521
- const displayPid = displayPidByKernelPid.get(entry.pid) ?? entry.pid;
1522
- const displayPpid = displayPidByKernelPid.get(entry.ppid) ?? entry.ppid;
1523
- const displayPgid = displayPidByKernelPid.get(entry.pgid) ?? entry.pgid;
1524
- const displaySid = displayPidByKernelPid.get(entry.sid) ?? entry.sid;
1525
- const processKey = `${entry.processId}:${entry.pid}`;
1526
- const startTime = tracked?.startTime ??
1527
- this.observedProcessStartTimes.get(processKey) ??
1528
- Date.now();
1529
- this.observedProcessStartTimes.set(processKey, startTime);
1530
- processMap.set(displayPid, {
1531
- pid: displayPid,
1532
- ppid: displayPpid,
1533
- pgid: displayPgid,
1534
- sid: displaySid,
1535
- driver: tracked?.driver ?? entry.driver,
1536
- command: tracked?.command ?? entry.command,
1537
- args: tracked?.args ?? entry.args,
1538
- cwd: tracked?.cwd ?? entry.cwd,
1539
- status: tracked?.exitCode !== null
1540
- ? "exited"
1541
- : tracked
1542
- ? "running"
1543
- : entry.status === "exited"
1544
- ? "exited"
1545
- : "running",
1546
- exitCode: tracked?.exitCode ?? entry.exitCode,
1547
- startTime,
1548
- exitTime: tracked?.exitTime ?? null,
1549
- });
1550
- }
1551
- for (const entry of this.trackedProcesses.values()) {
1552
- if (processMap.has(entry.pid)) {
1553
- continue;
1554
- }
709
+ for (const entry of snapshot) {
1555
710
  processMap.set(entry.pid, {
1556
711
  pid: entry.pid,
1557
- ppid: 0,
1558
- pgid: entry.pid,
1559
- sid: entry.pid,
712
+ ppid: entry.ppid,
713
+ pgid: entry.pgid,
714
+ sid: entry.sid,
1560
715
  driver: entry.driver,
1561
716
  command: entry.command,
1562
717
  args: entry.args,
1563
718
  cwd: entry.cwd,
1564
- status: entry.exitCode === null ? "running" : "exited",
719
+ status: entry.status,
1565
720
  exitCode: entry.exitCode,
1566
721
  startTime: entry.startTime,
1567
722
  exitTime: entry.exitTime,
@@ -1573,119 +728,10 @@ export class NativeSidecarKernelProxy {
1573
728
  }
1574
729
  return [...processMap.values()].sort((left, right) => left.pid - right.pid);
1575
730
  }
1576
- dispatchRead(path, handler, includeLocalMounts = true) {
1577
- const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1578
- if (local) {
1579
- return handler(local.mount, local.relativePath);
1580
- }
1581
- return this.dispatchNativeRead(path);
1582
- }
1583
- dispatchNativeRead(path) {
731
+ async dispatchNativeRead(path) {
732
+ await this.waitForMountReconfigure();
1584
733
  return this.client.readFile(this.session, this.vm, path);
1585
734
  }
1586
- async dispatchWrite(path, handler, nativeHandler, includeLocalMounts = true) {
1587
- this.assertGuestPathWritable(path);
1588
- const local = includeLocalMounts ? this.resolveLocalMount(path) : null;
1589
- if (local) {
1590
- this.assertLocalWritable(local.mount);
1591
- await handler(local.mount, local.relativePath);
1592
- return;
1593
- }
1594
- await nativeHandler();
1595
- }
1596
- resolveLocalMount(path) {
1597
- const normalizedPath = posixPath.normalize(path);
1598
- for (const mount of this.localMounts) {
1599
- if (normalizedPath !== mount.path &&
1600
- !normalizedPath.startsWith(`${mount.path}/`)) {
1601
- continue;
1602
- }
1603
- const relativePath = normalizedPath === mount.path
1604
- ? "/"
1605
- : `/${normalizedPath.slice(mount.path.length + 1)}`;
1606
- return {
1607
- mount,
1608
- relativePath,
1609
- };
1610
- }
1611
- return null;
1612
- }
1613
- assertGuestPathWritable(path) {
1614
- const normalizedPath = posixPath.normalize(path);
1615
- for (const root of PROTECTED_READ_ONLY_GUEST_ROOTS) {
1616
- if (normalizedPath === root || normalizedPath.startsWith(`${root}/`)) {
1617
- throw errnoError("EROFS", "read-only file system");
1618
- }
1619
- }
1620
- }
1621
- mountedChildNames(path) {
1622
- const normalizedPath = posixPath.normalize(path);
1623
- const names = new Set();
1624
- for (const mount of this.localMounts) {
1625
- if (mount.path === normalizedPath) {
1626
- continue;
1627
- }
1628
- if (!mount.path.startsWith(`${normalizedPath}/`) &&
1629
- normalizedPath !== "/") {
1630
- continue;
1631
- }
1632
- const relative = normalizedPath === "/"
1633
- ? mount.path.slice(1)
1634
- : mount.path.slice(normalizedPath.length + 1);
1635
- const name = relative.split("/").find(Boolean);
1636
- if (name) {
1637
- names.add(name);
1638
- }
1639
- }
1640
- return [...names];
1641
- }
1642
- assertLocalWritable(mount) {
1643
- if (mount.readOnly) {
1644
- throw errnoError("EROFS", "read-only file system");
1645
- }
1646
- }
1647
- updateTrackedProcessSnapshot(entry) {
1648
- this.processes.set(entry.pid, {
1649
- pid: entry.pid,
1650
- ppid: 0,
1651
- pgid: entry.pid,
1652
- sid: entry.pid,
1653
- driver: entry.driver,
1654
- command: entry.command,
1655
- args: entry.args,
1656
- cwd: entry.cwd,
1657
- status: entry.exitCode === null ? "running" : "exited",
1658
- exitCode: entry.exitCode,
1659
- startTime: entry.startTime,
1660
- exitTime: entry.exitTime,
1661
- });
1662
- }
1663
- }
1664
- function buildCommandMap(commandGuestPaths) {
1665
- const commands = new Map([
1666
- ["node", "node"],
1667
- ["npm", "node"],
1668
- ["npx", "node"],
1669
- ]);
1670
- for (const name of commandGuestPaths.keys()) {
1671
- commands.set(name, "wasmvm");
1672
- }
1673
- return commands;
1674
- }
1675
- function isNoSuchProcessError(error) {
1676
- if (!(error instanceof Error)) {
1677
- return false;
1678
- }
1679
- const message = error.message.toLowerCase();
1680
- return (error.message.includes("ESRCH") ||
1681
- message.includes("no such process") ||
1682
- message.includes("has no active process"));
1683
- }
1684
- function isUnknownVmError(error) {
1685
- if (!(error instanceof Error)) {
1686
- return false;
1687
- }
1688
- return error.message.toLowerCase().includes("unknown sidecar vm");
1689
735
  }
1690
736
  function isAlreadyExistsError(error) {
1691
737
  if (!(error instanceof Error)) {
@@ -1694,63 +740,32 @@ function isAlreadyExistsError(error) {
1694
740
  const message = error.message.toLowerCase();
1695
741
  return error.message.includes("EEXIST") || message.includes("file exists");
1696
742
  }
1697
- function isMissingHostProcessError(error) {
1698
- return (typeof error === "object" &&
1699
- error !== null &&
1700
- "code" in error &&
1701
- error.code === "ESRCH");
1702
- }
1703
- function errnoError(code, message) {
1704
- return Object.assign(new Error(`${code}: ${message}`), { code });
1705
- }
743
+ // VirtualStat is a numeric, Node-default-shaped view: u64 fields above
744
+ // Number.MAX_SAFE_INTEGER lose precision here, same as Node's non-bigint
745
+ // fs.stat on the host.
1706
746
  function toVirtualStat(stat) {
1707
747
  return {
1708
748
  mode: stat.mode,
1709
- size: stat.size,
1710
- blocks: stat.blocks,
1711
- dev: stat.dev,
1712
- rdev: stat.rdev,
749
+ size: Number(stat.size),
750
+ sizeExact: stat.size,
751
+ blocks: Number(stat.blocks),
752
+ dev: Number(stat.dev),
753
+ rdev: Number(stat.rdev),
1713
754
  isDirectory: stat.is_directory,
1714
755
  isSymbolicLink: stat.is_symbolic_link,
1715
756
  atimeMs: stat.atime_ms,
1716
757
  mtimeMs: stat.mtime_ms,
1717
758
  ctimeMs: stat.ctime_ms,
1718
759
  birthtimeMs: stat.birthtime_ms,
1719
- ino: stat.ino,
1720
- nlink: stat.nlink,
760
+ ino: Number(stat.ino),
761
+ inoExact: stat.ino,
762
+ nlink: Number(stat.nlink),
763
+ nlinkExact: stat.nlink,
1721
764
  uid: stat.uid,
1722
765
  gid: stat.gid,
1723
766
  };
1724
767
  }
1725
- function toKernelSocketSnapshot(socket) {
1726
- return {
1727
- processId: socket.processId,
1728
- ...(socket.host !== undefined ? { host: socket.host } : {}),
1729
- ...(socket.port !== undefined ? { port: socket.port } : {}),
1730
- ...(socket.path !== undefined ? { path: socket.path } : {}),
1731
- };
1732
- }
1733
- function toKernelSignalState(handlers) {
1734
- return {
1735
- handlers: new Map([...handlers.entries()].map(([signal, registration]) => [
1736
- signal,
1737
- {
1738
- action: registration.action,
1739
- mask: new Set(registration.mask),
1740
- flags: registration.flags,
1741
- },
1742
- ])),
1743
- };
1744
- }
1745
- function socketLookupKey(kind, request) {
1746
- return JSON.stringify({
1747
- kind,
1748
- host: request.host ?? null,
1749
- port: request.port ?? null,
1750
- path: request.path ?? null,
1751
- });
1752
- }
1753
- export { NATIVE_SIDECAR_FRAME_TIMEOUT_MS, SidecarProcess, SidecarEventBufferOverflow, SidecarProcessError, SidecarProcessExited, } from "./native-process-client.js";
768
+ export { NativeSidecarProcessClient, SidecarEventBufferOverflow, SidecarProcess, SidecarProcessError, SidecarProcessExited, } from "./native-process-client.js";
1754
769
  export class AgentOsSidecarVmHandle {
1755
770
  client;
1756
771
  sessionId;
@@ -1780,8 +795,8 @@ export class AgentOsSidecarSessionHandle {
1780
795
  listVms() {
1781
796
  return this.client.listVms(this.sessionId);
1782
797
  }
1783
- async createVm(options) {
1784
- return this.client.createVm(this.sessionId, options);
798
+ async createVm() {
799
+ return this.client.createVm(this.sessionId);
1785
800
  }
1786
801
  async dispose() {
1787
802
  await this.client.disposeSession(this.sessionId);
@@ -1802,13 +817,11 @@ export class AgentOsSidecarClient {
1802
817
  this.assertActive();
1803
818
  const sessionId = this.createId();
1804
819
  const placement = clonePlacement(options.placement);
1805
- const metadata = cloneMetadata(options.metadata);
1806
820
  const lifecycle = {
1807
821
  sessionId,
1808
822
  placement,
1809
823
  state: "connecting",
1810
824
  createdAt: this.now(),
1811
- metadata,
1812
825
  vmIds: [],
1813
826
  };
1814
827
  const entry = {
@@ -1820,7 +833,6 @@ export class AgentOsSidecarClient {
1820
833
  entry.transport = await this.createSessionTransport({
1821
834
  sessionId,
1822
835
  placement: clonePlacement(placement),
1823
- metadata: cloneMetadata(metadata),
1824
836
  signal: options.signal,
1825
837
  });
1826
838
  entry.lifecycle.state = "ready";
@@ -1848,21 +860,19 @@ export class AgentOsSidecarClient {
1848
860
  const vmEntry = this.getVmEntry(sessionId, vmId);
1849
861
  return cloneVmLifecycle(vmEntry.lifecycle);
1850
862
  }
1851
- async createVm(sessionId, options = {}) {
863
+ async createVm(sessionId) {
1852
864
  this.assertActive();
1853
865
  const entry = this.getSessionEntry(sessionId);
1854
866
  if (entry.lifecycle.state !== "ready" || !entry.transport) {
1855
867
  throw new Error(`Cannot create VM for sidecar session ${sessionId} while it is ${entry.lifecycle.state}`);
1856
868
  }
1857
869
  const vmId = this.createId();
1858
- const metadata = cloneMetadata(options.metadata);
1859
870
  const vmEntry = {
1860
871
  lifecycle: {
1861
872
  vmId,
1862
873
  sessionId,
1863
874
  state: "creating",
1864
875
  createdAt: this.now(),
1865
- metadata,
1866
876
  },
1867
877
  };
1868
878
  entry.vms.set(vmId, vmEntry);
@@ -1871,7 +881,6 @@ export class AgentOsSidecarClient {
1871
881
  await entry.transport.createVm?.({
1872
882
  vmId,
1873
883
  sessionId,
1874
- metadata: cloneMetadata(metadata),
1875
884
  });
1876
885
  vmEntry.lifecycle.state = "ready";
1877
886
  vmEntry.lifecycle.readyAt = this.now();
@@ -1933,10 +942,10 @@ export class AgentOsSidecarClient {
1933
942
  errors.push(toError(error));
1934
943
  }
1935
944
  }
1936
- this.disposed = true;
1937
945
  if (errors.length > 0) {
1938
946
  throw new Error(errors.map((error) => error.message).join("; "));
1939
947
  }
948
+ this.disposed = true;
1940
949
  }
1941
950
  async disposeVmEntry(sessionEntry, vmEntry) {
1942
951
  if (vmEntry.lifecycle.state === "disposed" ||
@@ -1983,32 +992,32 @@ export function serializeMountConfigForSidecar(mount) {
1983
992
  if ("driver" in mount) {
1984
993
  return {
1985
994
  guestPath: mount.path,
1986
- readOnly: mount.readOnly ?? false,
995
+ ...(mount.readOnly === undefined ? {} : { readOnly: mount.readOnly }),
1987
996
  plugin: {
1988
997
  id: "js_bridge",
1989
- config: {},
1990
998
  },
1991
999
  };
1992
1000
  }
1993
1001
  return {
1994
1002
  guestPath: mount.path,
1995
- readOnly: mount.readOnly ?? false,
1003
+ ...(mount.readOnly === undefined ? {} : { readOnly: mount.readOnly }),
1996
1004
  plugin: {
1997
1005
  id: mount.plugin.id,
1998
- config: mount.plugin.config ?? {},
1006
+ ...(mount.plugin.config === undefined
1007
+ ? {}
1008
+ : { config: mount.plugin.config }),
1999
1009
  },
2000
1010
  };
2001
1011
  }
2002
- export function serializeRootFilesystemForSidecar(config, bootstrapLower) {
2003
- const lowerInputs = [
2004
- ...(config?.lowers ?? []),
2005
- ...(bootstrapLower ? [bootstrapLower] : []),
2006
- ];
1012
+ export function serializeRootFilesystemForSidecar(config) {
2007
1013
  return {
2008
- mode: config?.mode === "read-only" ? "read-only" : "ephemeral",
2009
- disableDefaultBaseLayer: config?.disableDefaultBaseLayer ?? false,
2010
- lowers: lowerInputs.map(serializeRootLowerForSidecar),
2011
- bootstrapEntries: [],
1014
+ ...(config?.mode !== undefined ? { mode: config.mode } : {}),
1015
+ ...(config?.disableDefaultBaseLayer !== undefined
1016
+ ? { disableDefaultBaseLayer: config.disableDefaultBaseLayer }
1017
+ : {}),
1018
+ ...(config?.lowers !== undefined
1019
+ ? { lowers: config.lowers.map(serializeRootLowerForSidecar) }
1020
+ : {}),
2012
1021
  };
2013
1022
  }
2014
1023
  function clonePlacement(placement) {
@@ -2023,21 +1032,16 @@ function clonePlacement(placement) {
2023
1032
  sidecarId: placement.sidecarId,
2024
1033
  };
2025
1034
  }
2026
- function cloneMetadata(metadata) {
2027
- return { ...(metadata ?? {}) };
2028
- }
2029
1035
  function cloneSessionLifecycle(lifecycle) {
2030
1036
  return {
2031
1037
  ...lifecycle,
2032
1038
  placement: clonePlacement(lifecycle.placement),
2033
- metadata: cloneMetadata(lifecycle.metadata),
2034
1039
  vmIds: [...lifecycle.vmIds],
2035
1040
  };
2036
1041
  }
2037
1042
  function cloneVmLifecycle(lifecycle) {
2038
1043
  return {
2039
1044
  ...lifecycle,
2040
- metadata: cloneMetadata(lifecycle.metadata),
2041
1045
  };
2042
1046
  }
2043
1047
  function serializeRootLowerForSidecar(lower) {