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

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-04T09:45:27.540Z
6
+ Generated: 2026-10-05T07:29:02.107Z
7
7
  -->
8
8
 
9
9
  # @molecule/api-code-sandbox-e2b
@@ -25,9 +25,8 @@ the official `e2b` SDK.
25
25
  The design that makes it fast: a single golden SUPERSET template carries the
26
26
  entire `@molecule` fleet node_modules + postgres + warmed Vite deps, so a boot
27
27
  only copies the ONE selected app's source in and starts the dev servers — no
28
- per-boot `npm install`. The 133 flagship template sources are NOT baked into
29
- the image; they are copied from the control plane at boot, so templates and
30
- `mlcl` stay private.
28
+ per-boot `npm install`. The app template sources are not baked into the
29
+ image; they are copied in at boot.
31
30
 
32
31
  ## Quick Start
33
32
 
@@ -181,7 +180,7 @@ interface E2BConfig {
181
180
  * time; everything else is denied (`denyOut: [ALL_TRAFFIC]`). Wildcards like
182
181
  * `*.npmjs.org` are supported. Empty/omitted means the bond does NOT
183
182
  * constrain egress — prod must supply this or `verifyEgress` observes `open`
184
- * and the control plane refuses to boot (Rule 18).
183
+ * and the control plane refuses to boot.
185
184
  *
186
185
  * Verified against a live E2B sandbox: with `denyOut: [ALL_TRAFFIC]`, a
187
186
  * non-allowlisted host AND a raw destination IP are both blocked — a stronger
@@ -401,8 +400,7 @@ Only the required surface (`create`/`get`/`list`/`destroy`) plus the boot-path
401
400
  optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
402
401
  land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
403
402
  the control plane treats "unsupported" as `inconclusive` and refuses to boot
404
- in prod, which is the correct safe default until egress observation is proven
405
- (Rule 18 — never trade cost for security).
403
+ in prod, which is the correct safe default until egress observation is proven.
406
404
 
407
405
  ### Functions
408
406
 
@@ -414,8 +412,7 @@ Turn the probe's curl codes into a verdict.
414
412
  egress was OBSERVED;
415
413
  - `inconclusive` whenever the allow-listed control host did not answer — a
416
414
  probe sandbox with no network at all blocks everything, which says nothing
417
- about the policy (2026-10-04: `host=000, rawIP=000, allowed=000` was read
418
- as open and production refused to boot);
415
+ about the policy;
419
416
  - `filtered` only when the control host answered AND both denied probes were
420
417
  blocked (`000` / empty).
421
418
 
@@ -498,13 +495,19 @@ Peer dependencies:
498
495
  - `@molecule/api-i18n`
499
496
  - `e2b`
500
497
 
501
- **`verifyEgress` OBSERVES, it never attests.** It boots a throwaway sandbox,
502
- applies `{ allowOut: [npm], denyOut: [ALL_TRAFFIC] }`, and curls an
503
- allow-listed host, a non-allow-listed host AND a raw IP from inside it;
504
- `filtered` requires the last two to be blocked while the first answers. Any
505
- failure to run that probe is `inconclusive` — never `filtered`, because "I
506
- could not look" must not reach a control plane as "I looked, and it is safe"
507
- (Rule 18: never trade cost for security).
498
+ - `importFiles` spools an incoming archive into the sandbox in 8 MB pieces
499
+ whatever the size of the chunks it is given; `exportFiles` reads the whole
500
+ archive into memory once, so bound what you export.
501
+ - `importFiles` trusts the stream's length: an archive cut exactly between two
502
+ tar members extracts cleanly (tar exits 0) and is reported complete. Give it
503
+ a stream that errors on truncation (an authenticated encrypted stream does)
504
+ or check a length or hash yourself.
505
+ **`verifyEgress` OBSERVES, it never attests.** It boots a throwaway sandbox,
506
+ applies `{ allowOut: [npm], denyOut: [ALL_TRAFFIC] }`, and curls an
507
+ allow-listed host, a non-allow-listed host AND a raw IP from inside it;
508
+ `filtered` requires the last two to be blocked while the first answers. Any
509
+ failure to run that probe is `inconclusive` — never `filtered`, because "I
510
+ could not look" must not reach a control plane as "I looked, and it is safe".
508
511
 
509
512
  **A probe that sees NO network is `inconclusive`, not `open`.** When the
510
513
  allow-listed control host does not answer either, the probe sandbox itself has
@@ -585,8 +588,7 @@ can never be given a project's volume afterwards — a project that needs one
585
588
  must be booted fresh with it.
586
589
 
587
590
  **Volumes are a private beta on E2B.** An account without them answers
588
- `403 use of volumes is not enabled` (measured on the production account,
589
- 2026-08-16); ask E2B support to enable them. Every volume method throws in
591
+ `403 use of volumes is not enabled`; ask E2B support to enable them. Every volume method throws in
590
592
  that state rather than no-op-ing, because a control plane that believes it has
591
593
  durable storage and does not is the failure this bond exists to prevent.
592
594
 
package/dist/index.d.ts CHANGED
@@ -12,9 +12,8 @@
12
12
  * The design that makes it fast: a single golden SUPERSET template carries the
13
13
  * entire `@molecule` fleet node_modules + postgres + warmed Vite deps, so a boot
14
14
  * only copies the ONE selected app's source in and starts the dev servers — no
15
- * per-boot `npm install`. The 133 flagship template sources are NOT baked into
16
- * the image; they are copied from the control plane at boot, so templates and
17
- * `mlcl` stay private.
15
+ * per-boot `npm install`. The app template sources are not baked into the
16
+ * image; they are copied in at boot.
18
17
  *
19
18
  * @example
20
19
  * ```typescript
@@ -39,13 +38,19 @@
39
38
  * ```
40
39
  *
41
40
  * @remarks
41
+ * - `importFiles` spools an incoming archive into the sandbox in 8 MB pieces
42
+ * whatever the size of the chunks it is given; `exportFiles` reads the whole
43
+ * archive into memory once, so bound what you export.
44
+ * - `importFiles` trusts the stream's length: an archive cut exactly between two
45
+ * tar members extracts cleanly (tar exits 0) and is reported complete. Give it
46
+ * a stream that errors on truncation (an authenticated encrypted stream does)
47
+ * or check a length or hash yourself.
42
48
  * **`verifyEgress` OBSERVES, it never attests.** It boots a throwaway sandbox,
43
49
  * applies `{ allowOut: [npm], denyOut: [ALL_TRAFFIC] }`, and curls an
44
50
  * allow-listed host, a non-allow-listed host AND a raw IP from inside it;
45
51
  * `filtered` requires the last two to be blocked while the first answers. Any
46
52
  * failure to run that probe is `inconclusive` — never `filtered`, because "I
47
- * could not look" must not reach a control plane as "I looked, and it is safe"
48
- * (Rule 18: never trade cost for security).
53
+ * could not look" must not reach a control plane as "I looked, and it is safe".
49
54
  *
50
55
  * **A probe that sees NO network is `inconclusive`, not `open`.** When the
51
56
  * allow-listed control host does not answer either, the probe sandbox itself has
@@ -126,8 +131,7 @@
126
131
  * must be booted fresh with it.
127
132
  *
128
133
  * **Volumes are a private beta on E2B.** An account without them answers
129
- * `403 use of volumes is not enabled` (measured on the production account,
130
- * 2026-08-16); ask E2B support to enable them. Every volume method throws in
134
+ * `403 use of volumes is not enabled`; ask E2B support to enable them. Every volume method throws in
131
135
  * that state rather than no-op-ing, because a control plane that believes it has
132
136
  * durable storage and does not is the failure this bond exists to prevent.
133
137
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6JG;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,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}
package/dist/index.js CHANGED
@@ -12,9 +12,8 @@
12
12
  * The design that makes it fast: a single golden SUPERSET template carries the
