@molecule/api-code-sandbox-e2b 1.0.4 → 1.2.0
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 +302 -27
- package/dist/index.d.ts +98 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -11
- package/dist/provider.d.ts +161 -2
- package/dist/provider.d.ts.map +1 -1
- package/dist/provider.js +705 -33
- package/dist/types.d.ts +183 -13
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
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-08-
|
|
6
|
+
Generated: 2026-08-16T01:25:40.978Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-code-sandbox-e2b
|
|
@@ -45,12 +45,9 @@ import { createProvider } from '@molecule/api-code-sandbox-e2b'
|
|
|
45
45
|
const provider = createProvider({
|
|
46
46
|
templateId: 'molecule-superset',
|
|
47
47
|
defaultPreviewPort: 5173,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{ domain: 'github.com', action: 'allow' },
|
|
52
|
-
{ domain: '*', action: 'deny' },
|
|
53
|
-
],
|
|
48
|
+
// Deny-by-default egress: everything not listed here is blocked, raw IPs
|
|
49
|
+
// included. An empty/omitted list applies NO policy at all.
|
|
50
|
+
defaultAllowOut: ['registry.npmjs.org', '*.npmjs.org', 'github.com'],
|
|
54
51
|
})
|
|
55
52
|
```
|
|
56
53
|
|
|
@@ -61,13 +58,47 @@ const provider = createProvider({
|
|
|
61
58
|
## Installation
|
|
62
59
|
|
|
63
60
|
```bash
|
|
64
|
-
npm install @molecule/api-code-sandbox-e2b
|
|
61
|
+
npm install @molecule/api-code-sandbox-e2b @bufbuild/protobuf @molecule/api-bond @molecule/api-code-sandbox @molecule/api-i18n e2b
|
|
65
62
|
```
|
|
66
63
|
|
|
67
64
|
## API
|
|
68
65
|
|
|
69
66
|
### Interfaces
|
|
70
67
|
|
|
68
|
+
#### `E2BCommandHandleLike`
|
|
69
|
+
|
|
70
|
+
Handle to a command started with `background: true` (the SDK's `CommandHandle`).
|
|
71
|
+
|
|
72
|
+
The handle is what makes a backgrounded start HONEST: it carries the pid, and
|
|
73
|
+
`wait()` resolves with the real exit code once the started process exits —
|
|
74
|
+
even when it left a detached child behind. A launcher therefore learns whether
|
|
75
|
+
its command was accepted instead of being handed a fabricated success.
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
interface E2BCommandHandleLike {
|
|
79
|
+
/** The started process's pid inside the sandbox. */
|
|
80
|
+
pid: number
|
|
81
|
+
/** Output accumulated so far — readable before {@link E2BCommandHandleLike.wait} settles. */
|
|
82
|
+
stdout?: string
|
|
83
|
+
/** Error output accumulated so far. */
|
|
84
|
+
stderr?: string
|
|
85
|
+
/** Set once the process's exit is known; absent while it is still streaming. */
|
|
86
|
+
exitCode?: number
|
|
87
|
+
/**
|
|
88
|
+
* Resolve when the started process exits. Rejects with the SDK's
|
|
89
|
+
* `CommandExitError` (carrying `.result`) on a non-zero exit, and with a
|
|
90
|
+
* timeout error when `timeoutMs` elapses first.
|
|
91
|
+
*/
|
|
92
|
+
wait(): Promise<E2BCommandResultLike>
|
|
93
|
+
/** Write to the process's stdin (requires `stdin: true` at start). */
|
|
94
|
+
sendStdin(data: string | Uint8Array): Promise<void>
|
|
95
|
+
/** Kill the process. */
|
|
96
|
+
kill(): Promise<boolean>
|
|
97
|
+
/** Stop streaming without killing the process. */
|
|
98
|
+
disconnect?(): Promise<void>
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
71
102
|
#### `E2BCommandResultLike`
|
|
72
103
|
|
|
73
104
|
Result of an E2B command run (subset of the SDK's `CommandResult`).
|
|
@@ -80,22 +111,36 @@ interface E2BCommandResultLike {
|
|
|
80
111
|
}
|
|
81
112
|
```
|
|
82
113
|
|
|
114
|
+
#### `E2BCommandRunOpts`
|
|
115
|
+
|
|
116
|
+
Options accepted by the SDK's `commands.run`.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
interface E2BCommandRunOpts {
|
|
120
|
+
cwd?: string
|
|
121
|
+
timeoutMs?: number
|
|
122
|
+
envs?: Record<string, string>
|
|
123
|
+
/** Keep stdin open so {@link E2BCommandHandleLike.sendStdin} works. */
|
|
124
|
+
stdin?: boolean
|
|
125
|
+
onStdout?: (data: string) => void
|
|
126
|
+
onStderr?: (data: string) => void
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
83
130
|
#### `E2BCommandsLike`
|
|
84
131
|
|
|
85
132
|
Subset of the SDK's `Commands` the bond uses.
|
|
86
133
|
|
|
87
134
|
```typescript
|
|
88
135
|
interface E2BCommandsLike {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
},
|
|
98
|
-
): Promise<E2BCommandResultLike>
|
|
136
|
+
/**
|
|
137
|
+
* Start a command and return a handle immediately. The bond always uses this
|
|
138
|
+
* form: waiting inline blocks until the whole process GROUP ends, which never
|
|
139
|
+
* happens for a launch that leaves a dev server running.
|
|
140
|
+
*/
|
|
141
|
+
run(cmd: string, opts: E2BCommandRunOpts & { background: true }): Promise<E2BCommandHandleLike>
|
|
142
|
+
/** Run a command to completion (the SDK's default). */
|
|
143
|
+
run(cmd: string, opts?: E2BCommandRunOpts & { background?: false }): Promise<E2BCommandResultLike>
|
|
99
144
|
}
|
|
100
145
|
```
|
|
101
146
|
|
|
@@ -125,6 +170,10 @@ interface E2BConfig {
|
|
|
125
170
|
/**
|
|
126
171
|
* Default sandbox lifetime before E2B auto-pauses it, in milliseconds. The
|
|
127
172
|
* control plane extends this per-heartbeat; this is only the initial ceiling.
|
|
173
|
+
*
|
|
174
|
+
* E2B caps this per account: 1 hour on Hobby, 24 hours on Pro. A value above
|
|
175
|
+
* the account's cap is rejected at create time, so raising it is a decision
|
|
176
|
+
* about the account, not just the config.
|
|
128
177
|
*/
|
|
129
178
|
defaultTimeoutMs?: number
|
|
130
179
|
/**
|
|
@@ -158,6 +207,31 @@ interface E2BFilesystemLike {
|
|
|
158
207
|
}
|
|
159
208
|
```
|
|
160
209
|
|
|
210
|
+
#### `E2BPtyLike`
|
|
211
|
+
|
|
212
|
+
Subset of the SDK's `Pty` module the bond uses.
|
|
213
|
+
|
|
214
|
+
A PTY is a different mechanism from a command, not a flag on one: it has its
|
|
215
|
+
own create/input/resize/kill endpoints, and only it gives the sandbox side a
|
|
216
|
+
controlling terminal (job control, so Ctrl-C becomes SIGINT; a negotiated
|
|
217
|
+
width, so tools format to the real panel size).
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
interface E2BPtyLike {
|
|
221
|
+
create(opts: {
|
|
222
|
+
cols: number
|
|
223
|
+
rows: number
|
|
224
|
+
onData: (data: Uint8Array) => void
|
|
225
|
+
cwd?: string
|
|
226
|
+
envs?: Record<string, string>
|
|
227
|
+
timeoutMs?: number
|
|
228
|
+
}): Promise<E2BCommandHandleLike>
|
|
229
|
+
sendInput(pid: number, data: Uint8Array): Promise<void>
|
|
230
|
+
resize(pid: number, size: { cols: number; rows: number }): Promise<void>
|
|
231
|
+
kill(pid: number): Promise<boolean>
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
161
235
|
#### `E2BSandboxClientLike`
|
|
162
236
|
|
|
163
237
|
Subset of the SDK's `Sandbox` static surface the bond uses.
|
|
@@ -168,8 +242,64 @@ interface E2BSandboxClientLike {
|
|
|
168
242
|
connect(sandboxId: string, opts?: Record<string, unknown>): Promise<E2BSandboxLike>
|
|
169
243
|
list(
|
|
170
244
|
opts?: Record<string, unknown>,
|
|
171
|
-
): Promise<
|
|
245
|
+
): Promise<E2BSandboxListItem[] | { sandboxes?: E2BSandboxListItem[] }>
|
|
172
246
|
kill?(sandboxId: string, opts?: Record<string, unknown>): Promise<boolean>
|
|
247
|
+
/**
|
|
248
|
+
* Create a team volume. Optional: the volume API is a private beta on E2B, so a
|
|
249
|
+
* client built against an account without it does not expose these at all.
|
|
250
|
+
*/
|
|
251
|
+
createVolume?(name: string): Promise<E2BVolumeLike>
|
|
252
|
+
/** Enumerate team volumes. */
|
|
253
|
+
listVolumes?(): Promise<E2BVolumeLike[]>
|
|
254
|
+
/** Destroy a volume by its provider-native id. */
|
|
255
|
+
destroyVolume?(volumeId: string): Promise<boolean>
|
|
256
|
+
/**
|
|
257
|
+
* Capture a sandbox's filesystem + memory as a named snapshot. Re-using a name
|
|
258
|
+
* assigns a new build to the SAME snapshot rather than creating a second one,
|
|
259
|
+
* which is what makes a per-project restore point a fixed-size resource.
|
|
260
|
+
*/
|
|
261
|
+
createSnapshot?(sandboxId: string, name: string): Promise<E2BSnapshotLike>
|
|
262
|
+
/** Enumerate snapshots, optionally filtered to one exact name. */
|
|
263
|
+
listSnapshots?(opts?: { name?: string; limit?: number }): Promise<E2BSnapshotLike[]>
|
|
264
|
+
/** Delete a snapshot. Resolves `false` when there was nothing to delete. */
|
|
265
|
+
deleteSnapshot?(snapshotId: string): Promise<boolean>
|
|
266
|
+
/**
|
|
267
|
+
* Read a sandbox's record WITHOUT connecting to it — the only lookup that does
|
|
268
|
+
* not resume a paused sandbox. Throws `SandboxNotFoundError` on a 404.
|
|
269
|
+
*/
|
|
270
|
+
getInfo?(sandboxId: string, opts?: Record<string, unknown>): Promise<E2BSandboxInfoLike>
|
|
271
|
+
/**
|
|
272
|
+
* Whether an error means "this sandbox does not exist", as opposed to "the
|
|
273
|
+
* lookup failed". The distinction cannot be recovered from the error's shape by
|
|
274
|
+
* a consumer, and getting it wrong is what makes a control plane treat a
|
|
275
|
+
* provider outage as a destroyed sandbox.
|
|
276
|
+
*/
|
|
277
|
+
isNotFound?(error: unknown): boolean
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
#### `E2BSandboxInfoLike`
|
|
282
|
+
|
|
283
|
+
Subset of the SDK's `SandboxInfo` the bond reads.
|
|
284
|
+
|
|
285
|
+
This is what a sandbox looks like when you only LOOK at it. `connect` — which
|
|
286
|
+
is how a handle is obtained — RESUMES a paused sandbox and extends its
|
|
287
|
+
deadline, so it can never be used to answer "is this thing asleep?".
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
interface E2BSandboxInfoLike {
|
|
291
|
+
sandboxId: string
|
|
292
|
+
templateId?: string
|
|
293
|
+
/** `'running'` or `'paused'`; anything else is treated as running. */
|
|
294
|
+
state?: string
|
|
295
|
+
/** Caller metadata supplied at create (the bond puts `projectId` here). */
|
|
296
|
+
metadata?: Record<string, string>
|
|
297
|
+
/** When the sandbox last started running. */
|
|
298
|
+
startedAt?: Date | string
|
|
299
|
+
/** When the sandbox's current deadline expires. */
|
|
300
|
+
endAt?: Date | string
|
|
301
|
+
/** Volumes mounted into the sandbox, when the account uses them. */
|
|
302
|
+
volumeMounts?: Array<{ name: string; path: string }>
|
|
173
303
|
}
|
|
174
304
|
```
|
|
175
305
|
|
|
@@ -181,17 +311,71 @@ Subset of the SDK's `Sandbox` instance the bond uses.
|
|
|
181
311
|
interface E2BSandboxLike {
|
|
182
312
|
sandboxId: string
|
|
183
313
|
commands: E2BCommandsLike
|
|
314
|
+
/** Present on SDK builds that support pseudo-terminals; absent means no PTY. */
|
|
315
|
+
pty?: E2BPtyLike
|
|
184
316
|
files: E2BFilesystemLike
|
|
185
317
|
getHost(port: number): string
|
|
186
318
|
setTimeout(ms: number): Promise<void>
|
|
187
319
|
kill(): Promise<void>
|
|
188
|
-
|
|
189
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Suspend the sandbox (FS + memory snapshot). Resolves `false` when the API
|
|
322
|
+
* answered 409 because it was ALREADY paused — which is a success, not a
|
|
323
|
+
* failure, and the reason this is not typed as `void`.
|
|
324
|
+
*/
|
|
325
|
+
pause?(): Promise<boolean>
|
|
326
|
+
/** Deprecated alias of {@link E2BSandboxLike.pause}; same endpoint. */
|
|
327
|
+
betaPause?(): Promise<boolean>
|
|
190
328
|
isRunning(): Promise<boolean>
|
|
191
329
|
updateNetwork?(opts: { allowOut?: string[]; denyOut?: string[] }): Promise<void>
|
|
192
330
|
}
|
|
193
331
|
```
|
|
194
332
|
|
|
333
|
+
#### `E2BSandboxListItem`
|
|
334
|
+
|
|
335
|
+
One row of a sandbox listing.
|
|
336
|
+
|
|
337
|
+
`name` is the template the sandbox booted from (the SDK's `alias`) and
|
|
338
|
+
`volumeMounts` is what it has attached. Both are here for the same reason: they
|
|
339
|
+
are the only way to observe whether a volume or a snapshot is still IN USE, and
|
|
340
|
+
deleting one that is destroys a running sandbox.
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
interface E2BSandboxListItem {
|
|
344
|
+
sandboxId: string
|
|
345
|
+
state?: string
|
|
346
|
+
/** Template/snapshot name the sandbox booted from, when the API reports one. */
|
|
347
|
+
name?: string
|
|
348
|
+
/** Volumes attached to this sandbox. */
|
|
349
|
+
volumeMounts?: Array<{ name: string; path: string }>
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
#### `E2BSnapshotLike`
|
|
354
|
+
|
|
355
|
+
A snapshot as E2B reports it.
|
|
356
|
+
|
|
357
|
+
`snapshotId` is the namespaced, tag-qualified reference
|
|
358
|
+
(`<team-slug>/<name>:<tag>`) — opaque, and the thing `Sandbox.create()` boots
|
|
359
|
+
from. The bond's OWN identifier is the bare name the caller supplied.
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
interface E2BSnapshotLike {
|
|
363
|
+
snapshotId: string
|
|
364
|
+
names?: string[]
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### `E2BVolumeLike`
|
|
369
|
+
|
|
370
|
+
A team volume as E2B reports it.
|
|
371
|
+
|
|
372
|
+
```typescript
|
|
373
|
+
interface E2BVolumeLike {
|
|
374
|
+
name: string
|
|
375
|
+
volumeId: string
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
195
379
|
### Classes
|
|
196
380
|
|
|
197
381
|
#### `E2BSandboxProvider`
|
|
@@ -272,19 +456,110 @@ Peer dependencies:
|
|
|
272
456
|
|
|
273
457
|
### Runtime Dependencies
|
|
274
458
|
|
|
275
|
-
- `
|
|
459
|
+
- `@bufbuild/protobuf`
|
|
276
460
|
- `@molecule/api-bond`
|
|
277
461
|
- `@molecule/api-code-sandbox`
|
|
278
462
|
- `@molecule/api-i18n`
|
|
463
|
+
- `e2b`
|
|
279
464
|
|
|
280
|
-
**`verifyEgress`
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
465
|
+
**`verifyEgress` OBSERVES, it never attests.** It boots a throwaway sandbox,
|
|
466
|
+
applies `{ allowOut: [npm], denyOut: [ALL_TRAFFIC] }`, and curls an
|
|
467
|
+
allow-listed host, a non-allow-listed host AND a raw IP from inside it;
|
|
468
|
+
`filtered` requires the last two to be blocked while the first answers. Any
|
|
469
|
+
failure to run that probe is `inconclusive` — never `filtered`, because "I
|
|
470
|
+
could not look" must not reach a control plane as "I looked, and it is safe"
|
|
471
|
+
(Rule 18: never trade cost for security).
|
|
285
472
|
|
|
286
473
|
**E2B pauses, it does not stop.** `sleep()`/`stop()` both map to E2B pause
|
|
287
474
|
(FS + memory snapshot); `wake()`/`start()` reconnect by id. `hibernate()`/
|
|
288
475
|
`resume()` report `processesPreserved: true` because the memory snapshot
|
|
289
476
|
restores the process tree — unlike a Docker stop, a resumed E2B sandbox's
|
|
290
477
|
dev servers are still running.
|
|
478
|
+
|
|
479
|
+
**`get()` RESUMES a paused sandbox — use `describe()` to look at one.**
|
|
480
|
+
Obtaining a handle is `POST /sandboxes/{id}/connect`, which resumes a paused
|
|
481
|
+
sandbox and extends its deadline. That is right for a caller about to USE the
|
|
482
|
+
sandbox and wrong for every status check: polling `get()` every few seconds
|
|
483
|
+
silently un-hibernates every sleeping project and bills for the compute while
|
|
484
|
+
the UI still says "asleep". `describe(id)` reads the record instead, reports
|
|
485
|
+
`sleeping` for a paused sandbox, and changes nothing.
|
|
486
|
+
|
|
487
|
+
**`get()` returns `null` ONLY for a sandbox that does not exist.** Every other
|
|
488
|
+
failure throws. A control plane reads `null` as "gone", detaches the project
|
|
489
|
+
and rebuilds it from a template — and since an E2B microVM is the only copy of
|
|
490
|
+
a project's files, answering a transient 5xx with `null` destroys the user's
|
|
491
|
+
code. "I could not look" must never be delivered as "I looked, and it is not
|
|
492
|
+
there". The same rule governs `describe()`.
|
|
493
|
+
|
|
494
|
+
**`hibernate()`/`stop()`/`sleep()` pause or THROW.** They never resolve a
|
|
495
|
+
success-shaped outcome for a sandbox that is still running: a caller's next act
|
|
496
|
+
is to record the sandbox as stopped, and a control plane that believes a running
|
|
497
|
+
sandbox is asleep bills for it and — with the kill timeout — watches it die at
|
|
498
|
+
its deadline instead of hibernating. Note the converse trap: a pause is undone by
|
|
499
|
+
the very next `get()`, since obtaining a handle connects. Anything that polls a
|
|
500
|
+
stopped sandbox (status, logs, files) must go through `describe()`.
|
|
501
|
+
|
|
502
|
+
**`exec()` starts the command and waits on its HANDLE, never inline.** E2B's
|
|
503
|
+
inline `commands.run` waits for the whole process GROUP, so any command that
|
|
504
|
+
leaves a detached child behind — `nohup … >log 2>&1 &`, i.e. every dev-server
|
|
505
|
+
launch — blocks until the request deadline and then throws while the child is
|
|
506
|
+
running perfectly. Starting in the background and awaiting the handle returns
|
|
507
|
+
the STARTED process's real exit code in milliseconds, so a launcher learns
|
|
508
|
+
whether its command was accepted. Do not reintroduce the old shortcut of
|
|
509
|
+
sniffing the command string for a trailing `&`: it classified shell text
|
|
510
|
+
instead of observing the process, and got both halves wrong — a launch shaped
|
|
511
|
+
`… & fi` did not match and hung, while a user's `npm run build &` was answered
|
|
512
|
+
with a fabricated empty success.
|
|
513
|
+
|
|
514
|
+
**`spawn()` is what an editor and a terminal need, and `exec()` cannot give.**
|
|
515
|
+
It returns a live process: streaming stdout/stderr, writable stdin, `kill()`.
|
|
516
|
+
Pass `pty: { cols, rows }` for a real controlling terminal — then Ctrl-C
|
|
517
|
+
(`0x03`) becomes SIGINT for the foreground job and `handle.resize({cols,rows})`
|
|
518
|
+
renegotiates the width. Omit it for a language server, whose framed JSON-RPC a
|
|
519
|
+
PTY would corrupt with echo and CR translation. A PTY request is REJECTED
|
|
520
|
+
rather than downgraded when the SDK build has no `pty` module, because a
|
|
521
|
+
terminal that silently got pipes is a terminal whose Ctrl-C does nothing.
|
|
522
|
+
|
|
523
|
+
**Two ways to make a project outlive its sandbox, and they are not the same
|
|
524
|
+
strength.** A **volume** (`SandboxConfig.volumeName` + `volumeMountPath`) is
|
|
525
|
+
continuous: every write already lives outside the microVM, so losing the
|
|
526
|
+
sandbox loses nothing. A **snapshot** (`commitTemplate`, restored by
|
|
527
|
+
`create({ templateId })`) is point-in-time: a persistent image that survives
|
|
528
|
+
the sandbox, at the cost of everything written since the capture. Prefer the
|
|
529
|
+
volume; take snapshots when the account has no volumes, or as restore points
|
|
530
|
+
alongside one.
|
|
531
|
+
|
|
532
|
+
**A volume needs a mount path, and `create()` refuses without one.** E2B
|
|
533
|
+
mounts shadow whatever the image had at that path, and this bond's superset
|
|
534
|
+
template keeps a multi-GB `/workspace/node_modules` there — so the obvious
|
|
535
|
+
default (the workspace root) is the one value that boots a project unable to
|
|
536
|
+
resolve a single import. Mount the app directory instead
|
|
537
|
+
(`/workspace/<appDir>`): the durable source lands on the volume and the
|
|
538
|
+
regenerable tooling stays on the faster image-backed rootfs.
|
|
539
|
+
|
|
540
|
+
**A volume can only be attached when the sandbox is created.** There is no
|
|
541
|
+
attach-to-a-running-sandbox call, so a sandbox claimed from a pre-warmed pool
|
|
542
|
+
can never be given a project's volume afterwards — a project that needs one
|
|
543
|
+
must be booted fresh with it.
|
|
544
|
+
|
|
545
|
+
**Volumes are a private beta on E2B.** An account without them answers
|
|
546
|
+
`403 use of volumes is not enabled` (measured on the production account,
|
|
547
|
+
2026-08-16); ask E2B support to enable them. Every volume method throws in
|
|
548
|
+
that state rather than no-op-ing, because a control plane that believes it has
|
|
549
|
+
durable storage and does not is the failure this bond exists to prevent.
|
|
550
|
+
|
|
551
|
+
**Snapshots ARE available and they do survive a kill** — verified live: a
|
|
552
|
+
sandbox was killed and a new one created from its snapshot came up with the
|
|
553
|
+
same files, in ~2.3 s. Capturing takes well under a second, leaves the sandbox
|
|
554
|
+
running, and re-using a name replaces that snapshot rather than adding one, so
|
|
555
|
+
a per-project restore point is a fixed-size resource. It BRIEFLY pauses the
|
|
556
|
+
sandbox and drops open connections (PTYs, command streams, websockets), so
|
|
557
|
+
capture when a project goes quiet — never underneath a live terminal.
|
|
558
|
+
|
|
559
|
+
**Sandboxes are created to PAUSE at their timeout, not to be killed.** E2B's
|
|
560
|
+
default is `onTimeout: 'kill'`, so a sandbox nothing touched for its lifetime
|
|
561
|
+
would be destroyed with its files. This bond creates every sandbox with
|
|
562
|
+
`lifecycle: { onTimeout: { action: 'pause', keepMemory: true } }`; the memory
|
|
563
|
+
snapshot is what lets `resume()` truthfully report `processesPreserved: true`.
|
|
564
|
+
Extending the deadline is `keepAlive(ms)` — call it from a real activity
|
|
565
|
+
signal (an open editor's heartbeat), never as a side effect of polling.
|
package/dist/index.d.ts
CHANGED
|
@@ -32,21 +32,20 @@
|
|
|
32
32
|
* const provider = createProvider({
|
|
33
33
|
* templateId: 'molecule-superset',
|
|
34
34
|
* defaultPreviewPort: 5173,
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* { domain: 'github.com', action: 'allow' },
|
|
39
|
-
* { domain: '*', action: 'deny' },
|
|
40
|
-
* ],
|
|
35
|
+
* // Deny-by-default egress: everything not listed here is blocked, raw IPs
|
|
36
|
+
* // included. An empty/omitted list applies NO policy at all.
|
|
37
|
+
* defaultAllowOut: ['registry.npmjs.org', '*.npmjs.org', 'github.com'],
|
|
41
38
|
* })
|
|
42
39
|
* ```
|
|
43
40
|
*
|
|
44
41
|
* @remarks
|
|
45
|
-
* **`verifyEgress`
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
42
|
+
* **`verifyEgress` OBSERVES, it never attests.** It boots a throwaway sandbox,
|
|
43
|
+
* applies `{ allowOut: [npm], denyOut: [ALL_TRAFFIC] }`, and curls an
|
|
44
|
+
* allow-listed host, a non-allow-listed host AND a raw IP from inside it;
|
|
45
|
+
* `filtered` requires the last two to be blocked while the first answers. Any
|
|
46
|
+
* 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).
|
|
50
49
|
*
|
|
51
50
|
* **E2B pauses, it does not stop.** `sleep()`/`stop()` both map to E2B pause
|
|
52
51
|
* (FS + memory snapshot); `wake()`/`start()` reconnect by id. `hibernate()`/
|
|
@@ -54,6 +53,94 @@
|
|
|
54
53
|
* restores the process tree — unlike a Docker stop, a resumed E2B sandbox's
|
|
55
54
|
* dev servers are still running.
|
|
56
55
|
*
|
|
56
|
+
* **`get()` RESUMES a paused sandbox — use `describe()` to look at one.**
|
|
57
|
+
* Obtaining a handle is `POST /sandboxes/{id}/connect`, which resumes a paused
|
|
58
|
+
* sandbox and extends its deadline. That is right for a caller about to USE the
|
|
59
|
+
* sandbox and wrong for every status check: polling `get()` every few seconds
|
|
60
|
+
* silently un-hibernates every sleeping project and bills for the compute while
|
|
61
|
+
* the UI still says "asleep". `describe(id)` reads the record instead, reports
|
|
62
|
+
* `sleeping` for a paused sandbox, and changes nothing.
|
|
63
|
+
*
|
|
64
|
+
* **`get()` returns `null` ONLY for a sandbox that does not exist.** Every other
|
|
65
|
+
* failure throws. A control plane reads `null` as "gone", detaches the project
|
|
66
|
+
* and rebuilds it from a template — and since an E2B microVM is the only copy of
|
|
67
|
+
* a project's files, answering a transient 5xx with `null` destroys the user's
|
|
68
|
+
* code. "I could not look" must never be delivered as "I looked, and it is not
|
|
69
|
+
* there". The same rule governs `describe()`.
|
|
70
|
+
*
|
|
71
|
+
* **`hibernate()`/`stop()`/`sleep()` pause or THROW.** They never resolve a
|
|
72
|
+
* success-shaped outcome for a sandbox that is still running: a caller's next act
|
|
73
|
+
* is to record the sandbox as stopped, and a control plane that believes a running
|
|
74
|
+
* sandbox is asleep bills for it and — with the kill timeout — watches it die at
|
|
75
|
+
* its deadline instead of hibernating. Note the converse trap: a pause is undone by
|
|
76
|
+
* the very next `get()`, since obtaining a handle connects. Anything that polls a
|
|
77
|
+
* stopped sandbox (status, logs, files) must go through `describe()`.
|
|
78
|
+
*
|
|
79
|
+
* **`exec()` starts the command and waits on its HANDLE, never inline.** E2B's
|
|
80
|
+
* inline `commands.run` waits for the whole process GROUP, so any command that
|
|
81
|
+
* leaves a detached child behind — `nohup … >log 2>&1 &`, i.e. every dev-server
|
|
82
|
+
* launch — blocks until the request deadline and then throws while the child is
|
|
83
|
+
* running perfectly. Starting in the background and awaiting the handle returns
|
|
84
|
+
* the STARTED process's real exit code in milliseconds, so a launcher learns
|
|
85
|
+
* whether its command was accepted. Do not reintroduce the old shortcut of
|
|
86
|
+
* sniffing the command string for a trailing `&`: it classified shell text
|
|
87
|
+
* instead of observing the process, and got both halves wrong — a launch shaped
|
|
88
|
+
* `… & fi` did not match and hung, while a user's `npm run build &` was answered
|
|
89
|
+
* with a fabricated empty success.
|
|
90
|
+
*
|
|
91
|
+
* **`spawn()` is what an editor and a terminal need, and `exec()` cannot give.**
|
|
92
|
+
* It returns a live process: streaming stdout/stderr, writable stdin, `kill()`.
|
|
93
|
+
* Pass `pty: { cols, rows }` for a real controlling terminal — then Ctrl-C
|
|
94
|
+
* (`0x03`) becomes SIGINT for the foreground job and `handle.resize({cols,rows})`
|
|
95
|
+
* renegotiates the width. Omit it for a language server, whose framed JSON-RPC a
|
|
96
|
+
* PTY would corrupt with echo and CR translation. A PTY request is REJECTED
|
|
97
|
+
* rather than downgraded when the SDK build has no `pty` module, because a
|
|
98
|
+
* terminal that silently got pipes is a terminal whose Ctrl-C does nothing.
|
|
99
|
+
*
|
|
100
|
+
* **Two ways to make a project outlive its sandbox, and they are not the same
|
|
101
|
+
* strength.** A **volume** (`SandboxConfig.volumeName` + `volumeMountPath`) is
|
|
102
|
+
* continuous: every write already lives outside the microVM, so losing the
|
|
103
|
+
* sandbox loses nothing. A **snapshot** (`commitTemplate`, restored by
|
|
104
|
+
* `create({ templateId })`) is point-in-time: a persistent image that survives
|
|
105
|
+
* the sandbox, at the cost of everything written since the capture. Prefer the
|
|
106
|
+
* volume; take snapshots when the account has no volumes, or as restore points
|
|
107
|
+
* alongside one.
|
|
108
|
+
*
|
|
109
|
+
* **A volume needs a mount path, and `create()` refuses without one.** E2B
|
|
110
|
+
* mounts shadow whatever the image had at that path, and this bond's superset
|
|
111
|
+
* template keeps a multi-GB `/workspace/node_modules` there — so the obvious
|
|
112
|
+
* default (the workspace root) is the one value that boots a project unable to
|
|
113
|
+
* resolve a single import. Mount the app directory instead
|
|
114
|
+
* (`/workspace/<appDir>`): the durable source lands on the volume and the
|
|
115
|
+
* regenerable tooling stays on the faster image-backed rootfs.
|
|
116
|
+
*
|
|
117
|
+
* **A volume can only be attached when the sandbox is created.** There is no
|
|
118
|
+
* attach-to-a-running-sandbox call, so a sandbox claimed from a pre-warmed pool
|
|
119
|
+
* can never be given a project's volume afterwards — a project that needs one
|
|
120
|
+
* must be booted fresh with it.
|
|
121
|
+
*
|
|
122
|
+
* **Volumes are a private beta on E2B.** An account without them answers
|
|
123
|
+
* `403 use of volumes is not enabled` (measured on the production account,
|
|
124
|
+
* 2026-08-16); ask E2B support to enable them. Every volume method throws in
|
|
125
|
+
* that state rather than no-op-ing, because a control plane that believes it has
|
|
126
|
+
* durable storage and does not is the failure this bond exists to prevent.
|
|
127
|
+
*
|
|
128
|
+
* **Snapshots ARE available and they do survive a kill** — verified live: a
|
|
129
|
+
* sandbox was killed and a new one created from its snapshot came up with the
|
|
130
|
+
* same files, in ~2.3 s. Capturing takes well under a second, leaves the sandbox
|
|
131
|
+
* running, and re-using a name replaces that snapshot rather than adding one, so
|
|
132
|
+
* a per-project restore point is a fixed-size resource. It BRIEFLY pauses the
|
|
133
|
+
* sandbox and drops open connections (PTYs, command streams, websockets), so
|
|
134
|
+
* capture when a project goes quiet — never underneath a live terminal.
|
|
135
|
+
*
|
|
136
|
+
* **Sandboxes are created to PAUSE at their timeout, not to be killed.** E2B's
|
|
137
|
+
* default is `onTimeout: 'kill'`, so a sandbox nothing touched for its lifetime
|
|
138
|
+
* would be destroyed with its files. This bond creates every sandbox with
|
|
139
|
+
* `lifecycle: { onTimeout: { action: 'pause', keepMemory: true } }`; the memory
|
|
140
|
+
* snapshot is what lets `resume()` truthfully report `processesPreserved: true`.
|
|
141
|
+
* Extending the deadline is `keepAlive(ms)` — call it from a real activity
|
|
142
|
+
* signal (an open editor's heartbeat), never as a side effect of polling.
|
|
143
|
+
*
|
|
57
144
|
* @module
|
|
58
145
|
*/
|
|
59
146
|
export * from './provider.js';
|
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgJG;AAEH,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}
|