@molecule/api-code-sandbox-e2b 1.2.6 → 1.2.8
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 +20 -18
- package/dist/index.d.ts +11 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -7
- package/dist/provider.d.ts +2 -4
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +77 -23
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
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-
|
|
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
|
|
29
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
`
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
|
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
|
|
16
|
-
*
|
|
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
|
|
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
|
*
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
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
|
|
16
|
-
*
|
|
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
|
|
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
|
*
|
package/dist/provider.d.ts
CHANGED
|
@@ -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
|
|
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
|
*
|
package/dist/provider.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"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"}
|
package/dist/provider.js
CHANGED
|
@@ -26,6 +26,23 @@ 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;
|
|
29
46
|
/** Default lifetime for a PTY session; same reasoning as {@link DEFAULT_SPAWN_TIMEOUT_MS}. */
|
|
30
47
|
const DEFAULT_PTY_TIMEOUT_MS = 60 * 60 * 1000;
|
|
31
48
|
/**
|
|
@@ -309,6 +326,14 @@ class E2BSandbox {
|
|
|
309
326
|
* running and the caller's own timeout is the right bound. Never a guess from
|
|
310
327
|
* the shape of the command.
|
|
311
328
|
*
|
|
329
|
+
* But "over" is not "succeeded": the SDK delivers the exit code on the
|
|
330
|
+
* stream's `end` event, and the descendant holding the stream keeps that
|
|
331
|
+
* event from ever arriving. A gone pid therefore gets a bounded grace window
|
|
332
|
+
* for the handle to report the real code, and a handle that never does is
|
|
333
|
+
* reported as {@link EXIT_CODE_UNKNOWN} — failure — because an outcome that
|
|
334
|
+
* cannot be observed (a build OOM-killed mid-run) must never be fabricated
|
|
335
|
+
* into success.
|
|
336
|
+
*
|
|
312
337
|
* @param handle - The started command.
|
|
313
338
|
* @returns The command's result.
|
|
314
339
|
*/
|
|
@@ -328,11 +353,29 @@ class E2BSandbox {
|
|
|
328
353
|
const alive = await this.isProcessAlive(handle.pid);
|
|
329
354
|
if (alive)
|
|
330
355
|
return finished;
|
|
331
|
-
// The process is gone; whatever still holds the stream is not it.
|
|
356
|
+
// The process is gone; whatever still holds the stream is not it. The real
|
|
357
|
+
// exit code rides the stream's `end` event, so give the handle a bounded
|
|
358
|
+
// window to deliver it — when the process truly finished, envd closes the
|
|
359
|
+
// 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
|
+
]);
|
|
364
|
+
if (reported)
|
|
365
|
+
return reported;
|
|
366
|
+
if (typeof handle.exitCode === 'number') {
|
|
367
|
+
return { stdout: handle.stdout ?? '', stderr: handle.stderr ?? '', exitCode: handle.exitCode };
|
|
368
|
+
}
|
|
369
|
+
// Still nothing: the exit code is UNKNOWN. Reporting `0` here used to
|
|
370
|
+
// fabricate success for exactly this shape — a killed build whose stranded
|
|
371
|
+
// daemon held the stream shipped its partial `dist/` as a finished site —
|
|
372
|
+
// so fail closed instead, with a line that says why the code is missing.
|
|
373
|
+
const unknownNote = `e2b: exit code unknown — the started process (pid ${handle.pid}) is gone but its ` +
|
|
374
|
+
'output stream is still held, so its real outcome cannot be observed; reporting failure';
|
|
332
375
|
return {
|
|
333
376
|
stdout: handle.stdout ?? '',
|
|
334
|
-
stderr: handle.stderr
|
|
335
|
-
exitCode:
|
|
377
|
+
stderr: handle.stderr ? `${handle.stderr}\n${unknownNote}` : unknownNote,
|
|
378
|
+
exitCode: EXIT_CODE_UNKNOWN,
|
|
336
379
|
};
|
|
337
380
|
}
|
|
338
381
|
/**
|
|
@@ -646,7 +689,8 @@ class E2BSandbox {
|
|
|
646
689
|
* Extract a POSIX tar stream into the sandbox at `path`.
|
|
647
690
|
*
|
|
648
691
|
* The transfer primitive the scaffold path uses to copy a project tree in.
|
|
649
|
-
*
|
|
692
|
+
* Spools the stream into the sandbox in 8 MB pieces, so the archive is never
|
|
693
|
+
* held whole in memory, then extracts it with `tar x`.
|
|
650
694
|
* `--no-same-owner --no-same-permissions` enforces the contract that a
|
|
651
695
|
* caller/tenant-authored archive's ownership + setuid/setgid bits are NOT
|
|
652
696
|
* restored (they arrive owned by the sandbox user, no privilege bits).
|
|
@@ -679,10 +723,18 @@ class E2BSandbox {
|
|
|
679
723
|
};
|
|
680
724
|
try {
|
|
681
725
|
for await (const chunk of archive) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
726
|
+
// Sliced into the piece: a single chunk larger than a piece (a whole
|
|
727
|
+
// archive yielded at once) must not become one huge write (R93-B9).
|
|
728
|
+
let offset = 0;
|
|
729
|
+
while (offset < chunk.length) {
|
|
730
|
+
const room = IMPORT_PIECE_BYTES - pendingBytes;
|
|
731
|
+
const slice = chunk.subarray(offset, offset + room);
|
|
732
|
+
pending.push(slice);
|
|
733
|
+
pendingBytes += slice.length;
|
|
734
|
+
offset += slice.length;
|
|
735
|
+
if (pendingBytes >= IMPORT_PIECE_BYTES)
|
|
736
|
+
await flush();
|
|
737
|
+
}
|
|
686
738
|
}
|
|
687
739
|
await flush();
|
|
688
740
|
if (pieces === 0)
|
|
@@ -710,9 +762,6 @@ class E2BSandbox {
|
|
|
710
762
|
* <name>`), so `exportFiles('/workspace/my-app')` yields `my-app/…` — the
|
|
711
763
|
* shape Docker's archive endpoint produces and the shape every consumer
|
|
712
764
|
* (`importFiles(<parent>)`, host-side unpackers with `strip: 1`) expects.
|
|
713
|
-
* The previous `tar -C <path> .` rooting yielded `./…`, which
|
|
714
|
-
* `importFiles('/')` extracted at the filesystem root instead of the
|
|
715
|
-
* directory it was taken from.
|
|
716
765
|
*
|
|
717
766
|
* @param path - Absolute path inside the sandbox to archive (not `/`).
|
|
718
767
|
* @returns A POSIX tar byte stream of that path's contents.
|
|
@@ -725,15 +774,22 @@ class E2BSandbox {
|
|
|
725
774
|
if (!trimmed.startsWith('/') || !name) {
|
|
726
775
|
throw new Error(`exportFiles: path must be an absolute directory below "/" (got "${path}")`);
|
|
727
776
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
777
|
+
// A unique name (two exports in one millisecond collided on Date.now()
|
|
778
|
+
// alone), removed whether the tar or the read succeeds or not (R93-B8).
|
|
779
|
+
const tarPath = `/tmp/mol-export-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}.tar`;
|
|
780
|
+
let bytes;
|
|
781
|
+
try {
|
|
782
|
+
const r = await this.sbx.commands.run(`tar cf ${tarPath} -C ${shellQuote(parent)} ${shellQuote(name)}`, { timeoutMs: 300_000 });
|
|
783
|
+
if (r.exitCode !== 0) {
|
|
784
|
+
throw new Error(`exportFiles: tar create failed (${r.exitCode}): ${r.stderr.slice(0, 300)}`);
|
|
785
|
+
}
|
|
786
|
+
bytes = await this.sbx.files.read(tarPath, { format: 'bytes' });
|
|
787
|
+
}
|
|
788
|
+
finally {
|
|
789
|
+
await this.sbx.commands.run(`rm -f ${tarPath}`, { timeoutMs: 20_000 }).catch((_error) => {
|
|
790
|
+
// intentional noop — leftover /tmp tar is harmless; the sandbox is ephemeral.
|
|
791
|
+
});
|
|
732
792
|
}
|
|
733
|
-
const bytes = await this.sbx.files.read(tarPath, { format: 'bytes' });
|
|
734
|
-
await this.sbx.commands.run(`rm -f ${tarPath}`, { timeoutMs: 20_000 }).catch((_error) => {
|
|
735
|
-
// intentional noop — leftover /tmp tar is harmless; the sandbox is ephemeral.
|
|
736
|
-
});
|
|
737
793
|
return (async function* () {
|
|
738
794
|
yield bytes;
|
|
739
795
|
})();
|
|
@@ -765,8 +821,7 @@ class E2BSandbox {
|
|
|
765
821
|
* optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
|
|
766
822
|
* land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
|
|
767
823
|
* the control plane treats "unsupported" as `inconclusive` and refuses to boot
|
|
768
|
-
* in prod, which is the correct safe default until egress observation is proven
|
|
769
|
-
* (Rule 18 — never trade cost for security).
|
|
824
|
+
* in prod, which is the correct safe default until egress observation is proven.
|
|
770
825
|
*/
|
|
771
826
|
export class E2BSandboxProvider {
|
|
772
827
|
name = 'e2b';
|
|
@@ -1344,8 +1399,7 @@ export class E2BSandboxProvider {
|
|
|
1344
1399
|
* egress was OBSERVED;
|
|
1345
1400
|
* - `inconclusive` whenever the allow-listed control host did not answer — a
|
|
1346
1401
|
* probe sandbox with no network at all blocks everything, which says nothing
|
|
1347
|
-
* about the policy
|
|
1348
|
-
* as open and production refused to boot);
|
|
1402
|
+
* about the policy;
|
|
1349
1403
|
* - `filtered` only when the control host answered AND both denied probes were
|
|
1350
1404
|
* blocked (`000` / empty).
|
|
1351
1405
|
*
|
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
|
|
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