@molecule/api-code-sandbox-e2b 1.2.8 → 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":"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"}
package/dist/provider.js CHANGED
@@ -43,6 +43,41 @@ const EXIT_REPORT_GRACE_MS = 2_000;
43
43
  * build (partial `dist/` and all) as a successful one.
44
44
  */
45
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
+ }
46
81
  /** Default lifetime for a PTY session; same reasoning as {@link DEFAULT_SPAWN_TIMEOUT_MS}. */
47
82
  const DEFAULT_PTY_TIMEOUT_MS = 60 * 60 * 1000;
48
83
  /**
@@ -56,6 +91,32 @@ const ALL_TRAFFIC = '0.0.0.0/0';
56
91
  /** Hosts the egress probe treats as the canonical allow/deny witnesses. */
57
92
  const EGRESS_PROBE_ALLOW = 'registry.npmjs.org';
58
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
+ }
59
120
  /**
60
121
  * Adapt the real `e2b` SDK `Sandbox` class to {@link E2BSandboxClientLike}.
61
122
  * Imported lazily so the SDK is only required when the bond is actually used.
@@ -108,6 +169,14 @@ async function defaultClient(apiKey) {
108
169
  volumeMounts: it.volumeMounts,
109
170
  });
110
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).
111
180
  const r = await Promise.resolve(result).catch(() => result);
112
181
  if (Array.isArray(r)) {
113
182
  push(r);
@@ -126,6 +195,26 @@ async function defaultClient(apiKey) {
126
195
  else if (r && Array.isArray(r.sandboxes)) {
127
196
  push(r.sandboxes);
128
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
+ }
129
218
  return out;
130
219
  },
131
220
  };
@@ -342,10 +431,7 @@ class E2BSandbox {
342
431
  const finished = handle.wait().finally(() => {
343
432
  settled = true;
344
433
  });
345
- const raced = await Promise.race([
346
- finished,
347
- new Promise((resolve) => setTimeout(() => resolve(null), SETTLE_PROBE_MS)),
348
- ]);
434
+ const raced = await raceWithClearedTimer(finished, SETTLE_PROBE_MS);
349
435
  if (raced)
350
436
  return raced;
351
437
  if (settled)
@@ -357,10 +443,7 @@ class E2BSandbox {
357
443
  // exit code rides the stream's `end` event, so give the handle a bounded
358
444
  // window to deliver it — when the process truly finished, envd closes the
359
445
  // stream right behind it and the result lands here.
360
- const reported = await Promise.race([
361
- finished,
362
- new Promise((resolve) => setTimeout(() => resolve(null), EXIT_REPORT_GRACE_MS)),
363
- ]);
446
+ const reported = await raceWithClearedTimer(finished, EXIT_REPORT_GRACE_MS);
364
447
  if (reported)
365
448
  return reported;
366
449
  if (typeof handle.exitCode === 'number') {
@@ -685,6 +768,36 @@ class E2BSandbox {
685
768
  // Returning a no-op unsubscribe keeps callers that register-and-forget safe.
686
769
  return () => { };
687
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
+ }
688
801
  /**
689
802
  * Extract a POSIX tar stream into the sandbox at `path`.
690
803
  *
@@ -715,7 +828,7 @@ class E2BSandbox {
715
828
  pending = [];
716
829
  pendingBytes = 0;
717
830
  await this.sbx.files.write(piecePath, new Blob([piece]));
718
- 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);
719
832
  if (r.exitCode !== 0) {
720
833
  throw new Error(`importFiles: spooling the archive failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
721
834
  }
@@ -737,9 +850,18 @@ class E2BSandbox {
737
850
  }
738
851
  }
739
852
  await flush();
740
- if (pieces === 0)
741
- await this.sbx.commands.run(`: > ${tarPath}`, { timeoutMs: 30_000 });
742
- 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);
743
865
  if (r.exitCode !== 0) {
744
866
  throw new Error(`importFiles: tar extract failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
745
867
  }
@@ -779,7 +901,7 @@ class E2BSandbox {
779
901
  const tarPath = `/tmp/mol-export-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.tar`;
780
902
  let bytes;
781
903
  try {
782
- 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);
783
905
  if (r.exitCode !== 0) {
784
906
  throw new Error(`exportFiles: tar create failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
785
907
  }
@@ -817,11 +939,14 @@ class E2BSandbox {
817
939
  /**
818
940
  * E2B implementation of {@link SandboxProvider}.
819
941
  *
820
- * Only the required surface (`create`/`get`/`list`/`destroy`) plus the boot-path
821
- * optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
822
- * land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
823
- * the control plane treats "unsupported" as `inconclusive` and refuses to boot
824
- * 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.
825
950
  */
826
951
  export class E2BSandboxProvider {
827
952
  name = 'e2b';
@@ -1032,18 +1157,23 @@ export class E2BSandboxProvider {
1032
1157
  const client = await this.client();
1033
1158
  const running = await client.list({});
1034
1159
  const items = Array.isArray(running) ? running : (running.sandboxes ?? []);
1035
- const handles = [];
1036
- 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) => {
1037
1169
  // A PAUSED sandbox is deliberately skipped: building its handle means
1038
1170
  // connecting, and connecting resumes it. Enumerating an account would
1039
1171
  // otherwise wake — and start billing — every hibernated project on it.
1040
1172
  if (it.state === 'paused')
1041
- continue;
1042
- const h = await this.get(it.sandboxId);
1043
- if (h)
1044
- handles.push(h);
1045
- }
1046
- return handles;
1173
+ return null;
1174
+ return this.get(it.sandboxId);
1175
+ });
1176
+ return handles.filter((h) => h !== null);
1047
1177
  }
1048
1178
  /**
1049
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.8",
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",