13
13
  * entire `@molecule` fleet node_modules + postgres + warmed Vite deps, so a boot
14
14
  * only copies the ONE selected app's source in and starts the dev servers — no
15
- * per-boot `npm install`. The 133 flagship template sources are NOT baked into
16
- * the image; they are copied from the control plane at boot, so templates and
17
- * `mlcl` stay private.
15
+ * per-boot `npm install`. The app template sources are not baked into the
16
+ * image; they are copied in at boot.
18
17
  *
19
18
  * @example
20
19
  * ```typescript
@@ -39,13 +38,19 @@
39
38
  * ```
40
39
  *
41
40
  * @remarks
41
+ * - `importFiles` spools an incoming archive into the sandbox in 8 MB pieces
42
+ * whatever the size of the chunks it is given; `exportFiles` reads the whole
43
+ * archive into memory once, so bound what you export.
44
+ * - `importFiles` trusts the stream's length: an archive cut exactly between two
45
+ * tar members extracts cleanly (tar exits 0) and is reported complete. Give it
46
+ * a stream that errors on truncation (an authenticated encrypted stream does)
47
+ * or check a length or hash yourself.
42
48
  * **`verifyEgress` OBSERVES, it never attests.** It boots a throwaway sandbox,
43
49
  * applies `{ allowOut: [npm], denyOut: [ALL_TRAFFIC] }`, and curls an
