@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 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-11T00:53:00.606Z
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
- defaultNetworkRules: [
49
- { domain: 'registry.npmjs.org', action: 'allow' },
50
- { domain: '*.npmjs.org', action: 'allow' },
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 e2b @molecule/api-bond @molecule/api-code-sandbox @molecule/api-i18n
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
- run(
90
- cmd: string,
91
- opts?: {
92
- cwd?: string
93
- timeoutMs?: number
94
- envs?: Record<string, string>
95
- /** Fire-and-forget: returns immediately without waiting for exit. */
96
- background?: boolean
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<Array<{ sandboxId: string }> | { sandboxes?: Array<{ sandboxId: string }> }>
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
- pause?(): Promise<string>
189
- betaPause?(): Promise<string>
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
- - `e2b`
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` is intentionally not implemented yet.** The control plane
281
- treats an absent `verifyEgress` as an `inconclusive` verdict and refuses to
282
- boot sandboxes in production — the correct safe default until egress
283
- observation is proven against E2B's `updateNetwork` policy (Rule 18: never
284
- trade cost for security). Do not stub it with a fabricated `filtered`.
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
- * defaultNetworkRules: [
36
- * { domain: 'registry.npmjs.org', action: 'allow' },
37
- * { domain: '*.npmjs.org', action: 'allow' },
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` is intentionally not implemented yet.** The control plane
46
- * treats an absent `verifyEgress` as an `inconclusive` verdict and refuses to
47
- * boot sandboxes in production — the correct safe default until egress
48
- * observation is proven against E2B's `updateNetwork` policy (Rule 18: never
49
- * trade cost for security). Do not stub it with a fabricated `filtered`.
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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgJG;AAEH,cAAc,eAAe,CAAA;AAC7B,cAAc,YAAY,CAAA"}