@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 +9 -6
- package/dist/browser-guard.d.ts +2 -0
- package/dist/browser-guard.d.ts.map +1 -0
- package/dist/browser-guard.js +18 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/provider.d.ts +8 -5
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +198 -25
- 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-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
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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 @@
|
|
|
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
package/dist/index.d.ts.map
CHANGED
|
@@ -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
package/dist/provider.d.ts
CHANGED
|
@@ -13,11 +13,14 @@ import type { E2BConfig, E2BSandboxClientLike } from './types.js';
|
|
|
13
13
|
/**
|
|
14
14
|
* E2B implementation of {@link SandboxProvider}.
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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";
|
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":"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
|
|
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:
|
|
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.
|
|
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
|
-
|
|
699
|
-
|
|
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.
|
|
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
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
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
|
-
|
|
993
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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