@molecule/api-code-sandbox-e2b 1.2.7 → 1.2.9

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/README.md CHANGED
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
3
  Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
4
  Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
5
  To change this document, edit the module-level JSDoc in src/index.ts.
6
- Generated: 2026-10-05T07:29:02.107Z
6
+ Generated: 2026-10-09T06:56:17.299Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-code-sandbox-e2b
@@ -396,11 +396,14 @@ interface EgressProbeCodes {
396
396
 
397
397
  E2B implementation of {@link SandboxProvider}.
398
398
 
399
- Only the required surface (`create`/`get`/`list`/`destroy`) plus the boot-path
400
- optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
401
- land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
402
- the control plane treats "unsupported" as `inconclusive` and refuses to boot
403
- in prod, which is the correct safe default until egress observation is proven.
399
+ The required surface (`create`/`get`/`list`/`destroy`) plus the optionals the
400
+ control plane uses are all wired here: `describe`, volumes
401
+ (`createVolume`/`removeVolume`/`volumeExists`/`listVolumes`), snapshots
402
+ (`commitTemplate`/`getTemplate`/`listTemplates`/`removeTemplate`) and
403
+ `verifyEgress`. `verifyEgress` proves deny-by-default by OBSERVING a
404
+ throwaway probe sandbox, and answers `inconclusive` whenever it cannot look —
405
+ a failed create, a dead probe — so "could not observe" never wears the shape
406
+ of the safe verdict.
404
407
 
405
408
  ### Functions
406
409
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Browser guard — `@molecule/api-code-sandbox-e2b` is SERVER-ONLY.
3
+ *
4
+ * Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
5
+ * Evaluating a server package in a browser bundle is always an import-graph mistake
6
+ * (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
7
+ * ("Buffer is not defined") far from the culprit. Throwing here names the package and
8
+ * the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
9
+ * unaffected: the throw requires browser globals AND the absence of a node runtime.
10
+ */
11
+ const g = globalThis;
12
+ if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
13
+ throw new Error('@molecule/api-code-sandbox-e2b is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
14
+ 'from server code (a server route/function or your API), or dynamic-import it inside ' +
15
+ 'the server handler — never from components or shared client modules, and never ' +
16
+ 'polyfill Buffer/process to silence this.');
17
+ }
18
+ export {};
package/dist/index.d.ts CHANGED
@@ -160,6 +160,7 @@
160
160
  *
161
161
  * @module
162
162
  */
163
+ export * from './browser-guard.js';
163
164
  export * from './provider.js';
164
165
  export * from './types.js';
165
166
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiKG;AAEH,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiKG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -160,5 +160,6 @@
160
160
  *
161
161
  * @module
162
162
  */
163
+ export * from './browser-guard.js';
163
164
  export * from './provider.js';
164
165
  export * from './types.js';
@@ -13,11 +13,14 @@ import type { E2BConfig, E2BSandboxClientLike } from './types.js';
13
13
  /**
14
14
  * E2B implementation of {@link SandboxProvider}.
15
15
  *
16
- * Only the required surface (`create`/`get`/`list`/`destroy`) plus the boot-path
17
- * optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
18
- * land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
19
- * the control plane treats "unsupported" as `inconclusive` and refuses to boot
20
- * in prod, which is the correct safe default until egress observation is proven.
16
+ * The required surface (`create`/`get`/`list`/`destroy`) plus the optionals the
17
+ * control plane uses are all wired here: `describe`, volumes
18
+ * (`createVolume`/`removeVolume`/`volumeExists`/`listVolumes`), snapshots
19
+ * (`commitTemplate`/`getTemplate`/`listTemplates`/`removeTemplate`) and
20
+ * `verifyEgress`. `verifyEgress` proves deny-by-default by OBSERVING a
21
+ * throwaway probe sandbox, and answers `inconclusive` whenever it cannot look —
22
+ * a failed create, a dead probe — so "could not observe" never wears the shape
23
+ * of the safe verdict.
21
24
  */
22
25
  export declare class E2BSandboxProvider implements SandboxProvider {
23
26
  readonly name = "e2b";
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EACV,qBAAqB,EAErB,aAAa,EAKb,oBAAoB,EACpB,kBAAkB,EAClB,OAAO,EACP,aAAa,EACb,iBAAiB,EACjB,eAAe,EACf,eAAe,EAGf,UAAU,EACX,MAAM,4BAA4B,CAAA;AAEnC,OAAO,KAAK,EAGV,SAAS,EACT,oBAAoB,EAMrB,MAAM,YAAY,CAAA;AAw+BnB;;;;;;;;;;;GAWG;AACH,qBAAa,kBAAmB,YAAW,eAAe;IACxD,QAAQ,CAAC,IAAI,SAAQ;IAErB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0D;IACjF,OAAO,CAAC,aAAa,CAA6C;IAClE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAsB;IAEtD;;;;;OAKG;gBACS,MAAM,GAAE,SAAc,EAAE,cAAc,CAAC,EAAE,oBAAoB;IAYzE;;;;OAIG;YACW,MAAM;IASpB;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACG,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAIrD;;;;;;;;;;;;;;OAcG;YACW,aAAa;IAkD3B;;;;;OAKG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAoB9C;;;;;;;;;;;;;;;OAeG;IACG,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAkC7D;;;;;OAKG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAsB/C;;;;OAIG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxC;;;;;;;;;OASG;YACW,aAAa;IAM3B;;;;;OAKG;YACW,UAAU;IASxB;;;;;;;;;;;;;;OAcG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS/C;;;;OAIG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU/C;;;;;;;;;;;OAWG;IACG,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIlD;;;;;;;;;;OAUG;IACG,WAAW,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC;IAmBtE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,eAAe,CAAC;IA6B9E;;;;;;;;;OASG;IACG,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;IAoBtE;;;;;;;;;OASG;IACG,aAAa,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAmB/E;;;;;;;;;;OAUG;IACG,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAevD;;;;;;;;;;;OAWG;IACG,YAAY,IAAI,OAAO,CAAC,aAAa,CAAC;IAwB5C;;;;;OAKG;YACW,YAAY;IAa1B;;;;;OAKG;YACW,WAAW;CA6B1B;AAED,sEAAsE;AACtE,MAAM,WAAW,gBAAgB;IAC/B,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAA;IACf,mCAAmC;IACnC,UAAU,EAAE,MAAM,CAAA;IAClB,sCAAsC;IACtC,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,gBAAgB,GAAG,aAAa,CA8B1E;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,MAAM,GAAE,SAAc,EACtB,cAAc,CAAC,EAAE,oBAAoB,GACpC,kBAAkB,CAEpB;AAED,kEAAkE;AAClE,eAAO,MAAM,QAAQ,EAAE,eAAkC,CAAA"}
package/dist/provider.js CHANGED
@@ -26,6 +26,58 @@ const DEFAULT_SPAWN_TIMEOUT_MS = 60 * 60 * 1000;
26
26
  * this instead of the caller's whole timeout.
27
27
  */
28
28
  const SETTLE_PROBE_MS = 1_500;
29
+ /**
30
+ * How long to wait for the handle to REPORT the exit code after the started pid
31
+ * is confirmed gone. envd closes the output stream right behind a real exit, so
32
+ * a finished command's `end` event — the only thing that carries its exit code —
33
+ * lands within this window; only a stream held by a stranded descendant
34
+ * outlives it.
35
+ */
36
+ const EXIT_REPORT_GRACE_MS = 2_000;
37
+ /**
38
+ * The exit code reported when a command's outcome cannot be observed at all:
39
+ * the started pid is gone but the stream it was reading is still held, so the
40
+ * SDK never delivers the `end` event that carries the real code. 128+SIGKILL
41
+ * (137), the conventional status of a killed process — because "unknown" must
42
+ * read as failure, never as the fabricated `0` that once reported a killed
43
+ * build (partial `dist/` and all) as a successful one.
44
+ */
45
+ const EXIT_CODE_UNKNOWN = 137;
46
+ /**
47
+ * How many sandbox connects {@link E2BSandboxProvider.list} keeps in flight at
48
+ * once. Each handle costs a full connect round trip — on the real SDK, a live
49
+ * websocket to the sandbox's envd — and `list()` is what a control-plane sweep
50
+ * polls, so putting ALL N running sandboxes in flight at once spends N sockets
51
+ * and file descriptors per poll and walks straight into the API's concurrency
52
+ * limits. A bounded window keeps the sweep concurrent (O(⌈N/16⌉) round trips
53
+ * deep, not N) without the per-poll fan-out growing with the fleet.
54
+ */
55
+ const LIST_CONNECT_CONCURRENCY = 16;
56
+ /**
57
+ * Maps `items` through `fn` with at most `limit` calls in flight, preserving
58
+ * order in the result. The first rejection propagates (the remaining
59
+ * in-flight calls still run to completion, their results discarded) — the
60
+ * same fail-fast `Promise.all` gives, without its unbounded concurrency.
61
+ *
62
+ * @param items - The items to map.
63
+ * @param limit - Maximum concurrent `fn` calls.
64
+ * @param fn - The async mapping.
65
+ * @returns The results, in `items` order.
66
+ */
67
+ async function mapWithConcurrency(items, limit, fn) {
68
+ const out = new Array(items.length);
69
+ let next = 0;
70
+ const worker = async () => {
71
+ for (;;) {
72
+ const index = next++;
73
+ if (index >= items.length)
74
+ return;
75
+ out[index] = await fn(items[index]);
76
+ }
77
+ };
78
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
79
+ return out;
80
+ }
29
81
  /** Default lifetime for a PTY session; same reasoning as {@link DEFAULT_SPAWN_TIMEOUT_MS}. */
30
82
  const DEFAULT_PTY_TIMEOUT_MS = 60 * 60 * 1000;
31
83
  /**
@@ -39,6 +91,32 @@ const ALL_TRAFFIC = '0.0.0.0/0';
39
91
  /** Hosts the egress probe treats as the canonical allow/deny witnesses. */
40
92
  const EGRESS_PROBE_ALLOW = 'registry.npmjs.org';
41
93
  const EGRESS_PROBE_DENY = 'example.com';
94
+ /**
95
+ * Race a promise against a one-shot timer, CLEARING the timer the moment the
96
+ * race settles — win, lose, or reject. The bare `Promise.race` leaves the
97
+ * losing timer armed: resolving into an already-settled race is a no-op, but
98
+ * the timer itself stays on the event loop for its whole window, so every
99
+ * call whose command finishes first kept an otherwise-done short-lived
100
+ * process (test worker, CLI script) alive up to the window per call.
101
+ *
102
+ * @param promise - The promise to wait on.
103
+ * @param ms - The timeout window in milliseconds.
104
+ * @returns The promise's value, or null when the window elapsed first.
105
+ */
106
+ async function raceWithClearedTimer(promise, ms) {
107
+ let timer;
108
+ try {
109
+ return await Promise.race([
110
+ promise,
111
+ new Promise((resolve) => {
112
+ timer = setTimeout(() => resolve(null), ms);
113
+ }),
114
+ ]);
115
+ }
116
+ finally {
117
+ clearTimeout(timer);
118
+ }
119
+ }
42
120
  /**
43
121
  * Adapt the real `e2b` SDK `Sandbox` class to {@link E2BSandboxClientLike}.
44
122
  * Imported lazily so the SDK is only required when the bond is actually used.
@@ -91,6 +169,14 @@ async function defaultClient(apiKey) {
91
169
  volumeMounts: it.volumeMounts,
92
170
  });
93
171
  };
172
+ // A rejected listing (the promise-of-array SDK shape) must THROW, never
173
+ // degrade to `[]`: every consumer of this listing reads emptiness as
174
+ // absence (`list()`: no live sandboxes; `listVolumes()`: every volume
175
+ // unattached; `listTemplates()`: no template in use). The trailing
176
+ // `.catch(() => result)` is inert to that — a catch handler that
177
+ // returns the SAME rejected promise re-rejects the await below, so the
178
+ // failure always reaches the caller (pinned in
179
+ // __tests__/client-list-shapes.test.ts).
94
180
  const r = await Promise.resolve(result).catch(() => result);
95
181
  if (Array.isArray(r)) {
96
182
  push(r);
@@ -109,6 +195,26 @@ async function defaultClient(apiKey) {
109
195
  else if (r && Array.isArray(r.sandboxes)) {
110
196
  push(r.sandboxes);
111
197
  }
198
+ else {
199
+ // An UNRECOGNIZED shape (a minor SDK change — a new pager wrapper, an
200
+ // `{ items: [...] }` envelope, a bare null) must THROW, never fall
201
+ // through to `[]`: every consumer of this listing reads EMPTINESS as
202
+ // ABSENCE (`list()`: no live sandboxes; `listVolumes()`: every volume
203
+ // unattached; `getTemplate`/`listTemplates()`: no template in use —
204
+ // and an eviction/reclamation sweep deletes on that answer, destroying
205
+ // resources live sandboxes are still using). An unreadable listing is
206
+ // a failure to LOOK, and this bond's contracts never deliver that as
207
+ // "looked, and nothing exists".
208
+ let seen;
209
+ try {
210
+ seen = JSON.stringify(r) ?? String(r);
211
+ }
212
+ catch (_error) {
213
+ // Unserializable (a cyclic wrapper) — the type is the diagnosable part.
214
+ seen = typeof r;
215
+ }
216
+ throw new Error(`e2b: Sandbox.list() returned a shape this adapter cannot read (${seen.slice(0, 200)}) — refusing to answer an empty listing, since every consumer of it reads emptiness as absence`);
217
+ }
112
218
  return out;
113
219
  },
114
220
  };
@@ -309,6 +415,14 @@ class E2BSandbox {
309
415
  * running and the caller's own timeout is the right bound. Never a guess from
310
416
  * the shape of the command.
311
417
  *
418
+ * But "over" is not "succeeded": the SDK delivers the exit code on the
419
+ * stream's `end` event, and the descendant holding the stream keeps that
420
+ * event from ever arriving. A gone pid therefore gets a bounded grace window
421
+ * for the handle to report the real code, and a handle that never does is
422
+ * reported as {@link EXIT_CODE_UNKNOWN} — failure — because an outcome that
423
+ * cannot be observed (a build OOM-killed mid-run) must never be fabricated
424
+ * into success.
425
+ *
312
426
  * @param handle - The started command.
313
427
  * @returns The command's result.
314
428
  */
@@ -317,10 +431,7 @@ class E2BSandbox {
317
431
  const finished = handle.wait().finally(() => {
318
432
  settled = true;
319
433
  });
320
- const raced = await Promise.race([
321
- finished,
322
- new Promise((resolve) => setTimeout(() => resolve(null), SETTLE_PROBE_MS)),
323
- ]);
434
+ const raced = await raceWithClearedTimer(finished, SETTLE_PROBE_MS);
324
435
  if (raced)
325
436
  return raced;
326
437
  if (settled)
@@ -328,11 +439,26 @@ class E2BSandbox {
328
439
  const alive = await this.isProcessAlive(handle.pid);
329
440
  if (alive)
330
441
  return finished;
331
- // The process is gone; whatever still holds the stream is not it.
442
+ // The process is gone; whatever still holds the stream is not it. The real
443
+ // exit code rides the stream's `end` event, so give the handle a bounded
444
+ // window to deliver it — when the process truly finished, envd closes the
445
+ // stream right behind it and the result lands here.
446
+ const reported = await raceWithClearedTimer(finished, EXIT_REPORT_GRACE_MS);
447
+ if (reported)
448
+ return reported;
449
+ if (typeof handle.exitCode === 'number') {
450
+ return { stdout: handle.stdout ?? '', stderr: handle.stderr ?? '', exitCode: handle.exitCode };
451
+ }
452
+ // Still nothing: the exit code is UNKNOWN. Reporting `0` here used to
453
+ // fabricate success for exactly this shape — a killed build whose stranded
454
+ // daemon held the stream shipped its partial `dist/` as a finished site —
455
+ // so fail closed instead, with a line that says why the code is missing.
456
+ const unknownNote = `e2b: exit code unknown — the started process (pid ${handle.pid}) is gone but its ` +
457
+ 'output stream is still held, so its real outcome cannot be observed; reporting failure';
332
458
  return {
333
459
  stdout: handle.stdout ?? '',
334
- stderr: handle.stderr ?? '',
335
- exitCode: handle.exitCode ?? 0,
460
+ stderr: handle.stderr ? `${handle.stderr}\n${unknownNote}` : unknownNote,
461
+ exitCode: EXIT_CODE_UNKNOWN,
336
462
  };
337
463
  }
338
464
  /**
@@ -642,6 +768,36 @@ class E2BSandbox {
642
768
  // Returning a no-op unsubscribe keeps callers that register-and-forget safe.
643
769
  return () => { };
644
770
  }
771
+ /**
772
+ * Run a short, self-contained command to completion and return its result as
773
+ * data. The SDK's inline `commands.run()` (no `background`) THROWS
774
+ * `CommandExitError` on a non-zero exit — it never returns one — so an
775
+ * `r.exitCode !== 0` check after a bare `await run()` is dead code in
776
+ * production: the caller saw a bare `CommandExitError` ("exit status N") with
777
+ * no stage name and no stderr, and only the test fakes (which return the
778
+ * result) made those checks look live. Mapping the thrown `.result` back to
779
+ * data here — the same mapping `exec()`'s catch applies — makes the callers'
780
+ * exit-code checks real on both shapes.
781
+ *
782
+ * @param cmd - The shell command to run.
783
+ * @param timeoutMs - Deadline for the command in milliseconds.
784
+ * @returns stdout, stderr and the exit code (even when non-zero).
785
+ */
786
+ async runCommand(cmd, timeoutMs) {
787
+ try {
788
+ return await this.sbx.commands.run(cmd, { timeoutMs });
789
+ }
790
+ catch (error) {
791
+ // A non-zero exit arrives as CommandExitError carrying `.result` — map it
792
+ // to data so the caller's exit-code check owns the failure. Anything else
793
+ // (timeout, connection loss) is a genuine infrastructure failure and must
794
+ // propagate untouched.
795
+ const res = error?.result;
796
+ if (res && typeof res.exitCode === 'number')
797
+ return res;
798
+ throw error;
799
+ }
800
+ }
645
801
  /**
646
802
  * Extract a POSIX tar stream into the sandbox at `path`.
647
803
  *
@@ -672,7 +828,7 @@ class E2BSandbox {
672
828
  pending = [];
673
829
  pendingBytes = 0;
674
830
  await this.sbx.files.write(piecePath, new Blob([piece]));
675
- const r = await this.sbx.commands.run(`cat ${piecePath} ${pieces === 0 ? '>' : '>>'} ${tarPath} && rm -f ${piecePath}`, { timeoutMs: 120_000 });
831
+ const r = await this.runCommand(`cat ${piecePath} ${pieces === 0 ? '>' : '>>'} ${tarPath} && rm -f ${piecePath}`, 120_000);
676
832
  if (r.exitCode !== 0) {
677
833
  throw new Error(`importFiles: spooling the archive failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
678
834
  }
@@ -694,9 +850,18 @@ class E2BSandbox {
694
850
  }
695
851
  }
696
852
  await flush();
697
- if (pieces === 0)
698
- await this.sbx.commands.run(`: > ${tarPath}`, { timeoutMs: 30_000 });
699
- const r = await this.sbx.commands.run(`mkdir -p ${shellQuote(path)} && tar xf ${tarPath} -C ${shellQuote(path)} --no-same-owner --no-same-permissions`, { timeoutMs: 300_000 });
853
+ if (pieces === 0) {
854
+ // An archive that yielded nothing still gets its (empty) tar created
855
+ // so the extract below fails as BAD ARCHIVE DATA, not as a missing
856
+ // file — and through runCommand, so ITS failure (a full disk is the
857
+ // usual cause) reports its stage and stderr like the spool/extract
858
+ // failures do, never the SDK's bare exit-status error.
859
+ const created = await this.runCommand(`: > ${tarPath}`, 30_000);
860
+ if (created.exitCode !== 0) {
861
+ throw new Error(`importFiles: creating the empty archive failed (${created.exitCode}): ${created.stderr.slice(0, 300)}`);
862
+ }
863
+ }
864
+ const r = await this.runCommand(`mkdir -p ${shellQuote(path)} && tar xf ${tarPath} -C ${shellQuote(path)} --no-same-owner --no-same-permissions`, 300_000);
700
865
  if (r.exitCode !== 0) {
701
866
  throw new Error(`importFiles: tar extract failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
702
867
  }
@@ -736,7 +901,7 @@ class E2BSandbox {
736
901
  const tarPath = `/tmp/mol-export-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.tar`;
737
902
  let bytes;
738
903
  try {
739
- const r = await this.sbx.commands.run(`tar cf ${tarPath} -C ${shellQuote(parent)} ${shellQuote(name)}`, { timeoutMs: 300_000 });
904
+ const r = await this.runCommand(`tar cf ${tarPath} -C ${shellQuote(parent)} ${shellQuote(name)}`, 300_000);
740
905
  if (r.exitCode !== 0) {
741
906
  throw new Error(`exportFiles: tar create failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
742
907
  }
@@ -774,11 +939,14 @@ class E2BSandbox {
774
939
  /**
775
940
  * E2B implementation of {@link SandboxProvider}.
776
941
  *
777
- * Only the required surface (`create`/`get`/`list`/`destroy`) plus the boot-path
778
- * optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
779
- * land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
780
- * the control plane treats "unsupported" as `inconclusive` and refuses to boot
781
- * in prod, which is the correct safe default until egress observation is proven.
942
+ * The required surface (`create`/`get`/`list`/`destroy`) plus the optionals the
943
+ * control plane uses are all wired here: `describe`, volumes
944
+ * (`createVolume`/`removeVolume`/`volumeExists`/`listVolumes`), snapshots
945
+ * (`commitTemplate`/`getTemplate`/`listTemplates`/`removeTemplate`) and
946
+ * `verifyEgress`. `verifyEgress` proves deny-by-default by OBSERVING a
947
+ * throwaway probe sandbox, and answers `inconclusive` whenever it cannot look —
948
+ * a failed create, a dead probe — so "could not observe" never wears the shape
949
+ * of the safe verdict.
782
950
  */
783
951
  export class E2BSandboxProvider {
784
952
  name = 'e2b';
@@ -989,18 +1157,23 @@ export class E2BSandboxProvider {
989
1157
  const client = await this.client();
990
1158
  const running = await client.list({});
991
1159
  const items = Array.isArray(running) ? running : (running.sandboxes ?? []);
992
- const handles = [];
993
- for (const it of items) {
1160
+ // All the per-sandbox connects are issued through a bounded-concurrency
1161
+ // window, not one awaited after the next: each `get()` is a full connect
1162
+ // round trip, and a fleet sweep over N running sandboxes must not
1163
+ // serialize N of them (O(N) latency on a method the control plane polls) —
1164
+ // while still not putting all N sockets in flight at once (see
1165
+ // {@link LIST_CONNECT_CONCURRENCY}). Same semantics as the serial loop —
1166
+ // a paused sandbox is skipped, a gone sandbox contributes no handle, any
1167
+ // other failure still throws — in ⌈N/16⌉ round-trip depth instead of N.
1168
+ const handles = await mapWithConcurrency(items, LIST_CONNECT_CONCURRENCY, async (it) => {
994
1169
  // A PAUSED sandbox is deliberately skipped: building its handle means
995
1170
  // connecting, and connecting resumes it. Enumerating an account would
996
1171
  // otherwise wake — and start billing — every hibernated project on it.
997
1172
  if (it.state === 'paused')
998
- continue;
999
- const h = await this.get(it.sandboxId);
1000
- if (h)
1001
- handles.push(h);
1002
- }
1003
- return handles;
1173
+ return null;
1174
+ return this.get(it.sandboxId);
1175
+ });
1176
+ return handles.filter((h) => h !== null);
1004
1177
  }
1005
1178
  /**
1006
1179
  * Destroy a sandbox, freeing its resources.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-code-sandbox-e2b",
3
- "version": "1.2.7",
3
+ "version": "1.2.9",
4
4
  "description": "E2B (e2b.dev) code sandbox provider — Firecracker microVMs with golden templates, fork, and pause/resume",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",