44
50
  * allow-listed host, a non-allow-listed host AND a raw IP from inside it;
45
51
  * `filtered` requires the last two to be blocked while the first answers. Any
46
52
  * failure to run that probe is `inconclusive` — never `filtered`, because "I
47
- * could not look" must not reach a control plane as "I looked, and it is safe"
48
- * (Rule 18: never trade cost for security).
53
+ * could not look" must not reach a control plane as "I looked, and it is safe".
49
54
  *
50
55
  * **A probe that sees NO network is `inconclusive`, not `open`.** When the
51
56
  * allow-listed control host does not answer either, the probe sandbox itself has
@@ -126,8 +131,7 @@
126
131
  * must be booted fresh with it.
127
132
  *
128
133
  * **Volumes are a private beta on E2B.** An account without them answers
129
- * `403 use of volumes is not enabled` (measured on the production account,
130
- * 2026-08-16); ask E2B support to enable them. Every volume method throws in
134
+ * `403 use of volumes is not enabled`; ask E2B support to enable them. Every volume method throws in
131
135
  * that state rather than no-op-ing, because a control plane that believes it has
132
136
  * durable storage and does not is the failure this bond exists to prevent.
133
137
  *
@@ -17,8 +17,7 @@ import type { E2BConfig, E2BSandboxClientLike } from './types.js';
17
17
  * optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
18
18
  * land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
19
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
21
- * (Rule 18 — never trade cost for security).
20
+ * in prod, which is the correct safe default until egress observation is proven.
22
21
  */
