@deepseek-ai/dsh-subprocess-e2b 0.0.1-rc.1

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.
package/lib/index.js ADDED
@@ -0,0 +1,1444 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { posix } from "node:path";
3
+ import z from "@deepseek-ai/schemastery";
4
+ import { SENSITIVE_ENV_PATTERN, SubprocessService } from "@deepseek-ai/dsh-subprocess";
5
+ import { MAX_TIMER_DELAY_MS } from "@deepseek-ai/dsh-timeout";
6
+ import { CommandExitError, FileNotFoundError, SandboxNotFoundError, e2bControlEnvs, quoteE2BShellArg } from "@deepseek-ai/dsh-e2b";
7
+ import { PassThrough, Writable } from "node:stream";
8
+ import { Buffer } from "node:buffer";
9
+ //#region lib/types/environment.js
10
+ /** Shared remote-environment scrubbing for E2B process and terminal launchers. */
11
+ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
12
+ function remoteEnvironmentEntries(raw) {
13
+ const entries = [];
14
+ for (const entry of raw.split("\0")) {
15
+ if (entry.length === 0) continue;
16
+ const separator = entry.indexOf("=");
17
+ if (separator <= 0) continue;
18
+ entries.push([entry.slice(0, separator), entry.slice(separator + 1)]);
19
+ }
20
+ return entries;
21
+ }
22
+ /**
23
+ * Read the remote environment through ASCII base64 so SDK callback chunking cannot corrupt UTF-8.
24
+ * @param sandbox - shared E2B execution world.
25
+ * @param signal - optional cancellation for the control-plane request.
26
+ * @returns the complete NUL-delimited UTF-8 environment.
27
+ */
28
+ async function readRemoteEnvironment(sandbox, signal) {
29
+ const lines = (await sandbox.commands.run("set -o pipefail; dsh_e2b_passwd=\"$(getent passwd \"$(id -u)\")\"; IFS=: read -r _ _ _ _ _ dsh_e2b_home _ <<<\"$dsh_e2b_passwd\"; test -n \"$dsh_e2b_home\" -a -d \"$dsh_e2b_home\"; printf '%s' \"$dsh_e2b_home\" | base64 -w 0; printf '\\n'; env -0 | base64 -w 0", {
30
+ envs: e2bControlEnvs(),
31
+ ...signal === void 0 ? {} : { signal }
32
+ })).stdout.trim().split("\n");
33
+ if (lines.length !== 2 || !lines.every((line) => BASE64.test(line))) throw new Error("subprocess-e2b: remote environment transport returned invalid base64");
34
+ const [encodedHome, encodedEnvironment] = lines;
35
+ let home;
36
+ let raw;
37
+ try {
38
+ const decoder = new TextDecoder("utf-8", { fatal: true });
39
+ home = decoder.decode(Buffer.from(encodedHome, "base64"));
40
+ raw = decoder.decode(Buffer.from(encodedEnvironment, "base64"));
41
+ } catch (error) {
42
+ throw new Error("subprocess-e2b: remote environment is not valid UTF-8", { cause: error });
43
+ }
44
+ if (!posix.isAbsolute(home) || home.includes("\0")) throw new Error(`subprocess-e2b: remote login home is invalid: ${JSON.stringify(home)}`);
45
+ const environment = new Map(remoteEnvironmentEntries(raw));
46
+ environment.set("HOME", home);
47
+ return [...environment].map(([name, value]) => `${name}=${value}\0`).join("");
48
+ }
49
+ /**
50
+ * Parse an E2B NUL-delimited environment while removing harness-private and credential-shaped names.
51
+ * @param raw - The complete NUL-delimited remote environment.
52
+ * @returns Mutable retained entries for the caller to overlay and serialize.
53
+ */
54
+ function scrubRemoteEnvironment(raw) {
55
+ const environment = /* @__PURE__ */ new Map();
56
+ for (const [name, value] of remoteEnvironmentEntries(raw)) {
57
+ if (name.startsWith("DSH_") || SENSITIVE_ENV_PATTERN.test(name)) continue;
58
+ environment.set(name, value);
59
+ }
60
+ return environment;
61
+ }
62
+ /**
63
+ * Isolate E2B's fixed login-shell bootstrap from user profiles and ambient credentials.
64
+ * @param raw - The complete NUL-delimited remote environment.
65
+ * @returns Explicit E2B command or PTY overrides for bootstrap-shell startup.
66
+ */
67
+ function bootstrapEnvironment(raw) {
68
+ const environment = { TERM: "dumb" };
69
+ for (const [name] of remoteEnvironmentEntries(raw)) if (name.startsWith("DSH_") || SENSITIVE_ENV_PATTERN.test(name)) environment[name] = "";
70
+ return environment;
71
+ }
72
+ /**
73
+ * Overlay explicit entries and serialize one validated E2B environment.
74
+ * @param raw - The complete NUL-delimited remote environment.
75
+ * @param explicit - Deliberate caller overrides applied after ambient scrubbing; an `undefined` tombstone removes an ambient entry.
76
+ * @returns NUL-delimited `name=value` entries accepted by `env -i`.
77
+ */
78
+ function serializeRemoteEnvironment(raw, explicit) {
79
+ const environment = scrubRemoteEnvironment(raw);
80
+ for (const [name, value] of Object.entries(explicit ?? {})) {
81
+ if (name.length === 0 || name.includes("=") || name.includes("\0") || value?.includes("\0") === true) throw new Error("subprocess-e2b: environment entries require non-empty NUL-free names without = and NUL-free values");
82
+ if (value === void 0) environment.delete(name);
83
+ else environment.set(name, value);
84
+ }
85
+ return [...environment].map(([name, value]) => `${name}=${value}\0`).join("");
86
+ }
87
+ //#endregion
88
+ //#region lib/types/output.js
89
+ /** Bounded host-side projection of a complete output file retained in E2B. */
90
+ const BASE64_TEXT = /^[A-Za-z0-9+/]+={0,2}$/u;
91
+ /** Reserved non-base64 frame proving that one remote encoder reached clean EOF. */
92
+ const E2B_OUTPUT_COMPLETE_FRAME = "!dsh-e2b-output-complete!";
93
+ /** Incrementally decode newline-delimited base64 frames emitted by one remote encoder. */
94
+ var E2BBase64Decoder = class {
95
+ pending = "";
96
+ complete = false;
97
+ /**
98
+ * Decode every complete newline-delimited frame in one arbitrarily split SDK callback.
99
+ * @param text - ASCII base64 frames from E2B's decoded callback.
100
+ * @returns the complete raw bytes made available by this callback.
101
+ */
102
+ push(text) {
103
+ if (text.length === 0) return Buffer.alloc(0);
104
+ this.pending += text;
105
+ const decoded = [];
106
+ for (;;) {
107
+ const boundary = this.pending.indexOf("\n");
108
+ if (boundary < 0) break;
109
+ const frame = this.pending.slice(0, boundary);
110
+ this.pending = this.pending.slice(boundary + 1);
111
+ if (frame === "!dsh-e2b-output-complete!") {
112
+ if (this.complete) throw new Error("subprocess-e2b: duplicate output transport completion");
113
+ this.complete = true;
114
+ continue;
115
+ }
116
+ if (this.complete) throw new Error("subprocess-e2b: output transport continued after completion");
117
+ if (!BASE64_TEXT.test(frame)) throw new Error("subprocess-e2b: invalid base64 output transport");
118
+ const bytes = Buffer.from(frame, "base64");
119
+ if (bytes.toString("base64") !== frame) throw new Error("subprocess-e2b: invalid base64 output transport");
120
+ decoded.push(bytes);
121
+ }
122
+ return Buffer.concat(decoded);
123
+ }
124
+ /**
125
+ * Validate clean encoder completion, or discard an interrupted trailing frame after requested termination.
126
+ * @param requireComplete - Whether natural completion requires the reserved EOF frame.
127
+ */
128
+ finish(requireComplete = true) {
129
+ if (!requireComplete) {
130
+ this.pending = "";
131
+ return;
132
+ }
133
+ if (this.pending.length > 0) throw new Error("subprocess-e2b: truncated base64 output transport");
134
+ if (!this.complete) throw new Error("subprocess-e2b: incomplete output transport");
135
+ }
136
+ };
137
+ /** Offset reader used for one collect-mode E2B stream. */
138
+ var E2BOutputReader = class {
139
+ maxBytes;
140
+ maxSpillBytes;
141
+ spillPath;
142
+ chunks = [];
143
+ retainedBytes = 0;
144
+ totalBytes = 0;
145
+ spillValid = true;
146
+ /**
147
+ * Create a bounded reader over one remote spill path.
148
+ * @param maxBytes - In-memory tail cap.
149
+ * @param maxSpillBytes - Maximum complete remote file size the caller accepts.
150
+ * @param spillPath - Remote full-output path.
151
+ */
152
+ constructor(maxBytes, maxSpillBytes, spillPath) {
153
+ this.maxBytes = maxBytes;
154
+ this.maxSpillBytes = maxSpillBytes;
155
+ this.spillPath = spillPath;
156
+ }
157
+ /** Total bytes observed from the SDK stream. */
158
+ get size() {
159
+ return this.totalBytes;
160
+ }
161
+ /** Stop advertising a remote spill whose writer did not reach clean EOF. */
162
+ invalidateSpill() {
163
+ this.spillValid = false;
164
+ }
165
+ /**
166
+ * Append one byte-faithful decoded transport event.
167
+ * @param bytes - Raw command bytes recovered from the ASCII SDK transport.
168
+ */
169
+ push(bytes) {
170
+ if (bytes.length === 0) return;
171
+ const chunk = Buffer.from(bytes);
172
+ this.totalBytes += chunk.length;
173
+ this.chunks.push(chunk);
174
+ this.retainedBytes += chunk.length;
175
+ while (this.retainedBytes > this.maxBytes) {
176
+ const head = this.chunks[0];
177
+ const excess = this.retainedBytes - this.maxBytes;
178
+ if (head.length <= excess) {
179
+ this.chunks.shift();
180
+ this.retainedBytes -= head.length;
181
+ } else {
182
+ this.chunks[0] = head.subarray(excess);
183
+ this.retainedBytes -= excess;
184
+ }
185
+ }
186
+ }
187
+ /** @inheritdoc */
188
+ readFrom(fromByte) {
189
+ const retained = Buffer.concat(this.chunks, this.retainedBytes);
190
+ const firstRetained = this.totalBytes - this.retainedBytes;
191
+ const lossy = fromByte < firstRetained;
192
+ const start = lossy ? 0 : Math.min(retained.length, Math.max(0, fromByte - firstRetained));
193
+ return {
194
+ text: retained.subarray(start).toString("utf8"),
195
+ nextOffset: this.totalBytes,
196
+ lossy,
197
+ ...lossy && this.spillValid && this.maxSpillBytes !== void 0 && this.totalBytes <= this.maxSpillBytes ? { spillPath: this.spillPath } : {}
198
+ };
199
+ }
200
+ };
201
+ //#endregion
202
+ //#region lib/types/remote.js
203
+ /**
204
+ * Shared remote-control helpers for the E2B subprocess adapter: SDK option
205
+ * shaping, poll ticks, and the one tolerant process-group signal used by both
206
+ * the ordinary-process and terminal teardown ladders.
207
+ */
208
+ /**
209
+ * Normalize an unknown rejection into an Error.
210
+ * @param error - Any thrown or rejected value.
211
+ * @returns The value itself when already an Error, else a stringified wrapper.
212
+ */
213
+ function asError(error) {
214
+ return error instanceof Error ? error : new Error(String(error));
215
+ }
216
+ /**
217
+ * Shape the optional-signal SDK options object.
218
+ * @param signal - Optional cancellation for one SDK request.
219
+ * @returns An options fragment that omits an undefined signal.
220
+ */
221
+ function signalOpts(signal) {
222
+ return signal === void 0 ? {} : { signal };
223
+ }
224
+ /**
225
+ * Shape control-shell command options with the isolated HOME override.
226
+ * @param envs - Explicit environment entries for the control command.
227
+ * @param signal - Optional cancellation for the SDK request.
228
+ * @returns Options for `sandbox.commands.run` control invocations.
229
+ */
230
+ function commandOpts(envs, signal) {
231
+ return {
232
+ envs: e2bControlEnvs(envs),
233
+ ...signalOpts(signal)
234
+ };
235
+ }
236
+ /**
237
+ * Resolve after one duration.
238
+ * @param ms - Milliseconds to wait.
239
+ * @returns Settles after the timeout.
240
+ */
241
+ function delay(ms) {
242
+ return new Promise((resolve) => setTimeout(resolve, ms));
243
+ }
244
+ /**
245
+ * Wait one poll interval or until the signal aborts.
246
+ * @param pollMs - Poll cadence in milliseconds.
247
+ * @param signal - Optional abort that ends the wait early.
248
+ * @returns `true` after a full tick, `false` when aborted first.
249
+ */
250
+ function waitTick(pollMs, signal) {
251
+ if (signal?.aborted === true) return Promise.resolve(false);
252
+ return new Promise((resolve) => {
253
+ const timer = setTimeout(() => {
254
+ signal?.removeEventListener("abort", onAbort);
255
+ resolve(true);
256
+ }, pollMs);
257
+ const onAbort = () => {
258
+ clearTimeout(timer);
259
+ resolve(false);
260
+ };
261
+ signal?.addEventListener("abort", onAbort, { once: true });
262
+ });
263
+ }
264
+ /**
265
+ * Signal remote process groups, tolerating the shared teardown outcomes: a
266
+ * nonzero `kill` (groups already gone) and a disappeared sandbox. Both the
267
+ * pgid-keyed process ladder and the sid-keyed terminal ladder deliver signals
268
+ * through this single tolerance so they cannot drift apart.
269
+ * @param sandbox - Live SDK handle.
270
+ * @param envs - Control-shell environment entries.
271
+ * @param groups - Positive process-group ids to signal.
272
+ * @param signal - `TERM` or `KILL`.
273
+ */
274
+ async function signalRemoteGroups(sandbox, envs, groups, signal) {
275
+ try {
276
+ await sandbox.commands.run(`kill -${signal} -- ${groups.map((group) => `-${group}`).join(" ")}`, commandOpts(envs));
277
+ } catch (error) {
278
+ if (!(error instanceof CommandExitError) && !(error instanceof SandboxNotFoundError)) throw error;
279
+ }
280
+ }
281
+ //#endregion
282
+ //#region lib/types/process.js
283
+ /** One asynchronously-started E2B command projected onto the subprocess seam. */
284
+ const OUTPUT_ENCODER_SOURCE = [
285
+ "(async () => {",
286
+ " for await (const chunk of process.stdin) {",
287
+ " if (!process.stdout.write(chunk.toString('base64') + '\\n')) {",
288
+ " await new Promise(resolve => process.stdout.once('drain', resolve))",
289
+ " }",
290
+ " }",
291
+ ` if (!process.stdout.write(${JSON.stringify(E2B_OUTPUT_COMPLETE_FRAME)} + '\\n')) {`,
292
+ " await new Promise(resolve => process.stdout.once('drain', resolve))",
293
+ " }",
294
+ "})().catch(() => { process.exitCode = 1 })"
295
+ ].join("\n");
296
+ function isCollect(mode) {
297
+ return mode !== "pipe" && mode !== "inherit";
298
+ }
299
+ function hasSpill(mode) {
300
+ return isCollect(mode) && mode.spill !== void 0;
301
+ }
302
+ function isValidProcessId(value) {
303
+ return Number.isSafeInteger(value) && value > 0;
304
+ }
305
+ var DeferredStdin = class extends Writable {
306
+ ready;
307
+ constructor(ready) {
308
+ super({ decodeStrings: false });
309
+ this.ready = ready;
310
+ }
311
+ _write(chunk, _encoding, callback) {
312
+ this.ready.then((handle) => handle.sendStdin(chunk)).then(() => {
313
+ callback();
314
+ }, (error) => {
315
+ callback(asError(error));
316
+ });
317
+ }
318
+ _final(callback) {
319
+ this.ready.then((handle) => handle.closeStdin()).then(() => {
320
+ callback();
321
+ }, (error) => {
322
+ callback(asError(error));
323
+ });
324
+ }
325
+ };
326
+ function withinMs(settlement, timeoutMs) {
327
+ return new Promise((resolve) => {
328
+ const timer = setTimeout(() => {
329
+ resolve(void 0);
330
+ }, timeoutMs);
331
+ settlement.then((value) => {
332
+ clearTimeout(timer);
333
+ resolve(value);
334
+ });
335
+ });
336
+ }
337
+ function commandText(spec, paths) {
338
+ const encoder = `"$dsh_e2b_env_bin" -i "$dsh_e2b_node" -e ${quoteE2BShellArg(OUTPUT_ENCODER_SOURCE)}`;
339
+ const stdoutRedirect = hasSpill(spec.stdio.stdout) ? `> >("$dsh_e2b_tee" --output-error=warn-nopipe >("$dsh_e2b_head" -c ${spec.stdio.stdout.spill.maxBytes} > ${quoteE2BShellArg(paths.stdout)}) | ${encoder} 2>/dev/null)` : `> >(${encoder} 2>/dev/null)`;
340
+ const stderrRedirect = hasSpill(spec.stdio.stderr) ? `2> >("$dsh_e2b_tee" --output-error=warn-nopipe >("$dsh_e2b_head" -c ${spec.stdio.stderr.spill.maxBytes} > ${quoteE2BShellArg(paths.stderr)}) | ${encoder} >&2 2>/dev/null)` : `2> >(${encoder} >&2 2>/dev/null)`;
341
+ const inner = [
342
+ "set +e",
343
+ "dsh_e2b_env_bin=$1",
344
+ "dsh_e2b_node=$2",
345
+ "dsh_e2b_ps=$3",
346
+ "dsh_e2b_tr=$4",
347
+ "dsh_e2b_tee=$5",
348
+ "dsh_e2b_head=$6",
349
+ "dsh_e2b_rm=$7",
350
+ "shift 7",
351
+ "dsh_e2b_pgid=\"$(\"$dsh_e2b_ps\" -o pgid= -p \"$$\" | \"$dsh_e2b_tr\" -d \" \")\"",
352
+ `printf '%s\\n' "$dsh_e2b_pgid" > ${quoteE2BShellArg(paths.pid)}`,
353
+ `mapfile -d '' -t dsh_e2b_env < ${quoteE2BShellArg(paths.environment)}`,
354
+ `"$dsh_e2b_rm" -f -- ${quoteE2BShellArg(paths.environment)}`,
355
+ `"$dsh_e2b_env_bin" -i -- "\${dsh_e2b_env[@]}" "$@" ${stdoutRedirect} ${stderrRedirect}`.trimEnd(),
356
+ "dsh_e2b_status=$?",
357
+ `printf '%s\\n' "$dsh_e2b_status" > ${quoteE2BShellArg(paths.status)}`,
358
+ "wait",
359
+ "exit \"$dsh_e2b_status\""
360
+ ].join("\n");
361
+ const argv = spec.argv.map(quoteE2BShellArg).join(" ");
362
+ return [
363
+ `mapfile -d '' -t dsh_e2b_env < ${quoteE2BShellArg(paths.environment)}`,
364
+ "dsh_e2b_env_bin=\"$(command -v env)\"",
365
+ "dsh_e2b_setsid=\"$(command -v setsid)\"",
366
+ "dsh_e2b_bash=\"$(command -v bash)\"",
367
+ "dsh_e2b_node=\"$(command -v node)\"",
368
+ "dsh_e2b_ps=\"$(command -v ps)\"",
369
+ "dsh_e2b_tr=\"$(command -v tr)\"",
370
+ "dsh_e2b_tee=\"$(command -v tee)\"",
371
+ "dsh_e2b_head=\"$(command -v head)\"",
372
+ "dsh_e2b_rm=\"$(command -v rm)\"",
373
+ "for dsh_e2b_tool in \"$dsh_e2b_env_bin\" \"$dsh_e2b_setsid\" \"$dsh_e2b_bash\" \"$dsh_e2b_node\" \"$dsh_e2b_ps\" \"$dsh_e2b_tr\" \"$dsh_e2b_tee\" \"$dsh_e2b_head\" \"$dsh_e2b_rm\"; do",
374
+ " [[ \"$dsh_e2b_tool\" == /* && -x \"$dsh_e2b_tool\" ]] || exit 125",
375
+ "done",
376
+ `exec "$dsh_e2b_env_bin" -i -- "\${dsh_e2b_env[@]}" "$dsh_e2b_setsid" --wait -- "$dsh_e2b_bash" -c ${quoteE2BShellArg(inner)} dsh-e2b "$dsh_e2b_env_bin" "$dsh_e2b_node" "$dsh_e2b_ps" "$dsh_e2b_tr" "$dsh_e2b_tee" "$dsh_e2b_head" "$dsh_e2b_rm" ${argv}`
377
+ ].join("\n");
378
+ }
379
+ const WAIT_ABORTED = Symbol("wait aborted");
380
+ function waitWithSignal(promise, signal) {
381
+ if (signal === void 0) return promise;
382
+ if (signal.aborted) return Promise.resolve(WAIT_ABORTED);
383
+ return new Promise((resolve) => {
384
+ const onAbort = () => {
385
+ cleanup();
386
+ resolve(WAIT_ABORTED);
387
+ };
388
+ const cleanup = () => {
389
+ signal.removeEventListener("abort", onAbort);
390
+ };
391
+ signal.addEventListener("abort", onAbort, { once: true });
392
+ if (signal.aborted) {
393
+ onAbort();
394
+ return;
395
+ }
396
+ promise.then((value) => {
397
+ cleanup();
398
+ resolve(value);
399
+ });
400
+ });
401
+ }
402
+ /** E2B-backed subprocess handle with deferred remote PID acquisition. */
403
+ var E2BSubprocessHandle = class {
404
+ runtime;
405
+ spec;
406
+ stateDir;
407
+ pollMs;
408
+ stdin;
409
+ stdout;
410
+ stderr;
411
+ collected;
412
+ done;
413
+ commandState = Promise.withResolvers();
414
+ readyState = Promise.withResolvers();
415
+ stdoutDecoder = new E2BBase64Decoder();
416
+ stderrDecoder = new E2BBase64Decoder();
417
+ terminationController = new AbortController();
418
+ /** Releases output waits that survive the command outcome, so blocked SDK callbacks settle. */
419
+ outputReleased = new AbortController();
420
+ stdoutReader;
421
+ stderrReader;
422
+ paths;
423
+ controlEnvs = {};
424
+ remotePid = -1;
425
+ outputTransportError;
426
+ outputDrainExpired = false;
427
+ stateDirectoryCreated = false;
428
+ quiescenceProven = false;
429
+ terminationAttempt;
430
+ terminationFailure;
431
+ terminationSignal = null;
432
+ /**
433
+ * Begin an E2B command without blocking the synchronous subprocess spawn call.
434
+ * @param runtime - Shared E2B sandbox owner.
435
+ * @param spec - Fully resolved subprocess request.
436
+ * @param stateDir - Remote directory retaining process identity, status, and valid spills.
437
+ * @param pollMs - Remote status/liveness poll cadence.
438
+ */
439
+ constructor(runtime, spec, stateDir, pollMs) {
440
+ this.runtime = runtime;
441
+ this.spec = spec;
442
+ this.stateDir = stateDir;
443
+ this.pollMs = pollMs;
444
+ this.paths = {
445
+ pid: posix.join(stateDir, "pid"),
446
+ status: posix.join(stateDir, "exit-code"),
447
+ environment: posix.join(stateDir, "environment"),
448
+ stdout: posix.join(stateDir, "stdout.log"),
449
+ stderr: posix.join(stateDir, "stderr.log")
450
+ };
451
+ const outMode = spec.stdio.stdout;
452
+ const errMode = spec.stdio.stderr;
453
+ this.stdout = outMode === "pipe" ? new PassThrough() : void 0;
454
+ this.stderr = errMode === "pipe" ? new PassThrough() : void 0;
455
+ this.stdoutReader = isCollect(outMode) ? new E2BOutputReader(outMode.maxBytes, outMode.spill?.maxBytes, this.paths.stdout) : void 0;
456
+ this.stderrReader = isCollect(errMode) ? new E2BOutputReader(errMode.maxBytes, errMode.spill?.maxBytes, this.paths.stderr) : void 0;
457
+ this.collected = {
458
+ ...this.stdoutReader !== void 0 ? { stdout: this.stdoutReader } : {},
459
+ ...this.stderrReader !== void 0 ? { stderr: this.stderrReader } : {}
460
+ };
461
+ this.stdin = spec.stdio.stdin === "pipe" ? new DeferredStdin(this.readyState.promise) : void 0;
462
+ this.readyState.promise.catch(() => {});
463
+ spec.signal?.addEventListener("abort", this.onAbort, { once: true });
464
+ this.done = this.run();
465
+ this.done.catch(() => {});
466
+ if (spec.signal?.aborted === true) this.terminate();
467
+ }
468
+ /** Remote process id after start; `-1` while E2B startup is pending or after it fails. */
469
+ get pid() {
470
+ return this.remotePid;
471
+ }
472
+ /** @inheritdoc */
473
+ terminate() {
474
+ if (this.quiescenceProven || this.terminationAttempt !== void 0) return;
475
+ this.terminationController.abort(/* @__PURE__ */ new Error("subprocess-e2b: command terminated"));
476
+ this.stdout?.destroy();
477
+ this.stderr?.destroy();
478
+ this.terminationFailure = void 0;
479
+ const attempt = this.terminateRemote();
480
+ this.terminationAttempt = attempt;
481
+ attempt.then(() => {
482
+ this.terminationAttempt = void 0;
483
+ }, (error) => {
484
+ if (!this.quiescenceProven) this.terminationFailure = asError(error);
485
+ this.terminationAttempt = void 0;
486
+ });
487
+ }
488
+ /** @inheritdoc */
489
+ async waitForExit(signal) {
490
+ if (this.quiescenceProven) return true;
491
+ let handle;
492
+ if (this.terminationController.signal.aborted) {
493
+ const observed = await waitWithSignal(this.commandState.promise, signal);
494
+ if (observed === WAIT_ABORTED) return false;
495
+ handle = observed;
496
+ if (handle === void 0) {
497
+ this.markQuiescent();
498
+ return true;
499
+ }
500
+ if (this.remotePid <= 0) {
501
+ const attempt = this.terminationAttempt;
502
+ if (attempt !== void 0 && await waitWithSignal(attempt.catch(() => void 0), signal) === WAIT_ABORTED) return false;
503
+ this.throwTerminationFailure();
504
+ return true;
505
+ }
506
+ } else {
507
+ const observed = await waitWithSignal(this.readyState.promise.catch(() => this.commandState.promise), signal);
508
+ if (observed === WAIT_ABORTED) return false;
509
+ handle = observed;
510
+ if (handle === void 0) {
511
+ this.markQuiescent();
512
+ return true;
513
+ }
514
+ }
515
+ this.throwTerminationFailure();
516
+ let sandbox;
517
+ try {
518
+ sandbox = await this.runtime.getSandbox();
519
+ } catch (error) {
520
+ if (signal?.aborted === true) return false;
521
+ if (error instanceof SandboxNotFoundError) {
522
+ this.markQuiescent();
523
+ return true;
524
+ }
525
+ throw error;
526
+ }
527
+ const processGroupId = this.remotePid > 0 ? this.remotePid : handle.pid;
528
+ while (await this.groupAlive(sandbox, processGroupId, signal)) {
529
+ this.throwTerminationFailure();
530
+ if (!await waitTick(this.pollMs, signal)) return false;
531
+ }
532
+ this.throwTerminationFailure();
533
+ if (signal?.aborted === true) return false;
534
+ this.markQuiescent();
535
+ return true;
536
+ }
537
+ onAbort = () => {
538
+ this.terminate();
539
+ };
540
+ markQuiescent() {
541
+ this.quiescenceProven = true;
542
+ this.terminationFailure = void 0;
543
+ }
544
+ async run() {
545
+ let sandbox;
546
+ let preparing = true;
547
+ try {
548
+ sandbox = await this.runtime.getSandbox();
549
+ await this.prepareState(sandbox);
550
+ preparing = false;
551
+ const handle = await sandbox.commands.run(commandText(this.spec, this.paths), {
552
+ background: true,
553
+ cwd: this.spec.cwd,
554
+ envs: e2bControlEnvs(this.controlEnvs),
555
+ stdin: this.spec.stdio.stdin !== "ignore",
556
+ timeoutMs: 0,
557
+ onStdout: async (data) => {
558
+ await this.dispatchOutput("stdout", data);
559
+ },
560
+ onStderr: async (data) => {
561
+ await this.dispatchOutput("stderr", data);
562
+ }
563
+ });
564
+ const completion = handle.wait();
565
+ completion.catch(() => {});
566
+ if (!isValidProcessId(handle.pid)) {
567
+ const invalidPid = /* @__PURE__ */ new Error(`subprocess-e2b: E2B returned invalid command pid ${handle.pid}`);
568
+ try {
569
+ await handle.kill();
570
+ this.markQuiescent();
571
+ } catch (cleanupError) {
572
+ this.terminationFailure = asError(cleanupError);
573
+ this.commandState.resolve(handle);
574
+ throw new AggregateError([invalidPid, cleanupError], "subprocess-e2b: invalid command pid rollback did not reach quiescence");
575
+ }
576
+ throw invalidPid;
577
+ }
578
+ this.commandState.resolve(handle);
579
+ try {
580
+ this.remotePid = await this.waitForProcessGroupId(sandbox, completion);
581
+ } catch (error) {
582
+ try {
583
+ await this.rollbackUnpublishedGroup(sandbox, handle);
584
+ } catch (cleanupError) {
585
+ throw new AggregateError([error, cleanupError], "subprocess-e2b: process-group publication failed and rollback did not reach quiescence");
586
+ }
587
+ throw error;
588
+ }
589
+ this.readyState.resolve(handle);
590
+ await this.writeBatchStdin(handle);
591
+ const outcome = await this.waitForCommand(sandbox, handle, completion);
592
+ if (this.outputTransportError !== void 0) throw this.outputTransportError;
593
+ const requireCompleteOutput = this.terminationSignal === null && !this.outputDrainExpired;
594
+ this.stdoutDecoder.finish(requireCompleteOutput);
595
+ this.stderrDecoder.finish(requireCompleteOutput);
596
+ await this.finalizeSpills(sandbox);
597
+ return outcome;
598
+ } catch (error) {
599
+ const canceledPreparation = preparing && this.terminationController.signal.aborted;
600
+ let failure = await this.rollbackPublishedFailure(error);
601
+ if (sandbox !== void 0 && this.stateDirectoryCreated) try {
602
+ await this.removeFailedState(sandbox);
603
+ } catch (cleanupError) {
604
+ failure = new AggregateError([failure, cleanupError], "subprocess-e2b: command failed and private state cleanup failed");
605
+ }
606
+ this.commandState.resolve(void 0);
607
+ this.readyState.reject(failure);
608
+ if (canceledPreparation && failure === error) return {
609
+ exitCode: null,
610
+ signal: "SIGTERM"
611
+ };
612
+ throw failure;
613
+ } finally {
614
+ this.spec.signal?.removeEventListener("abort", this.onAbort);
615
+ this.stdout?.end();
616
+ this.stderr?.end();
617
+ }
618
+ }
619
+ async prepareState(sandbox) {
620
+ const signal = this.terminationController.signal;
621
+ const ambient = await readRemoteEnvironment(sandbox, signal);
622
+ this.controlEnvs = bootstrapEnvironment(ambient);
623
+ this.stateDirectoryCreated = true;
624
+ await sandbox.files.makeDir(this.stateDir, { signal });
625
+ await sandbox.commands.run(`chmod 700 -- ${quoteE2BShellArg(this.stateDir)}`, commandOpts(this.controlEnvs, signal));
626
+ const files = [
627
+ {
628
+ path: this.paths.pid,
629
+ data: ""
630
+ },
631
+ {
632
+ path: this.paths.status,
633
+ data: ""
634
+ },
635
+ {
636
+ path: this.paths.environment,
637
+ data: serializeRemoteEnvironment(ambient, this.spec.env)
638
+ },
639
+ ...hasSpill(this.spec.stdio.stdout) ? [{
640
+ path: this.paths.stdout,
641
+ data: ""
642
+ }] : [],
643
+ ...hasSpill(this.spec.stdio.stderr) ? [{
644
+ path: this.paths.stderr,
645
+ data: ""
646
+ }] : []
647
+ ];
648
+ await sandbox.files.write(files, { signal });
649
+ await sandbox.commands.run(`chmod 600 -- ${files.map((file) => quoteE2BShellArg(file.path)).join(" ")}`, commandOpts(this.controlEnvs, signal));
650
+ signal.throwIfAborted();
651
+ }
652
+ async writeBatchStdin(handle) {
653
+ if (typeof this.spec.stdio.stdin !== "object") return;
654
+ try {
655
+ await handle.sendStdin(this.spec.stdio.stdin.data);
656
+ await handle.closeStdin();
657
+ } catch (_processClosedItsInput) {}
658
+ }
659
+ async dispatchOutput(stream, data) {
660
+ let bytes;
661
+ try {
662
+ bytes = stream === "stdout" ? this.stdoutDecoder.push(data) : this.stderrDecoder.push(data);
663
+ } catch (error) {
664
+ this.outputTransportError ??= asError(error);
665
+ (stream === "stdout" ? this.stdout : this.stderr)?.destroy(this.outputTransportError);
666
+ return;
667
+ }
668
+ try {
669
+ if (stream === "stdout") {
670
+ this.stdoutReader?.push(bytes);
671
+ await this.writeOutput(this.stdout, this.spec.stdio.stdout === "inherit" ? process.stdout : void 0, bytes);
672
+ return;
673
+ }
674
+ this.stderrReader?.push(bytes);
675
+ await this.writeOutput(this.stderr, this.spec.stdio.stderr === "inherit" ? process.stderr : void 0, bytes);
676
+ } catch (error) {
677
+ (stream === "stdout" ? this.stdout : this.stderr)?.destroy(asError(error));
678
+ }
679
+ }
680
+ async writeOutput(pipe, inherited, data) {
681
+ const target = pipe ?? inherited;
682
+ if (target === void 0 || data.length === 0 || this.terminationController.signal.aborted) return;
683
+ if (target.destroyed) throw new Error("subprocess output stream is closed");
684
+ if (target.write(data)) return;
685
+ await new Promise((resolve, reject) => {
686
+ const onDrain = () => {
687
+ cleanup();
688
+ resolve();
689
+ };
690
+ const onClose = () => {
691
+ cleanup();
692
+ resolve();
693
+ };
694
+ const onRelease = () => {
695
+ cleanup();
696
+ resolve();
697
+ };
698
+ const onError = (error) => {
699
+ cleanup();
700
+ reject(error);
701
+ };
702
+ const cleanup = () => {
703
+ target.removeListener("drain", onDrain);
704
+ target.removeListener("close", onClose);
705
+ target.removeListener("error", onError);
706
+ this.terminationController.signal.removeEventListener("abort", onRelease);
707
+ this.outputReleased.signal.removeEventListener("abort", onRelease);
708
+ };
709
+ target.once("drain", onDrain);
710
+ target.once("close", onClose);
711
+ target.once("error", onError);
712
+ this.terminationController.signal.addEventListener("abort", onRelease, { once: true });
713
+ this.outputReleased.signal.addEventListener("abort", onRelease, { once: true });
714
+ if (this.terminationController.signal.aborted || this.outputReleased.signal.aborted) onRelease();
715
+ });
716
+ }
717
+ async waitForProcessGroupId(sandbox, completion) {
718
+ const commandSettled = completion.then(() => true, () => true);
719
+ while (true) {
720
+ const value = (await sandbox.files.read(this.paths.pid)).trim();
721
+ if (value.length > 0) {
722
+ const pid = Number(value);
723
+ if (!/^[1-9][0-9]*$/.test(value) || !Number.isSafeInteger(pid)) throw new Error(`subprocess-e2b: remote wrapper published invalid process-group id ${JSON.stringify(value)}`);
724
+ if (pid <= 1) throw new Error(`subprocess-e2b: unsafe published process-group id ${pid}`);
725
+ return pid;
726
+ }
727
+ if (await Promise.race([commandSettled, waitTick(this.pollMs).then(() => false)])) throw new Error("subprocess-e2b: remote command exited before publishing its process-group id");
728
+ }
729
+ }
730
+ async waitForCommand(sandbox, handle, completion) {
731
+ const settlement = completion.then((result) => ({
732
+ kind: "result",
733
+ result
734
+ }), (error) => ({
735
+ kind: "error",
736
+ error
737
+ }));
738
+ let completed = this.spec.stdio.stdout === "pipe" || this.spec.stdio.stderr === "pipe" ? await settlement : void 0;
739
+ while (true) {
740
+ const rawStatus = (await sandbox.files.read(this.paths.status)).trim();
741
+ if (rawStatus.length > 0) {
742
+ const exitCode = Number(rawStatus);
743
+ if (!/^(?:0|[1-9][0-9]*)$/.test(rawStatus) || !Number.isSafeInteger(exitCode) || exitCode > 255) throw new Error(`subprocess-e2b: remote wrapper published invalid exit code ${JSON.stringify(rawStatus)}`);
744
+ if (completed !== void 0) return this.commandOutcome(completed, exitCode);
745
+ const drained = await withinMs(settlement, this.spec.graceMs);
746
+ if (drained !== void 0) return this.commandOutcome(drained, exitCode);
747
+ this.outputDrainExpired = true;
748
+ this.stdoutReader?.invalidateSpill();
749
+ this.stderrReader?.invalidateSpill();
750
+ this.outputReleased.abort(/* @__PURE__ */ new Error("subprocess-e2b: output drain grace expired"));
751
+ await handle.disconnect();
752
+ return {
753
+ exitCode,
754
+ signal: null
755
+ };
756
+ }
757
+ if (completed !== void 0) return this.commandOutcome(completed);
758
+ completed = await Promise.race([settlement, waitTick(this.pollMs).then(() => void 0)]);
759
+ }
760
+ }
761
+ commandOutcome(settlement, publishedExitCode) {
762
+ if (settlement.kind === "result") return {
763
+ exitCode: publishedExitCode ?? settlement.result.exitCode,
764
+ signal: null
765
+ };
766
+ if (settlement.error instanceof CommandExitError) {
767
+ if (publishedExitCode !== void 0) return {
768
+ exitCode: publishedExitCode,
769
+ signal: null
770
+ };
771
+ return this.terminationSignal === null ? {
772
+ exitCode: settlement.error.exitCode,
773
+ signal: null
774
+ } : {
775
+ exitCode: null,
776
+ signal: this.terminationSignal
777
+ };
778
+ }
779
+ throw settlement.error;
780
+ }
781
+ async rollbackPublishedFailure(error) {
782
+ if (this.remotePid <= 0 || this.quiescenceProven) return error;
783
+ this.terminate();
784
+ try {
785
+ await this.waitForExit();
786
+ return error;
787
+ } catch (cleanupError) {
788
+ return new AggregateError([asError(error), asError(cleanupError)], "subprocess-e2b: command monitoring failed and process-group rollback did not reach quiescence");
789
+ }
790
+ }
791
+ async rollbackUnpublishedGroup(sandbox, handle) {
792
+ await this.forceKillGroup(sandbox, handle, handle.pid);
793
+ this.markQuiescent();
794
+ }
795
+ async terminateRemote() {
796
+ try {
797
+ await this.terminateRemoteInSandbox();
798
+ } catch (error) {
799
+ if (error instanceof SandboxNotFoundError) {
800
+ this.markQuiescent();
801
+ return;
802
+ }
803
+ throw error;
804
+ }
805
+ }
806
+ async terminateRemoteInSandbox() {
807
+ const handle = await this.commandState.promise;
808
+ if (handle === void 0) {
809
+ this.markQuiescent();
810
+ return;
811
+ }
812
+ if (!isValidProcessId(handle.pid) && this.remotePid <= 0) {
813
+ await handle.kill();
814
+ this.markQuiescent();
815
+ return;
816
+ }
817
+ const sandbox = await this.runtime.getSandbox();
818
+ const processGroupId = this.remotePid > 0 ? this.remotePid : handle.pid;
819
+ await this.terminateGroup(sandbox, handle, processGroupId);
820
+ }
821
+ async terminateGroup(sandbox, handle, processGroupId) {
822
+ this.terminationSignal = "SIGTERM";
823
+ try {
824
+ await signalRemoteGroups(sandbox, this.controlEnvs, [processGroupId], "TERM");
825
+ if (await this.waitForGroupExit(sandbox, processGroupId)) {
826
+ this.markQuiescent();
827
+ return;
828
+ }
829
+ } catch (_gracefulTerminationFailure) {}
830
+ this.terminationSignal = "SIGKILL";
831
+ await this.forceKillGroup(sandbox, handle, processGroupId);
832
+ this.markQuiescent();
833
+ }
834
+ async forceKillGroup(sandbox, handle, processGroupId) {
835
+ try {
836
+ await signalRemoteGroups(sandbox, this.controlEnvs, [processGroupId], "KILL");
837
+ } catch (_processGroupKillFailure) {}
838
+ try {
839
+ await handle.kill();
840
+ } catch (_sdkKillFailure) {}
841
+ if (await this.waitForGroupExit(sandbox, processGroupId)) return;
842
+ throw new Error(`subprocess-e2b: remote process group ${processGroupId} remained live after force termination`);
843
+ }
844
+ async waitForGroupExit(sandbox, processGroupId) {
845
+ const deadline = Date.now() + this.spec.graceMs;
846
+ while (await this.groupAlive(sandbox, processGroupId)) {
847
+ if (Date.now() >= deadline) return false;
848
+ await waitTick(this.pollMs);
849
+ }
850
+ return true;
851
+ }
852
+ throwTerminationFailure() {
853
+ if (this.terminationFailure !== void 0) throw this.terminationFailure;
854
+ }
855
+ async groupAlive(sandbox, pid, signal) {
856
+ return (await sandbox.commands.run(`set -o pipefail; ps -eo pgid=,stat= | awk '$1 == ${pid} && $2 !~ /^[ZXx]/ { live=1 } END { if (live) print "live" }'`, commandOpts(this.controlEnvs, signal)).catch((error) => {
857
+ if (signal?.aborted === true) return void 0;
858
+ if (error instanceof SandboxNotFoundError) return {
859
+ exitCode: 0,
860
+ stdout: "",
861
+ stderr: ""
862
+ };
863
+ throw error;
864
+ }))?.stdout.trim() === "live";
865
+ }
866
+ async finalizeSpills(sandbox) {
867
+ const removals = [];
868
+ const collect = (mode, reader, path) => {
869
+ if (!hasSpill(mode)) return;
870
+ const size = reader.size;
871
+ if (this.outputDrainExpired || size <= mode.maxBytes || size > mode.spill.maxBytes) removals.push(sandbox.files.remove(path).catch((_adapterPrivateSpillRemovalFailure) => {}));
872
+ };
873
+ collect(this.spec.stdio.stdout, this.stdoutReader, this.paths.stdout);
874
+ collect(this.spec.stdio.stderr, this.stderrReader, this.paths.stderr);
875
+ await Promise.all(removals);
876
+ }
877
+ async removeFailedState(sandbox) {
878
+ const failures = [];
879
+ for (const path of [this.paths.environment, this.stateDir]) try {
880
+ await sandbox.files.remove(path);
881
+ } catch (error) {
882
+ if (!(error instanceof FileNotFoundError)) failures.push(asError(error));
883
+ }
884
+ if (failures.length > 0) throw new AggregateError(failures, "subprocess-e2b: failed to remove private command state");
885
+ }
886
+ };
887
+ //#endregion
888
+ //#region lib/types/terminal.js
889
+ /** E2B PTY allocation and process-session ownership for the subprocess seam. */
890
+ const TERMINAL_RUNNER_SOURCE = [
891
+ "#!/bin/bash",
892
+ "set -euo pipefail",
893
+ "dsh_state=$1",
894
+ "mapfile -d '' -t dsh_env < \"$dsh_state/environment\"",
895
+ "mapfile -d '' -t dsh_argv < \"$dsh_state/argv\"",
896
+ "dsh_output_marker=$(<\"$dsh_state/output-marker\")",
897
+ "rm -f -- \"$dsh_state/environment\" \"$dsh_state/argv\" \"$dsh_state/output-marker\" \"$dsh_state/runner.bash\"",
898
+ "if (( ${#dsh_argv[@]} == 0 )); then",
899
+ " printf 'terminal runner received empty argv\\n' >&2",
900
+ " exit 125",
901
+ "fi",
902
+ "printf '%s' \"$dsh_output_marker\"",
903
+ "exec env -i -- \"${dsh_env[@]}\" \"${dsh_argv[@]}\"",
904
+ ""
905
+ ].join("\n");
906
+ var BootstrapOutputFilter = class {
907
+ marker;
908
+ output;
909
+ ready;
910
+ readyState = Promise.withResolvers();
911
+ pending = Buffer.alloc(0);
912
+ published = false;
913
+ constructor(marker, output) {
914
+ this.marker = marker;
915
+ this.output = output;
916
+ this.ready = this.readyState.promise;
917
+ }
918
+ push(data) {
919
+ if (this.published) {
920
+ this.write(data);
921
+ return;
922
+ }
923
+ const combined = Buffer.concat([this.pending, Buffer.from(data)]);
924
+ const markerOffset = combined.indexOf(this.marker);
925
+ if (markerOffset < 0) {
926
+ const retained = Math.min(combined.length, this.marker.length - 1);
927
+ this.pending = Buffer.from(combined.subarray(combined.length - retained));
928
+ return;
929
+ }
930
+ this.published = true;
931
+ this.pending = Buffer.alloc(0);
932
+ this.readyState.resolve();
933
+ this.write(combined.subarray(markerOffset + this.marker.length));
934
+ }
935
+ write(data) {
936
+ if (data.length > 0 && !this.output.destroyed) this.output.write(data);
937
+ }
938
+ };
939
+ async function waitForBootstrapOutput(ready, completion, signal) {
940
+ signal?.throwIfAborted();
941
+ await new Promise((resolve, reject) => {
942
+ let settled = false;
943
+ let removeAbort;
944
+ const finish = (complete) => {
945
+ if (settled) return;
946
+ settled = true;
947
+ removeAbort?.();
948
+ complete();
949
+ };
950
+ const onExit = () => {
951
+ finish(() => {
952
+ reject(/* @__PURE__ */ new Error("subprocess-e2b: terminal exited before publishing its output boundary"));
953
+ });
954
+ };
955
+ if (signal !== void 0) {
956
+ const onAbort = () => {
957
+ finish(() => {
958
+ reject(asError(signal.reason));
959
+ });
960
+ };
961
+ signal.addEventListener("abort", onAbort, { once: true });
962
+ removeAbort = () => {
963
+ signal.removeEventListener("abort", onAbort);
964
+ };
965
+ }
966
+ ready.then(() => {
967
+ finish(resolve);
968
+ });
969
+ completion.then(onExit, onExit);
970
+ });
971
+ }
972
+ function parsePositiveId(value, message) {
973
+ const raw = value.trim();
974
+ const id = Number(raw);
975
+ if (!/^[1-9][0-9]*$/.test(raw) || !Number.isSafeInteger(id)) throw new Error(message);
976
+ return id;
977
+ }
978
+ function serializeValues(values, kind) {
979
+ for (const value of values) if (value.includes("\0")) throw new Error(`subprocess-e2b: terminal ${kind} must not contain NUL bytes`);
980
+ return values.map((value) => `${value}\0`).join("");
981
+ }
982
+ async function terminalSessionId(sandbox, pid, envs, signal) {
983
+ const result = await sandbox.commands.run(`ps -o sid= -p ${pid}`, commandOpts(envs, signal));
984
+ signal?.throwIfAborted();
985
+ return parsePositiveId(result.stdout, `subprocess-e2b: cannot resolve process session for terminal ${pid}`);
986
+ }
987
+ async function sessionProcessGroups(sandbox, sessionId, envs) {
988
+ let result;
989
+ try {
990
+ result = await sandbox.commands.run(`set -o pipefail; ps -eo sid=,pgid=,stat= | awk '$1 == ${sessionId} && $3 !~ /^[ZXx]/ { print $2 }'`, commandOpts(envs));
991
+ } catch (error) {
992
+ if (error instanceof SandboxNotFoundError) return [];
993
+ throw error;
994
+ }
995
+ const groups = /* @__PURE__ */ new Set();
996
+ for (const raw of result.stdout.trim().split(/\s+/)) {
997
+ if (raw.length === 0) continue;
998
+ const group = parsePositiveId(raw, `subprocess-e2b: invalid process group ${JSON.stringify(raw)} in terminal session ${sessionId}`);
999
+ if (group <= 1) throw new Error(`subprocess-e2b: unsafe process group ${group} in terminal session ${sessionId}`);
1000
+ groups.add(group);
1001
+ }
1002
+ return [...groups];
1003
+ }
1004
+ async function awaitSessionEmpty(sandbox, sessionId, envs, graceMs, pollMs, kill = false) {
1005
+ const deadline = Date.now() + graceMs;
1006
+ for (;;) {
1007
+ const groups = await sessionProcessGroups(sandbox, sessionId, envs);
1008
+ if (groups.length === 0) return groups;
1009
+ if (kill) {
1010
+ await signalRemoteGroups(sandbox, envs, groups, "KILL");
1011
+ if (Date.now() >= deadline) return await sessionProcessGroups(sandbox, sessionId, envs);
1012
+ } else if (Date.now() >= deadline) return groups;
1013
+ await delay(Math.min(pollMs, Math.max(1, deadline - Date.now())));
1014
+ }
1015
+ }
1016
+ async function rollbackUnpublishedTerminal(sandbox, handle, completion, envs, graceMs, pollMs) {
1017
+ let topLevelExited = false;
1018
+ completion.then(() => {
1019
+ topLevelExited = true;
1020
+ }, () => {
1021
+ topLevelExited = true;
1022
+ });
1023
+ const validPid = Number.isSafeInteger(handle.pid) && handle.pid > 1;
1024
+ const attemptFailures = [];
1025
+ let sessionId;
1026
+ if (validPid) {
1027
+ sessionId = handle.pid;
1028
+ try {
1029
+ sessionId = await terminalSessionId(sandbox, handle.pid, envs);
1030
+ } catch (_sessionLookupFailure) {}
1031
+ try {
1032
+ let groups = await sessionProcessGroups(sandbox, sessionId, envs);
1033
+ if (groups.length > 0) {
1034
+ await signalRemoteGroups(sandbox, envs, groups, "TERM");
1035
+ groups = await awaitSessionEmpty(sandbox, sessionId, envs, graceMs, pollMs);
1036
+ }
1037
+ if (groups.length > 0) await awaitSessionEmpty(sandbox, sessionId, envs, graceMs, pollMs, true);
1038
+ } catch (error) {
1039
+ attemptFailures.push(asError(error));
1040
+ }
1041
+ }
1042
+ if (!topLevelExited) {
1043
+ try {
1044
+ await handle.kill();
1045
+ } catch (error) {
1046
+ if (error instanceof SandboxNotFoundError) return;
1047
+ attemptFailures.push(asError(error));
1048
+ }
1049
+ await Promise.race([completion.catch(() => void 0), delay(graceMs)]);
1050
+ }
1051
+ const proofFailures = [];
1052
+ if (sessionId !== void 0) try {
1053
+ const groups = await awaitSessionEmpty(sandbox, sessionId, envs, graceMs, pollMs, true);
1054
+ if (groups.length > 0) proofFailures.push(/* @__PURE__ */ new Error(`subprocess-e2b: terminal setup rollback failed; surviving process groups: ${groups.join(", ")}`));
1055
+ } catch (error) {
1056
+ proofFailures.push(asError(error));
1057
+ }
1058
+ if (!topLevelExited) proofFailures.push(/* @__PURE__ */ new Error(`subprocess-e2b: terminal setup rollback failed; surviving pid: ${handle.pid}`));
1059
+ if (proofFailures.length > 0) throw new AggregateError([...attemptFailures, ...proofFailures], "subprocess-e2b: terminal setup rollback did not reach quiescence");
1060
+ try {
1061
+ await handle.disconnect();
1062
+ } catch (error) {
1063
+ if (!(error instanceof SandboxNotFoundError)) throw error;
1064
+ }
1065
+ }
1066
+ /** One E2B PTY and all process groups in its remote process session. */
1067
+ var E2BTerminalHandle = class {
1068
+ sandbox;
1069
+ handle;
1070
+ output;
1071
+ completion;
1072
+ sessionId;
1073
+ controlEnvs;
1074
+ stateDir;
1075
+ graceMs;
1076
+ pollMs;
1077
+ pid;
1078
+ done;
1079
+ topLevelExited = false;
1080
+ cleanup;
1081
+ operationController = new AbortController();
1082
+ operations = /* @__PURE__ */ new Set();
1083
+ terminationSignal = null;
1084
+ constructor(sandbox, handle, output, completion, sessionId, controlEnvs, stateDir, graceMs, pollMs) {
1085
+ this.sandbox = sandbox;
1086
+ this.handle = handle;
1087
+ this.output = output;
1088
+ this.completion = completion;
1089
+ this.sessionId = sessionId;
1090
+ this.controlEnvs = controlEnvs;
1091
+ this.stateDir = stateDir;
1092
+ this.graceMs = graceMs;
1093
+ this.pollMs = pollMs;
1094
+ this.pid = handle.pid;
1095
+ this.done = this.waitForCommand();
1096
+ }
1097
+ /** @inheritdoc */
1098
+ write(data) {
1099
+ return this.trackOperation(async (signal) => {
1100
+ if (this.topLevelExited) throw new Error("terminal process has exited");
1101
+ await this.sandbox.pty.sendInput(this.pid, Buffer.from(data, "utf8"), { signal });
1102
+ });
1103
+ }
1104
+ /** @inheritdoc */
1105
+ inspectForeground() {
1106
+ return this.trackOperation((signal) => this.inspectForegroundOnce(signal));
1107
+ }
1108
+ /** @inheritdoc */
1109
+ signalForeground(signal) {
1110
+ return this.trackOperation(async (operationSignal) => {
1111
+ const foreground = await this.inspectForegroundOnce(operationSignal);
1112
+ if (foreground === void 0) throw new Error(`subprocess-e2b: cannot resolve foreground process group for terminal ${this.pid}`);
1113
+ if (signal === "SIGKILL" && foreground.processGroupId === this.pid) throw new Error("refusing to SIGKILL the terminal shell; terminate the terminal session instead");
1114
+ await this.sandbox.commands.run(`kill -${signal.slice(3)} -- -${foreground.processGroupId}`, commandOpts(this.controlEnvs, operationSignal));
1115
+ return foreground.processGroupId;
1116
+ });
1117
+ }
1118
+ /** @inheritdoc */
1119
+ terminate() {
1120
+ if (this.cleanup !== void 0) return this.cleanup;
1121
+ this.operationController.abort(/* @__PURE__ */ new Error("subprocess-e2b: terminal is terminating"));
1122
+ const cleanup = this.closeAfterOperations();
1123
+ this.cleanup = cleanup;
1124
+ cleanup.catch((_cleanupFailure) => {
1125
+ this.cleanup = void 0;
1126
+ });
1127
+ return cleanup;
1128
+ }
1129
+ async inspectForegroundOnce(signal) {
1130
+ try {
1131
+ return {
1132
+ processGroupId: parsePositiveId((await this.sandbox.commands.run(`ps -o tpgid= -p ${this.pid}`, commandOpts(this.controlEnvs, signal))).stdout, `subprocess-e2b: cannot resolve foreground process group for terminal ${this.pid}`),
1133
+ inputWaiting: false
1134
+ };
1135
+ } catch (error) {
1136
+ if (error instanceof CommandExitError && (error.exitCode === 1 || this.topLevelExited)) return void 0;
1137
+ throw error;
1138
+ }
1139
+ }
1140
+ trackOperation(operation) {
1141
+ if (this.operationController.signal.aborted) return Promise.reject(/* @__PURE__ */ new Error("subprocess-e2b: terminal is terminating"));
1142
+ const pending = operation(this.operationController.signal);
1143
+ this.operations.add(pending);
1144
+ pending.then(() => {
1145
+ this.operations.delete(pending);
1146
+ }, () => {
1147
+ this.operations.delete(pending);
1148
+ });
1149
+ return pending;
1150
+ }
1151
+ async closeAfterOperations() {
1152
+ await Promise.allSettled(this.operations);
1153
+ await this.closeOnce();
1154
+ }
1155
+ async waitForCommand() {
1156
+ try {
1157
+ return {
1158
+ exitCode: (await this.completion).exitCode,
1159
+ signal: null
1160
+ };
1161
+ } catch (error) {
1162
+ if (error instanceof CommandExitError) return this.terminationSignal === null ? {
1163
+ exitCode: error.exitCode,
1164
+ signal: null
1165
+ } : {
1166
+ exitCode: null,
1167
+ signal: this.terminationSignal
1168
+ };
1169
+ this.output.destroy(error instanceof Error ? error : new Error(String(error)));
1170
+ throw error;
1171
+ } finally {
1172
+ this.topLevelExited = true;
1173
+ if (!this.output.destroyed) this.output.end();
1174
+ }
1175
+ }
1176
+ async closeOnce() {
1177
+ let groups = await sessionProcessGroups(this.sandbox, this.sessionId, this.controlEnvs);
1178
+ if (groups.length > 0) {
1179
+ this.terminationSignal = "SIGTERM";
1180
+ await signalRemoteGroups(this.sandbox, this.controlEnvs, groups, "TERM");
1181
+ groups = await awaitSessionEmpty(this.sandbox, this.sessionId, this.controlEnvs, this.graceMs, this.pollMs);
1182
+ }
1183
+ if (groups.length === 0 && !this.topLevelExited) await Promise.race([this.done.catch(() => void 0), delay(this.graceMs)]);
1184
+ if (groups.length > 0 || !this.topLevelExited) {
1185
+ this.terminationSignal = "SIGKILL";
1186
+ if (!this.topLevelExited) try {
1187
+ await this.handle.kill();
1188
+ } catch (error) {
1189
+ if (error instanceof SandboxNotFoundError) return;
1190
+ throw error;
1191
+ }
1192
+ groups = await awaitSessionEmpty(this.sandbox, this.sessionId, this.controlEnvs, this.graceMs, this.pollMs, true);
1193
+ if (!this.topLevelExited) await Promise.race([this.done.catch(() => void 0), delay(this.graceMs)]);
1194
+ }
1195
+ if (groups.length > 0) throw new Error(`subprocess-e2b: terminal cleanup failed; surviving process groups: ${groups.join(", ")}`);
1196
+ if (!this.topLevelExited) throw new Error(`subprocess-e2b: terminal cleanup failed; surviving pid: ${this.pid}`);
1197
+ try {
1198
+ await this.handle.disconnect();
1199
+ } catch (error) {
1200
+ if (!(error instanceof SandboxNotFoundError)) throw error;
1201
+ }
1202
+ try {
1203
+ await this.sandbox.files.remove(this.stateDir);
1204
+ } catch (_adapterPrivateStateRemovalFailure) {}
1205
+ }
1206
+ };
1207
+ /**
1208
+ * Allocate an E2B PTY, replace its bootstrap shell with the requested argv,
1209
+ * and return only after the private runner has published readiness.
1210
+ * @param runtime - Shared E2B sandbox owner.
1211
+ * @param spec - Fully specified terminal-process request.
1212
+ * @param stateDir - Private remote directory for one startup transaction.
1213
+ * @param pollMs - Remote session liveness poll cadence.
1214
+ * @returns The live subprocess terminal handle.
1215
+ */
1216
+ async function spawnE2BTerminal(runtime, spec, stateDir, pollMs) {
1217
+ const sandbox = await runtime.getSandbox();
1218
+ spec.signal?.throwIfAborted();
1219
+ const paths = {
1220
+ runner: posix.join(stateDir, "runner.bash"),
1221
+ environment: posix.join(stateDir, "environment"),
1222
+ argv: posix.join(stateDir, "argv"),
1223
+ outputMarker: posix.join(stateDir, "output-marker")
1224
+ };
1225
+ const outputMarker = Buffer.from(`dsh-e2b-bootstrap:${randomUUID()}`);
1226
+ const output = new PassThrough();
1227
+ const outputFilter = new BootstrapOutputFilter(outputMarker, output);
1228
+ let handle;
1229
+ let completion;
1230
+ let stateDirectoryCreated = false;
1231
+ let controlEnvs = {};
1232
+ try {
1233
+ const ambient = await readRemoteEnvironment(sandbox, spec.signal);
1234
+ controlEnvs = bootstrapEnvironment(ambient);
1235
+ const environment = serializeRemoteEnvironment(ambient, spec.env);
1236
+ const argv = serializeValues(spec.argv, "argv");
1237
+ stateDirectoryCreated = true;
1238
+ await sandbox.files.makeDir(stateDir, signalOpts(spec.signal));
1239
+ await sandbox.commands.run(`chmod 700 -- ${quoteE2BShellArg(stateDir)}`, commandOpts(controlEnvs, spec.signal));
1240
+ await sandbox.files.write([
1241
+ {
1242
+ path: paths.runner,
1243
+ data: TERMINAL_RUNNER_SOURCE
1244
+ },
1245
+ {
1246
+ path: paths.environment,
1247
+ data: environment
1248
+ },
1249
+ {
1250
+ path: paths.argv,
1251
+ data: argv
1252
+ },
1253
+ {
1254
+ path: paths.outputMarker,
1255
+ data: outputMarker.toString("utf8")
1256
+ }
1257
+ ], signalOpts(spec.signal));
1258
+ await sandbox.commands.run(`chmod 600 -- ${quoteE2BShellArg(paths.runner)} ${quoteE2BShellArg(paths.environment)} ${quoteE2BShellArg(paths.argv)} ${quoteE2BShellArg(paths.outputMarker)}`, commandOpts(controlEnvs, spec.signal));
1259
+ handle = await sandbox.pty.create({
1260
+ rows: spec.rows,
1261
+ cols: spec.cols,
1262
+ cwd: spec.cwd,
1263
+ envs: e2bControlEnvs(controlEnvs),
1264
+ timeoutMs: 0,
1265
+ onData: (data) => {
1266
+ outputFilter.push(data);
1267
+ }
1268
+ });
1269
+ completion = handle.wait();
1270
+ completion.catch(() => {});
1271
+ spec.signal?.throwIfAborted();
1272
+ if (!Number.isSafeInteger(handle.pid) || handle.pid <= 0) throw new Error(`subprocess-e2b: E2B returned invalid terminal pid ${handle.pid}`);
1273
+ const command = `exec /bin/bash ${quoteE2BShellArg(paths.runner)} ${quoteE2BShellArg(stateDir)}\r`;
1274
+ await sandbox.pty.sendInput(handle.pid, Buffer.from(command), signalOpts(spec.signal));
1275
+ await waitForBootstrapOutput(outputFilter.ready, completion, spec.signal);
1276
+ const sessionId = await terminalSessionId(sandbox, handle.pid, controlEnvs, spec.signal);
1277
+ return new E2BTerminalHandle(sandbox, handle, output, completion, sessionId, controlEnvs, stateDir, spec.graceMs, pollMs);
1278
+ } catch (error) {
1279
+ output.destroy();
1280
+ let terminalQuiescent = handle === void 0;
1281
+ let stateRemoved = !stateDirectoryCreated;
1282
+ const cleanup = async () => {
1283
+ const failures = [];
1284
+ if (!terminalQuiescent && handle !== void 0) try {
1285
+ if (completion === void 0) await handle.kill();
1286
+ else await rollbackUnpublishedTerminal(sandbox, handle, completion, controlEnvs, spec.graceMs, pollMs);
1287
+ terminalQuiescent = true;
1288
+ } catch (cleanupError) {
1289
+ if (cleanupError instanceof SandboxNotFoundError) terminalQuiescent = true;
1290
+ else failures.push(asError(cleanupError));
1291
+ }
1292
+ if (!stateRemoved) try {
1293
+ await sandbox.files.remove(stateDir);
1294
+ stateRemoved = true;
1295
+ } catch (stateError) {
1296
+ if (stateError instanceof FileNotFoundError || stateError instanceof SandboxNotFoundError) stateRemoved = true;
1297
+ else failures.push(asError(stateError));
1298
+ }
1299
+ if (failures.length > 0) throw new AggregateError(failures, "subprocess-e2b: terminal setup cleanup did not complete");
1300
+ };
1301
+ try {
1302
+ await cleanup();
1303
+ } catch (cleanupError) {
1304
+ throw new AggregateError([asError(error), asError(cleanupError)], asError(error).message);
1305
+ }
1306
+ throw error;
1307
+ }
1308
+ }
1309
+ //#endregion
1310
+ //#region lib/types/index.js
1311
+ /**
1312
+ * E2B Service provider for the subprocess capability seam. Each handle starts through the
1313
+ * shared sandbox and retains command output/status paths in that remote world.
1314
+ * @module @deepseek-ai/dsh-subprocess-e2b
1315
+ */
1316
+ /**
1317
+ * Enforce the seam's documented grace bound (positive, finite, one Node timer),
1318
+ * matching subprocess-local's spawn-time check; an unbounded grace would make
1319
+ * the remote force-escalation deadline unreachable.
1320
+ * @param graceMs - The spec's cleanup grace in milliseconds.
1321
+ */
1322
+ function requireRepresentableGrace(graceMs) {
1323
+ if (!Number.isFinite(graceMs) || graceMs <= 0 || graceMs > MAX_TIMER_DELAY_MS) throw new Error(`subprocess graceMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
1324
+ }
1325
+ /** E2B command manager registered as `ctx.subprocess`. */
1326
+ var E2BSubprocessService = class extends SubprocessService {
1327
+ static inject = ["e2b"];
1328
+ static Config = z.object({ pollMs: z.number().default(20) });
1329
+ live = /* @__PURE__ */ new Set();
1330
+ terminals = /* @__PURE__ */ new Set();
1331
+ terminalSetups = /* @__PURE__ */ new Set();
1332
+ pollMs;
1333
+ disposing = false;
1334
+ /** Create the E2B subprocess service and bind its disposal policy. */
1335
+ constructor(ctx, config) {
1336
+ super(ctx);
1337
+ const { pollMs } = config;
1338
+ if (!Number.isSafeInteger(pollMs) || pollMs <= 0) throw new Error("subprocess-e2b: pollMs must be a positive safe integer");
1339
+ this.pollMs = pollMs;
1340
+ ctx.effect(() => async () => {
1341
+ this.disposing = true;
1342
+ for (const setup of this.terminalSetups) setup.controller.abort(/* @__PURE__ */ new Error("subprocess-e2b: service disposed during terminal setup"));
1343
+ await Promise.all([...this.terminalSetups].map((setup) => setup.done));
1344
+ const handles = [...this.live];
1345
+ const terminals = [...this.terminals];
1346
+ const pending = [];
1347
+ for (const handle of handles) {
1348
+ handle.terminate();
1349
+ pending.push(handle.waitForExit().then(async () => {
1350
+ await handle.done.catch(() => void 0);
1351
+ this.live.delete(handle);
1352
+ }));
1353
+ }
1354
+ for (const terminal of terminals) pending.push(terminal.terminate().then(() => {
1355
+ this.terminals.delete(terminal);
1356
+ }));
1357
+ const failures = (await Promise.allSettled(pending)).flatMap((outcome) => outcome.status === "rejected" ? [outcome.reason] : []);
1358
+ if (failures.length === 1) throw asError(failures[0]);
1359
+ if (failures.length > 1) throw new AggregateError(failures, "subprocess-e2b: teardown failed");
1360
+ }, "e2b subprocess teardown");
1361
+ }
1362
+ /** @inheritdoc */
1363
+ async resolveExecutable(command, env, signal) {
1364
+ if (command.length === 0) throw new Error("subprocess-e2b: executable name must be non-empty");
1365
+ signal?.throwIfAborted();
1366
+ const sandbox = await this.ctx.e2b.getSandbox();
1367
+ if (posix.isAbsolute(command)) {
1368
+ await sandbox.commands.run(`test -f ${quoteE2BShellArg(command)} -a -x ${quoteE2BShellArg(command)}`, {
1369
+ envs: e2bControlEnvs(),
1370
+ ...signalOpts(signal)
1371
+ });
1372
+ signal?.throwIfAborted();
1373
+ return command;
1374
+ }
1375
+ if (command.includes("/")) throw new Error(`subprocess-e2b: command ${JSON.stringify(command)} is a relative path; use an absolute path or a bare PATH name`);
1376
+ const path = env?.PATH;
1377
+ const prefix = path === void 0 ? "" : `PATH=${quoteE2BShellArg(path)} `;
1378
+ const result = await sandbox.commands.run(`${prefix}command -v -- ${quoteE2BShellArg(command)}`, {
1379
+ cwd: this.ctx.e2b.cwd,
1380
+ envs: e2bControlEnvs(),
1381
+ ...signalOpts(signal)
1382
+ });
1383
+ signal?.throwIfAborted();
1384
+ const executable = result.stdout.trim();
1385
+ if (executable.includes("\n") || !posix.isAbsolute(executable) && !executable.includes("/")) throw new Error(`subprocess-e2b: executable ${JSON.stringify(command)} did not resolve to one absolute path`);
1386
+ return posix.resolve(this.ctx.e2b.cwd, executable);
1387
+ }
1388
+ /** @inheritdoc */
1389
+ spawn(spec) {
1390
+ if (this.disposing) throw new Error("subprocess-e2b: service is disposing");
1391
+ const program = spec.argv[0];
1392
+ if (program === void 0 || program.length === 0) throw new Error("invalid argv: expected a non-empty program name at argv[0]");
1393
+ requireRepresentableGrace(spec.graceMs);
1394
+ if (spec.signal?.aborted === true) throw new Error(`aborted before spawn: ${String(spec.signal.reason)}`);
1395
+ const stateDir = posix.join(this.ctx.e2b.runtimeRoot, "processes", randomUUID());
1396
+ const handle = new E2BSubprocessHandle(this.ctx.e2b, spec, stateDir, this.pollMs);
1397
+ this.live.add(handle);
1398
+ const release = async () => {
1399
+ await handle.waitForExit();
1400
+ this.live.delete(handle);
1401
+ };
1402
+ handle.done.then(release, release).catch((_automaticReleaseFailure) => {});
1403
+ return handle;
1404
+ }
1405
+ /** @inheritdoc */
1406
+ async spawnTerminal(spec) {
1407
+ if (this.disposing) throw new Error("subprocess-e2b: service is disposing");
1408
+ const program = spec.argv[0];
1409
+ if (program === void 0 || program.length === 0) throw new Error("subprocess-e2b: terminal argv must contain a program");
1410
+ requireRepresentableGrace(spec.graceMs);
1411
+ spec.signal?.throwIfAborted();
1412
+ const stateDir = posix.join(this.ctx.e2b.runtimeRoot, "terminals", randomUUID());
1413
+ const done = Promise.withResolvers();
1414
+ const setup = {
1415
+ done: done.promise,
1416
+ controller: new AbortController()
1417
+ };
1418
+ const setupSignal = spec.signal === void 0 ? setup.controller.signal : AbortSignal.any([spec.signal, setup.controller.signal]);
1419
+ this.terminalSetups.add(setup);
1420
+ try {
1421
+ const terminal = await spawnE2BTerminal(this.ctx.e2b, {
1422
+ ...spec,
1423
+ signal: setupSignal
1424
+ }, stateDir, this.pollMs);
1425
+ this.terminals.add(terminal);
1426
+ if (this.disposing) {
1427
+ await terminal.terminate();
1428
+ this.terminals.delete(terminal);
1429
+ throw new Error("subprocess-e2b: service disposed during terminal setup");
1430
+ }
1431
+ const release = async () => {
1432
+ await terminal.terminate();
1433
+ this.terminals.delete(terminal);
1434
+ };
1435
+ terminal.done.then(release, release).catch((_automaticReleaseFailure) => {});
1436
+ return terminal;
1437
+ } finally {
1438
+ this.terminalSetups.delete(setup);
1439
+ done.resolve();
1440
+ }
1441
+ }
1442
+ };
1443
+ //#endregion
1444
+ export { E2BSubprocessService, E2BSubprocessService as default };