23
22
  export declare class E2BSandboxProvider implements SandboxProvider {
24
23
  readonly name = "e2b";
@@ -284,8 +283,7 @@ export interface EgressProbeCodes {
284
283
  * egress was OBSERVED;
285
284
  * - `inconclusive` whenever the allow-listed control host did not answer — a
286
285
  * probe sandbox with no network at all blocks everything, which says nothing
287
- * about the policy (2026-10-04: `host=000, rawIP=000, allowed=000` was read
288
- * as open and production refused to boot);
286
+ * about the policy;
289
287
  * - `filtered` only when the control host answered AND both denied probes were
290
288
  * blocked (`000` / empty).
291
289
  *
@@ -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
@@ -128,6 +128,8 @@ function bareSnapshotName(snapshot) {
128
128
  const withoutTag = full.includes(':') ? full.slice(0, full.lastIndexOf(':')) : full;
129
129
  return withoutTag.includes('/') ? withoutTag.slice(withoutTag.lastIndexOf('/') + 1) : withoutTag;
130
130
  }
131
+ /** How much of an incoming archive `importFiles` holds before spooling it into the sandbox. */
132
+ const IMPORT_PIECE_BYTES = 8 * 1024 * 1024;
131
133
  /**
132
134
  * Single-quote a value for POSIX `sh` so an arbitrary path is one argument.
133
135
  *
@@ -644,7 +646,8 @@ class E2BSandbox {
644
646
  * Extract a POSIX tar stream into the sandbox at `path`.
645
647
  *
646
648
  * The transfer primitive the scaffold path uses to copy a project tree in.
647
- * Buffers the stream, writes it as one blob, and `tar x`-tracts it.
649
+ * Spools the stream into the sandbox in 8 MB pieces, so the archive is never
650
+ * held whole in memory, then extracts it with `tar x`.
648
651
  * `--no-same-owner --no-same-permissions` enforces the contract that a
649
652
  * caller/tenant-authored archive's ownership + setuid/setgid bits are NOT
650
653
  * restored (they arrive owned by the sandbox user, no privilege bits).
@@ -653,14 +656,55 @@ class E2BSandbox {
653
656
  * @param archive - POSIX tar byte stream to extract there.
654
657
  */
655
658
  async importFiles(path, archive) {
656
- const chunks = [];
657
- for await (const chunk of archive)
658
- chunks.push(chunk);
659
- const tarPath = `/tmp/mol-import-${Date.now().toString(36)}.tar`;
660
- await this.sbx.files.write(tarPath, new Blob([Buffer.concat(chunks)]));
661
- const r = await this.sbx.commands.run(`mkdir -p ${shellQuote(path)} && tar xf ${tarPath} -C ${shellQuote(path)} --no-same-owner --no-same-permissions && rm -f ${tarPath}`, { timeoutMs: 300_000 });
662
- if (r.exitCode !== 0) {
663
- throw new Error(`importFiles: tar extract failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
659
+ // Spooled into the sandbox in bounded pieces: the archive is never held
660
+ // whole in this process (an uploaded tarball can inflate to gigabytes, and
661
+ // this process is shared by every tenant).
662
+ const tag = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
663
+ const tarPath = `/tmp/mol-import-${tag}.tar`;
664
+ const piecePath = `${tarPath}.piece`;
665
+ let pending = [];
666
+ let pendingBytes = 0;
667
+ let pieces = 0;
668
+ const flush = async () => {
669
+ if (pendingBytes === 0)
670
+ return;
671
+ const piece = Buffer.concat(pending);
672
+ pending = [];
673
+ pendingBytes = 0;
674
+ 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 });
676
+ if (r.exitCode !== 0) {
677
+ throw new Error(`importFiles: spooling the archive failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
678
+ }
679
+ pieces++;
680
+ };
681
+ try {
682
+ for await (const chunk of archive) {
683
+ // Sliced into the piece: a single chunk larger than a piece (a whole
684
+ // archive yielded at once) must not become one huge write (R93-B9).
685
+ let offset = 0;
686
+ while (offset < chunk.length) {
687
+ const room = IMPORT_PIECE_BYTES - pendingBytes;
688
+ const slice = chunk.subarray(offset, offset + room);
689
+ pending.push(slice);
690
+ pendingBytes += slice.length;
691
+ offset += slice.length;
692
+ if (pendingBytes >= IMPORT_PIECE_BYTES)
693
+ await flush();
694
+ }
695
+ }
696
+ 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 });
700
+ if (r.exitCode !== 0) {
701
+ throw new Error(`importFiles: tar extract failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
702
+ }
703
+ }
704
+ finally {
705
+ await this.sbx.commands
706
+ .run(`rm -f ${tarPath} ${piecePath}`, { timeoutMs: 30_000 })
707
+ .catch(() => undefined);
664
708
  }
665
709
  }
666
710
  /**
@@ -675,9 +719,6 @@ class E2BSandbox {
675
719
  * <name>`), so `exportFiles('/workspace/my-app')` yields `my-app/…` — the
676
720
  * shape Docker's archive endpoint produces and the shape every consumer
677
721
  * (`importFiles(<parent>)`, host-side unpackers with `strip: 1`) expects.
678
- * The previous `tar -C <path> .` rooting yielded `./…`, which
679
- * `importFiles('/')` extracted at the filesystem root instead of the
680
- * directory it was taken from.
681
722
  *
682
723
  * @param path - Absolute path inside the sandbox to archive (not `/`).
683
724
  * @returns A POSIX tar byte stream of that path's contents.
@@ -690,15 +731,22 @@ class E2BSandbox {
690
731
  if (!trimmed.startsWith('/') || !name) {
691
732
  throw new Error(`exportFiles: path must be an absolute directory below "/" (got "${path}")`);
692
733
  }
693
- const tarPath = `/tmp/mol-export-${Date.now().toString(36)}.tar`;
694
- const r = await this.sbx.commands.run(`tar cf ${tarPath} -C ${shellQuote(parent)} ${shellQuote(name)}`, { timeoutMs: 300_000 });
695
- if (r.exitCode !== 0) {
696
- throw new Error(`exportFiles: tar create failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
734
+ // A unique name (two exports in one millisecond collided on Date.now()
735
+ // alone), removed whether the tar or the read succeeds or not (R93-B8).
736
+ const tarPath = `/tmp/mol-export-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.tar`;
737
+ let bytes;
738
+ try {
739
+ const r = await this.sbx.commands.run(`tar cf ${tarPath} -C ${shellQuote(parent)} ${shellQuote(name)}`, { timeoutMs: 300_000 });
740
+ if (r.exitCode !== 0) {
741
+ throw new Error(`exportFiles: tar create failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
742
+ }
743
+ bytes = await this.sbx.files.read(tarPath, { format: 'bytes' });
744
+ }
745
+ finally {
746
+ await this.sbx.commands.run(`rm -f ${tarPath}`, { timeoutMs: 20_000 }).catch((_error) => {
747
+ // intentional noop — leftover /tmp tar is harmless; the sandbox is ephemeral.
748
+ });
697
749
  }
698
- const bytes = await this.sbx.files.read(tarPath, { format: 'bytes' });
699
- await this.sbx.commands.run(`rm -f ${tarPath}`, { timeoutMs: 20_000 }).catch((_error) => {
700
- // intentional noop — leftover /tmp tar is harmless; the sandbox is ephemeral.
701
- });
702
750
  return (async function* () {
703
751
  yield bytes;
704
752
  })();
@@ -730,8 +778,7 @@ class E2BSandbox {
730
778
  * optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
731
779
  * land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
732
780
  * the control plane treats "unsupported" as `inconclusive` and refuses to boot
733
- * in prod, which is the correct safe default until egress observation is proven
734
- * (Rule 18 — never trade cost for security).
781
+ * in prod, which is the correct safe default until egress observation is proven.
735
782
  */
736
783
  export class E2BSandboxProvider {
737
784
  name = 'e2b';
@@ -1309,8 +1356,7 @@ export class E2BSandboxProvider {
1309
1356
  * egress was OBSERVED;
1310
1357
  * - `inconclusive` whenever the allow-listed control host did not answer — a
1311
1358
  * probe sandbox with no network at all blocks everything, which says nothing
1312
- * about the policy (2026-10-04: `host=000, rawIP=000, allowed=000` was read
1313
- * as open and production refused to boot);
1359
+ * about the policy;
1314
1360
  * - `filtered` only when the control host answered AND both denied probes were
1315
1361
  * blocked (`000` / empty).
1316
1362
  *
package/dist/types.d.ts CHANGED
@@ -42,7 +42,7 @@ export interface E2BConfig {
42
42
  * time; everything else is denied (`denyOut: [ALL_TRAFFIC]`). Wildcards like
43
43
  * `*.npmjs.org` are supported. Empty/omitted means the bond does NOT
44
44
  * constrain egress — prod must supply this or `verifyEgress` observes `open`
45
- * and the control plane refuses to boot (Rule 18).
45
+ * and the control plane refuses to boot.
46
46
  *
47
47
  * Verified against a live E2B sandbox: with `denyOut: [ALL_TRAFFIC]`, a
48
48
  * non-allowlisted host AND a raw destination IP are both blocked — a stronger
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@molecule/api-code-sandbox-e2b",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
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",