@molecule/api-code-sandbox-e2b 1.0.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/LICENSE ADDED
@@ -0,0 +1,115 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work.
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship.
43
+
44
+ "Contribution" shall mean any work of authorship, including the
45
+ original version of the Work and any modifications or additions
46
+ to that Work, that is intentionally submitted to the Licensor for
47
+ inclusion in the Work by the copyright owner or by an individual or
48
+ Legal Entity authorized to submit on behalf of the copyright owner.
49
+
50
+ "Contributor" shall mean Licensor and any individual or Legal Entity
51
+ on behalf of whom a Contribution has been received by the Licensor and
52
+ subsequently incorporated within the Work.
53
+
54
+ 2. Grant of Copyright License. Subject to the terms and conditions of
55
+ this License, each Contributor hereby grants to You a perpetual,
56
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
57
+ copyright license to reproduce, prepare Derivative Works of,
58
+ publicly display, publicly perform, sublicense, and distribute the
59
+ Work and such Derivative Works in Source or Object form.
60
+
61
+ 3. Grant of Patent License. Subject to the terms and conditions of
62
+ this License, each Contributor hereby grants to You a perpetual,
63
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
64
+ patent license to make, have made, use, offer to sell, sell, import,
65
+ and otherwise transfer the Work.
66
+
67
+ 4. Redistribution. You may reproduce and distribute copies of the
68
+ Work or Derivative Works thereof in any medium, with or without
69
+ modifications, and in Source or Object form, provided that You
70
+ meet the following conditions:
71
+
72
+ (a) You must give any other recipients of the Work or
73
+ Derivative Works a copy of this License; and
74
+
75
+ (b) You must cause any modified files to carry prominent notices
76
+ stating that You changed the files; and
77
+
78
+ (c) You must retain, in the Source form of any Derivative Works
79
+ that You distribute, all copyright, patent, trademark, and
80
+ attribution notices from the Source form of the Work,
81
+ excluding those notices that do not pertain to any part of
82
+ the Derivative Works; and
83
+
84
+ (d) If the Work includes a "NOTICE" text file as part of its
85
+ distribution, then any Derivative Works that You distribute must
86
+ include a readable copy of the attribution notices contained
87
+ within such NOTICE file.
88
+
89
+ 5. Submission of Contributions.
90
+
91
+ 6. Trademarks. This License does not grant permission to use the trade
92
+ names, trademarks, service marks, or product names of the Licensor.
93
+
94
+ 7. Disclaimer of Warranty. Unless required by applicable law or
95
+ agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
96
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
97
+
98
+ 8. Limitation of Liability. In no event and under no legal theory shall
99
+ any Contributor be liable to You for damages.
100
+
101
+ 9. Accepting Warranty or Additional Liability.
102
+
103
+ Copyright 2026 Molecule Dev, Inc.
104
+
105
+ Licensed under the Apache License, Version 2.0 (the "License");
106
+ you may not use this file except in compliance with the License.
107
+ You may obtain a copy of the License at
108
+
109
+ http://www.apache.org/licenses/LICENSE-2.0
110
+
111
+ Unless required by applicable law or agreed to in writing, software
112
+ distributed under the License is distributed on an "AS IS" BASIS,
113
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
114
+ See the License for the specific language governing permissions and
115
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,281 @@
1
+ <!--
2
+ AUTO-GENERATED — DO NOT EDIT THIS FILE.
3
+ Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
4
+ Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
5
+ To change this document, edit the module-level JSDoc in src/index.ts.
6
+ Generated: 2026-08-10T23:56:44.341Z
7
+ -->
8
+
9
+ # @molecule/api-code-sandbox-e2b
10
+
11
+ > **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
12
+ > It is written to be read by coding agents as much as by people, and is generated from this
13
+ > package's source — edit `src/index.ts` JSDoc, not this file.
14
+
15
+ E2B (e2b.dev) code sandbox provider.
16
+
17
+ E2B runs isolated Firecracker microVMs purpose-built for agent/dev workloads:
18
+ a sandbox spawns from a golden **template** (a Dockerfile-built image with the
19
+ whole dependency set baked in) in ~1s, exposes every internal port at
20
+ `https://<port>-<id>.e2b.app`, pauses/resumes with filesystem + memory state
21
+ preserved in ~1s, and governs outbound traffic by a DNS network policy. This
22
+ bond maps that platform onto the `@molecule/api-code-sandbox` contract through
23
+ the official `e2b` SDK.
24
+
25
+ The design that makes it fast: a single golden SUPERSET template carries the
26
+ entire `@molecule` fleet node_modules + postgres + warmed Vite deps, so a boot
27
+ only copies the ONE selected app's source in and starts the dev servers — no
28
+ per-boot `npm install`. The 133 flagship template sources are NOT baked into
29
+ the image; they are copied from the control plane at boot, so templates and
30
+ `mlcl` stay private.
31
+
32
+ ## Quick Start
33
+
34
+ ```typescript
35
+ import { bond } from '@molecule/api-bond'
36
+ import { provider } from '@molecule/api-code-sandbox-e2b'
37
+
38
+ bond('codeSandbox', provider)
39
+ // Requires E2B_API_KEY (and E2B_TEMPLATE_ID for the golden superset template).
40
+ ```
41
+
42
+ ```typescript
43
+ import { createProvider } from '@molecule/api-code-sandbox-e2b'
44
+
45
+ const provider = createProvider({
46
+ templateId: 'molecule-superset',
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
+ ],
54
+ })
55
+ ```
56
+
57
+ ## Type
58
+
59
+ `provider`
60
+
61
+ ## Installation
62
+
63
+ ```bash
64
+ npm install @molecule/api-code-sandbox-e2b e2b @molecule/api-bond @molecule/api-code-sandbox @molecule/api-i18n
65
+ ```
66
+
67
+ ## API
68
+
69
+ ### Interfaces
70
+
71
+ #### `E2BCommandResultLike`
72
+
73
+ Result of an E2B command run (subset of the SDK's `CommandResult`).
74
+
75
+ ```typescript
76
+ interface E2BCommandResultLike {
77
+ stdout: string
78
+ stderr: string
79
+ exitCode: number
80
+ }
81
+ ```
82
+
83
+ #### `E2BCommandsLike`
84
+
85
+ Subset of the SDK's `Commands` the bond uses.
86
+
87
+ ```typescript
88
+ interface E2BCommandsLike {
89
+ run(
90
+ cmd: string,
91
+ opts?: { cwd?: string; timeoutMs?: number; envs?: Record<string, string> },
92
+ ): Promise<E2BCommandResultLike>
93
+ }
94
+ ```
95
+
96
+ #### `E2BConfig`
97
+
98
+ Bond configuration. All fields have safe defaults; see {@link createProvider}.
99
+
100
+ ```typescript
101
+ interface E2BConfig {
102
+ /**
103
+ * E2B API key. Falls back to `E2B_API_KEY` in the environment. Required — the
104
+ * provider throws on first use if neither is set.
105
+ */
106
+ apiKey?: string
107
+ /**
108
+ * The golden template id every sandbox boots from. Falls back to
109
+ * `E2B_TEMPLATE_ID`, then E2B's `base` template. This is the caller's OWN
110
+ * identifier for the superset template (fleet node_modules + postgres +
111
+ * warmed vite deps), built out of band by the template pipeline.
112
+ */
113
+ templateId?: string
114
+ /**
115
+ * Default port the preview URL points at (the app's Vite dev server).
116
+ * `getPreviewUrl()` uses this when no port is given.
117
+ */
118
+ defaultPreviewPort?: number
119
+ /**
120
+ * Default sandbox lifetime before E2B auto-pauses it, in milliseconds. The
121
+ * control plane extends this per-heartbeat; this is only the initial ceiling.
122
+ */
123
+ defaultTimeoutMs?: number
124
+ /**
125
+ * Egress allow-list (domains / CIDRs) applied to every sandbox at create
126
+ * time; everything else is denied (`denyOut: [ALL_TRAFFIC]`). Wildcards like
127
+ * `*.npmjs.org` are supported. Empty/omitted means the bond does NOT
128
+ * constrain egress — prod must supply this or `verifyEgress` observes `open`
129
+ * and the control plane refuses to boot (Rule 18).
130
+ *
131
+ * Verified against a live E2B sandbox: with `denyOut: [ALL_TRAFFIC]`, a
132
+ * non-allowlisted host AND a raw destination IP are both blocked — a stronger
133
+ * boundary than a DNS-only policy.
134
+ */
135
+ defaultAllowOut?: string[]
136
+ }
137
+ ```
138
+
139
+ #### `E2BFilesystemLike`
140
+
141
+ Subset of the SDK's `Filesystem` the bond uses.
142
+
143
+ ```typescript
144
+ interface E2BFilesystemLike {
145
+ read(path: string): Promise<string>
146
+ write(path: string, data: string): Promise<unknown>
147
+ list(path: string): Promise<Array<{ name: string; type?: string; size?: number }>>
148
+ remove(path: string): Promise<void>
149
+ }
150
+ ```
151
+
152
+ #### `E2BSandboxClientLike`
153
+
154
+ Subset of the SDK's `Sandbox` static surface the bond uses.
155
+
156
+ ```typescript
157
+ interface E2BSandboxClientLike {
158
+ create(templateId: string, opts?: Record<string, unknown>): Promise<E2BSandboxLike>
159
+ connect(sandboxId: string, opts?: Record<string, unknown>): Promise<E2BSandboxLike>
160
+ list(
161
+ opts?: Record<string, unknown>,
162
+ ): Promise<Array<{ sandboxId: string }> | { sandboxes?: Array<{ sandboxId: string }> }>
163
+ kill?(sandboxId: string, opts?: Record<string, unknown>): Promise<boolean>
164
+ }
165
+ ```
166
+
167
+ #### `E2BSandboxLike`
168
+
169
+ Subset of the SDK's `Sandbox` instance the bond uses.
170
+
171
+ ```typescript
172
+ interface E2BSandboxLike {
173
+ sandboxId: string
174
+ commands: E2BCommandsLike
175
+ files: E2BFilesystemLike
176
+ getHost(port: number): string
177
+ setTimeout(ms: number): Promise<void>
178
+ kill(): Promise<void>
179
+ pause?(): Promise<string>
180
+ betaPause?(): Promise<string>
181
+ isRunning(): Promise<boolean>
182
+ updateNetwork?(opts: { allowOut?: string[]; denyOut?: string[] }): Promise<void>
183
+ }
184
+ ```
185
+
186
+ ### Classes
187
+
188
+ #### `E2BSandboxProvider`
189
+
190
+ E2B implementation of {@link SandboxProvider}.
191
+
192
+ Only the required surface (`create`/`get`/`list`/`destroy`) plus the boot-path
193
+ optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
194
+ land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
195
+ the control plane treats "unsupported" as `inconclusive` and refuses to boot
196
+ in prod, which is the correct safe default until egress observation is proven
197
+ (Rule 18 — never trade cost for security).
198
+
199
+ ### Functions
200
+
201
+ #### `createProvider(config, clientOverride)`
202
+
203
+ Create an E2B provider with the given configuration.
204
+
205
+ ```typescript
206
+ function createProvider(
207
+ config?: E2BConfig,
208
+ clientOverride?: E2BSandboxClientLike,
209
+ ): E2BSandboxProvider
210
+ ```
211
+
212
+ - `config` — Bond configuration; API key falls back to `E2B_API_KEY`.
213
+ - `clientOverride` — Inject a fake client (tests).
214
+
215
+ **Returns:** A configured provider ready to `bond('codeSandbox', provider)`.
216
+
217
+ ### Constants
218
+
219
+ #### `provider`
220
+
221
+ Default provider instance, configured from the environment.
222
+
223
+ ```typescript
224
+ const provider: SandboxProvider
225
+ ```
226
+
227
+ ## Core Interface
228
+
229
+ Implements `@molecule/api-code-sandbox` interface.
230
+
231
+ ## Bond Wiring
232
+
233
+ Setup function to register this provider with the core interface:
234
+
235
+ ```typescript
236
+ import { setProvider } from '@molecule/api-code-sandbox'
237
+ import { provider } from '@molecule/api-code-sandbox-e2b'
238
+
239
+ export function setupCodeSandboxE2b(): void {
240
+ setProvider(provider)
241
+ }
242
+ ```
243
+
244
+ ## Injection Notes
245
+
246
+ ### Requirements
247
+
248
+ Peer dependencies:
249
+
250
+ - `@molecule/api-bond` ^1.0.1
251
+ - `@molecule/api-code-sandbox` ^1.0.1
252
+ - `@molecule/api-i18n` ^1.0.1
253
+
254
+ ### Environment Variables
255
+
256
+ - `E2B_API_KEY` _(required)_ — E2B API key
257
+ - Setup: Create an API key in the E2B dashboard.
258
+ - Get it here: [https://e2b.dev/dashboard](https://e2b.dev/dashboard)
259
+ - Example: `e2b_...`
260
+ - `E2B_TEMPLATE_ID` _(optional)_ — E2B golden template id
261
+ - Setup: The superset template id every sandbox boots from; defaults to E2B base.
262
+ - Example: `molecule-superset`
263
+
264
+ ### Runtime Dependencies
265
+
266
+ - `e2b`
267
+ - `@molecule/api-bond`
268
+ - `@molecule/api-code-sandbox`
269
+ - `@molecule/api-i18n`
270
+
271
+ **`verifyEgress` is intentionally not implemented yet.** The control plane
272
+ treats an absent `verifyEgress` as an `inconclusive` verdict and refuses to
273
+ boot sandboxes in production — the correct safe default until egress
274
+ observation is proven against E2B's `updateNetwork` policy (Rule 18: never
275
+ trade cost for security). Do not stub it with a fabricated `filtered`.
276
+
277
+ **E2B pauses, it does not stop.** `sleep()`/`stop()` both map to E2B pause
278
+ (FS + memory snapshot); `wake()`/`start()` reconnect by id. `hibernate()`/
279
+ `resume()` report `processesPreserved: true` because the memory snapshot
280
+ restores the process tree — unlike a Docker stop, a resumed E2B sandbox's
281
+ dev servers are still running.
@@ -0,0 +1,61 @@
1
+ /**
2
+ * E2B (e2b.dev) code sandbox provider.
3
+ *
4
+ * E2B runs isolated Firecracker microVMs purpose-built for agent/dev workloads:
5
+ * a sandbox spawns from a golden **template** (a Dockerfile-built image with the
6
+ * whole dependency set baked in) in ~1s, exposes every internal port at
7
+ * `https://<port>-<id>.e2b.app`, pauses/resumes with filesystem + memory state
8
+ * preserved in ~1s, and governs outbound traffic by a DNS network policy. This
9
+ * bond maps that platform onto the `@molecule/api-code-sandbox` contract through
10
+ * the official `e2b` SDK.
11
+ *
12
+ * The design that makes it fast: a single golden SUPERSET template carries the
13
+ * entire `@molecule` fleet node_modules + postgres + warmed Vite deps, so a boot
14
+ * only copies the ONE selected app's source in and starts the dev servers — no
15
+ * per-boot `npm install`. The 133 flagship template sources are NOT baked into
16
+ * the image; they are copied from the control plane at boot, so templates and
17
+ * `mlcl` stay private.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { bond } from '@molecule/api-bond'
22
+ * import { provider } from '@molecule/api-code-sandbox-e2b'
23
+ *
24
+ * bond('codeSandbox', provider)
25
+ * // Requires E2B_API_KEY (and E2B_TEMPLATE_ID for the golden superset template).
26
+ * ```
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { createProvider } from '@molecule/api-code-sandbox-e2b'
31
+ *
32
+ * const provider = createProvider({
33
+ * templateId: 'molecule-superset',
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
+ * ],
41
+ * })
42
+ * ```
43
+ *
44
+ * @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`.
50
+ *
51
+ * **E2B pauses, it does not stop.** `sleep()`/`stop()` both map to E2B pause
52
+ * (FS + memory snapshot); `wake()`/`start()` reconnect by id. `hibernate()`/
53
+ * `resume()` report `processesPreserved: true` because the memory snapshot
54
+ * restores the process tree — unlike a Docker stop, a resumed E2B sandbox's
55
+ * dev servers are still running.
56
+ *
57
+ * @module
58
+ */
59
+ export * from './provider.js';
60
+ export * from './types.js';
61
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * E2B (e2b.dev) code sandbox provider.
3
+ *
4
+ * E2B runs isolated Firecracker microVMs purpose-built for agent/dev workloads:
5
+ * a sandbox spawns from a golden **template** (a Dockerfile-built image with the
6
+ * whole dependency set baked in) in ~1s, exposes every internal port at
7
+ * `https://<port>-<id>.e2b.app`, pauses/resumes with filesystem + memory state
8
+ * preserved in ~1s, and governs outbound traffic by a DNS network policy. This
9
+ * bond maps that platform onto the `@molecule/api-code-sandbox` contract through
10
+ * the official `e2b` SDK.
11
+ *
12
+ * The design that makes it fast: a single golden SUPERSET template carries the
13
+ * entire `@molecule` fleet node_modules + postgres + warmed Vite deps, so a boot
14
+ * only copies the ONE selected app's source in and starts the dev servers — no
15
+ * per-boot `npm install`. The 133 flagship template sources are NOT baked into
16
+ * the image; they are copied from the control plane at boot, so templates and
17
+ * `mlcl` stay private.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * import { bond } from '@molecule/api-bond'
22
+ * import { provider } from '@molecule/api-code-sandbox-e2b'
23
+ *
24
+ * bond('codeSandbox', provider)
25
+ * // Requires E2B_API_KEY (and E2B_TEMPLATE_ID for the golden superset template).
26
+ * ```
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * import { createProvider } from '@molecule/api-code-sandbox-e2b'
31
+ *
32
+ * const provider = createProvider({
33
+ * templateId: 'molecule-superset',
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
+ * ],
41
+ * })
42
+ * ```
43
+ *
44
+ * @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`.
50
+ *
51
+ * **E2B pauses, it does not stop.** `sleep()`/`stop()` both map to E2B pause
52
+ * (FS + memory snapshot); `wake()`/`start()` reconnect by id. `hibernate()`/
53
+ * `resume()` report `processesPreserved: true` because the memory snapshot
54
+ * restores the process tree — unlike a Docker stop, a resumed E2B sandbox's
55
+ * dev servers are still running.
56
+ *
57
+ * @module
58
+ */
59
+ export * from './provider.js';
60
+ export * from './types.js';
@@ -0,0 +1,92 @@
1
+ /**
2
+ * E2B code-sandbox provider implementation.
3
+ *
4
+ * Maps the `@molecule/api-code-sandbox` contract onto E2B's Firecracker microVM
5
+ * platform via the official `e2b` SDK. The SDK is reached through the structural
6
+ * {@link E2BSandboxClientLike} shim so the provider is unit-testable with a fake
7
+ * client; {@link defaultClient} adapts the real `Sandbox` class.
8
+ *
9
+ * @module
10
+ */
11
+ import type { EgressVerdict, Sandbox, SandboxConfig, SandboxProvider } from '@molecule/api-code-sandbox';
12
+ import type { E2BConfig, E2BSandboxClientLike } from './types.js';
13
+ /**
14
+ * E2B implementation of {@link SandboxProvider}.
15
+ *
16
+ * Only the required surface (`create`/`get`/`list`/`destroy`) plus the boot-path
17
+ * optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
18
+ * land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
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).
22
+ */
23
+ export declare class E2BSandboxProvider implements SandboxProvider {
24
+ readonly name = "e2b";
25
+ private readonly config;
26
+ private clientPromise;
27
+ private readonly clientOverride?;
28
+ /**
29
+ * Construct the provider from config, resolving the API key and template id.
30
+ *
31
+ * @param config - Bond configuration; API key falls back to `E2B_API_KEY`.
32
+ * @param clientOverride - Inject a fake client (tests).
33
+ */
34
+ constructor(config?: E2BConfig, clientOverride?: E2BSandboxClientLike);
35
+ /**
36
+ * Resolve the SDK client, memoized. Throws when no API key is configured.
37
+ *
38
+ * @returns The E2B client (injected override or real SDK adapter).
39
+ */
40
+ private client;
41
+ /**
42
+ * Create a new sandbox from the golden template and apply the egress policy.
43
+ *
44
+ * @param config - Project id, env, optional per-boot templateId + labels.
45
+ * @returns A live sandbox handle.
46
+ */
47
+ create(config: SandboxConfig): Promise<Sandbox>;
48
+ /**
49
+ * Resolve an existing sandbox by id.
50
+ *
51
+ * @param id - The sandbox id.
52
+ * @returns A live handle, or `null` if no sandbox has that id.
53
+ */
54
+ get(id: string): Promise<Sandbox | null>;
55
+ /**
56
+ * List the caller's live sandboxes.
57
+ *
58
+ * @param _userId - Unused; E2B scopes by API key, not per-user labels here.
59
+ * @returns Live handles for every running sandbox.
60
+ */
61
+ list(_userId: string): Promise<Sandbox[]>;
62
+ /**
63
+ * Destroy a sandbox, freeing its resources.
64
+ *
65
+ * @param id - The sandbox id.
66
+ */
67
+ destroy(id: string): Promise<void>;
68
+ /**
69
+ * PROVE egress is deny-by-default by OBSERVING it on a throwaway sandbox.
70
+ *
71
+ * Creates a sandbox, applies `{ allowOut: [npm], denyOut: [ALL_TRAFFIC] }`,
72
+ * then curls an allow-listed host, a non-allow-listed host, AND a raw IP from
73
+ * inside. `filtered` requires BOTH the non-allow-listed host and the raw IP to
74
+ * be blocked while the allow-listed host is reachable — never an attestation.
75
+ * Any failure to run the probe is `inconclusive`, never `filtered` ("could not
76
+ * look" must not read as "safe"). Verified live: E2B blocks raw IPs too.
77
+ *
78
+ * @returns The observed egress verdict.
79
+ */
80
+ verifyEgress(): Promise<EgressVerdict>;
81
+ }
82
+ /**
83
+ * Create an E2B provider with the given configuration.
84
+ *
85
+ * @param config - Bond configuration; API key falls back to `E2B_API_KEY`.
86
+ * @param clientOverride - Inject a fake client (tests).
87
+ * @returns A configured provider ready to `bond('codeSandbox', provider)`.
88
+ */
89
+ export declare function createProvider(config?: E2BConfig, clientOverride?: E2BSandboxClientLike): E2BSandboxProvider;
90
+ /** Default provider instance, configured from the environment. */
91
+ export declare const provider: SandboxProvider;
92
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAEV,aAAa,EAKb,OAAO,EACP,aAAa,EACb,eAAe,EAChB,MAAM,4BAA4B,CAAA;AAEnC,OAAO,KAAK,EAAE,SAAS,EAAE,oBAAoB,EAAkB,MAAM,YAAY,CAAA;AAoRjF;;;;;;;;;GASG;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;;;;;OAKG;IACG,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IAmBrD;;;;;OAKG;IACG,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAgB9C;;;;;OAKG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAY/C;;;;OAIG;IACG,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxC;;;;;;;;;;;OAWG;IACG,YAAY,IAAI,OAAO,CAAC,aAAa,CAAC;CAyD7C;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"}
@@ -0,0 +1,465 @@
1
+ /**
2
+ * E2B code-sandbox provider implementation.
3
+ *
4
+ * Maps the `@molecule/api-code-sandbox` contract onto E2B's Firecracker microVM
5
+ * platform via the official `e2b` SDK. The SDK is reached through the structural
6
+ * {@link E2BSandboxClientLike} shim so the provider is unit-testable with a fake
7
+ * client; {@link defaultClient} adapts the real `Sandbox` class.
8
+ *
9
+ * @module
10
+ */
11
+ /** Default Vite dev-server port the preview URL points at. */
12
+ const DEFAULT_PREVIEW_PORT = 5173;
13
+ /** Default sandbox lifetime before E2B auto-pauses (control plane extends per heartbeat). */
14
+ const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
15
+ /**
16
+ * E2B's "all destinations" selector (`0.0.0.0/0`, the SDK's `ALL_TRAFFIC`).
17
+ * Put in `denyOut` alongside an `allowOut` list to make egress deny-by-default —
18
+ * E2B REQUIRES this pairing (a bare `allowOut` is a 400) and it blocks raw IPs
19
+ * too, not just DNS names. Inlined as the literal to avoid importing the SDK at
20
+ * module load (the provider imports `e2b` lazily).
21
+ */
22
+ const ALL_TRAFFIC = '0.0.0.0/0';
23
+ /** Hosts the egress probe treats as the canonical allow/deny witnesses. */
24
+ const EGRESS_PROBE_ALLOW = 'registry.npmjs.org';
25
+ const EGRESS_PROBE_DENY = 'example.com';
26
+ /**
27
+ * Adapt the real `e2b` SDK `Sandbox` class to {@link E2BSandboxClientLike}.
28
+ * Imported lazily so the SDK is only required when the bond is actually used.
29
+ */
30
+ async function defaultClient(apiKey) {
31
+ const { Sandbox } = await import('e2b');
32
+ const S = Sandbox;
33
+ return {
34
+ create: (templateId, opts) => S.create(templateId, { apiKey, ...opts }),
35
+ connect: (id, opts) => S.connect(id, { apiKey, ...opts }),
36
+ kill: (id, opts) => S.kill(id, { apiKey, ...opts }),
37
+ async list(opts) {
38
+ // Sandbox.list returns a paginator; normalize to a flat array of running
39
+ // sandboxes. Support the async-iterator, nextItems(), and array shapes so
40
+ // a minor SDK change does not silently return nothing.
41
+ const result = S.list({ apiKey, ...opts });
42
+ const out = [];
43
+ const push = (items) => {
44
+ for (const it of items)
45
+ if (it?.sandboxId)
46
+ out.push({ sandboxId: it.sandboxId });
47
+ };
48
+ const r = await Promise.resolve(result).catch(() => result);
49
+ if (Array.isArray(r)) {
50
+ push(r);
51
+ }
52
+ else if (r && typeof r.nextItems === 'function') {
53
+ const pager = r;
54
+ do {
55
+ push(await pager.nextItems());
56
+ } while (pager.hasNext);
57
+ }
58
+ else if (r &&
59
+ typeof r[Symbol.asyncIterator] === 'function') {
60
+ for await (const it of r)
61
+ push([it]);
62
+ }
63
+ else if (r && Array.isArray(r.sandboxes)) {
64
+ push(r.sandboxes);
65
+ }
66
+ return out;
67
+ },
68
+ };
69
+ }
70
+ /**
71
+ * Build a deny-by-default network update from an allow-list.
72
+ *
73
+ * @param allowOut - Domains/CIDRs permitted; everything else is denied.
74
+ * @returns The SDK `updateNetwork` payload (allowOut + denyOut ALL_TRAFFIC).
75
+ */
76
+ function denyByDefault(allowOut) {
77
+ return { allowOut, denyOut: [ALL_TRAFFIC] };
78
+ }
79
+ /**
80
+ * A live E2B sandbox mapped onto the core `Sandbox` handle.
81
+ *
82
+ * E2B pauses/resumes rather than start/stop; a resume yields a NEW underlying
83
+ * SDK instance, so the handle re-resolves it by id on wake and swaps
84
+ * {@link E2BSandbox.sbx}. `getHost` is a pure string builder
85
+ * (`<port>-<id>.e2b.app`) so `previewUrl` is known the instant the id is.
86
+ */
87
+ class E2BSandbox {
88
+ id;
89
+ status = 'running';
90
+ previewUrl;
91
+ sbx;
92
+ client;
93
+ previewPort;
94
+ timeoutMs;
95
+ /**
96
+ * Wrap a live E2B SDK sandbox as a core `Sandbox` handle.
97
+ *
98
+ * @param sbx - The live E2B SDK sandbox instance.
99
+ * @param client - The client used to reconnect the instance on resume.
100
+ * @param opts - Preview port and auto-pause timeout for this sandbox.
101
+ */
102
+ constructor(sbx, client, opts) {
103
+ this.sbx = sbx;
104
+ this.id = sbx.sandboxId;
105
+ this.client = client;
106
+ this.previewPort = opts.previewPort;
107
+ this.timeoutMs = opts.timeoutMs;
108
+ this.previewUrl = this.getPreviewUrl();
109
+ }
110
+ /**
111
+ * The public HTTPS URL for a port inside the sandbox.
112
+ *
113
+ * @param port - Sandbox port; defaults to the configured preview port.
114
+ * @returns `https://<port>-<id>.e2b.app`.
115
+ */
116
+ getPreviewUrl(port) {
117
+ return `https://${this.sbx.getHost(port ?? this.previewPort)}`;
118
+ }
119
+ /** Resume the sandbox (E2B has no separate cold start). */
120
+ async start() {
121
+ await this.wake();
122
+ }
123
+ /** Pause the sandbox (E2B has no stop distinct from pause). */
124
+ async stop() {
125
+ // E2B has no stop-without-losing-state distinct from pause; treat as pause.
126
+ await this.sleep();
127
+ }
128
+ /** Pause the sandbox, discarding the hibernation outcome. */
129
+ async sleep() {
130
+ await this.hibernate();
131
+ }
132
+ /** Resume the sandbox, discarding the hibernation outcome. */
133
+ async wake() {
134
+ await this.resume();
135
+ }
136
+ /**
137
+ * Pause the sandbox (E2B FS + memory snapshot).
138
+ *
139
+ * @returns The outcome; `processesPreserved` is true because the memory
140
+ * snapshot restores the process tree on resume.
141
+ */
142
+ async hibernate() {
143
+ const pause = this.sbx.betaPause ?? this.sbx.pause;
144
+ if (!pause) {
145
+ // No pause available — nothing we can honestly suspend; report it.
146
+ return { processesPreserved: true, mechanism: 'noop', detail: 'pause not supported by SDK' };
147
+ }
148
+ await pause.call(this.sbx);
149
+ this.status = 'sleeping';
150
+ // E2B pause snapshots FS + memory, so the process tree survives resume.
151
+ return { processesPreserved: true, mechanism: 'pause' };
152
+ }
153
+ /**
154
+ * Resume the sandbox by reconnecting to a fresh live instance for its id.
155
+ *
156
+ * @returns The outcome; `processesPreserved` is true (memory snapshot).
157
+ */
158
+ async resume() {
159
+ // Reconnect resolves a fresh live instance for the same id (a no-op if it is
160
+ // already running); swap it in so subsequent calls hit the live sandbox.
161
+ this.sbx = await this.client.connect(this.id, { timeoutMs: this.timeoutMs });
162
+ this.status = 'running';
163
+ this.previewUrl = this.getPreviewUrl();
164
+ return { processesPreserved: true, mechanism: 'resume' };
165
+ }
166
+ /**
167
+ * Run a command to completion in the sandbox.
168
+ *
169
+ * @param command - The shell command to run.
170
+ * @param opts - Working directory, timeout (ms), and env vars.
171
+ * @returns stdout, stderr and the exit code.
172
+ */
173
+ async exec(command, opts) {
174
+ const r = await this.sbx.commands.run(command, {
175
+ cwd: opts?.cwd,
176
+ timeoutMs: opts?.timeout,
177
+ envs: opts?.env,
178
+ });
179
+ return { stdout: r.stdout, stderr: r.stderr, exitCode: r.exitCode };
180
+ }
181
+ /**
182
+ * Read a file's contents as text.
183
+ *
184
+ * @param path - Absolute path inside the sandbox.
185
+ * @returns The file contents.
186
+ */
187
+ async readFile(path) {
188
+ return this.sbx.files.read(path);
189
+ }
190
+ /**
191
+ * Write text to a file, creating parent directories as needed.
192
+ *
193
+ * @param path - Absolute path inside the sandbox.
194
+ * @param content - The text to write.
195
+ */
196
+ async writeFile(path, content) {
197
+ await this.sbx.files.write(path, content);
198
+ }
199
+ /**
200
+ * List a directory. Throws when the path does not exist (an empty array
201
+ * means "exists and is empty", never "missing").
202
+ *
203
+ * @param path - Absolute directory path inside the sandbox.
204
+ * @returns The directory entries.
205
+ */
206
+ async readDir(path) {
207
+ // E2B throws when the path is missing (contract: empty array ONLY means
208
+ // "exists and empty"), so we do not catch here.
209
+ const entries = await this.sbx.files.list(path);
210
+ return entries.map((e) => ({
211
+ name: e.name,
212
+ type: e.type === 'dir' || e.type === 'directory' ? 'directory' : 'file',
213
+ ...(typeof e.size === 'number' ? { size: e.size } : {}),
214
+ }));
215
+ }
216
+ /**
217
+ * Delete a file.
218
+ *
219
+ * @param path - Absolute path inside the sandbox.
220
+ */
221
+ async deleteFile(path) {
222
+ await this.sbx.files.remove(path);
223
+ }
224
+ /**
225
+ * Subscribe to filesystem changes. Not wired for E2B (the control plane polls
226
+ * the tree); returns a no-op unsubscribe so register-and-forget callers are safe.
227
+ *
228
+ * @param _cb - Ignored change callback.
229
+ * @returns A no-op unsubscribe function.
230
+ */
231
+ onFileChange(_cb) {
232
+ // E2B exposes filesystem watch via files.watchDir; the control plane polls
233
+ // the tree rather than subscribing, so this bond does not wire it yet.
234
+ // Returning a no-op unsubscribe keeps callers that register-and-forget safe.
235
+ return () => { };
236
+ }
237
+ /**
238
+ * Extend the sandbox's auto-pause deadline (heartbeat).
239
+ *
240
+ * @param ms - New lifetime in milliseconds from now.
241
+ */
242
+ async keepAlive(ms) {
243
+ await this.sbx.setTimeout(ms);
244
+ }
245
+ /**
246
+ * Apply a deny-by-default egress allow-list to this sandbox.
247
+ *
248
+ * @param allowOut - Domains/CIDRs to permit; everything else denied. A no-op
249
+ * when empty or when the SDK build lacks `updateNetwork`.
250
+ */
251
+ async applyNetwork(allowOut) {
252
+ if (!this.sbx.updateNetwork || allowOut.length === 0)
253
+ return;
254
+ await this.sbx.updateNetwork(denyByDefault(allowOut));
255
+ }
256
+ }
257
+ /**
258
+ * E2B implementation of {@link SandboxProvider}.
259
+ *
260
+ * Only the required surface (`create`/`get`/`list`/`destroy`) plus the boot-path
261
+ * optionals are wired here; `verifyEgress` and `commitTemplate`/`getTemplate`
262
+ * land in follow-up steps. Leaving `verifyEgress` UNimplemented is deliberate:
263
+ * the control plane treats "unsupported" as `inconclusive` and refuses to boot
264
+ * in prod, which is the correct safe default until egress observation is proven
265
+ * (Rule 18 — never trade cost for security).
266
+ */
267
+ export class E2BSandboxProvider {
268
+ name = 'e2b';
269
+ config;
270
+ clientPromise = null;
271
+ clientOverride;
272
+ /**
273
+ * Construct the provider from config, resolving the API key and template id.
274
+ *
275
+ * @param config - Bond configuration; API key falls back to `E2B_API_KEY`.
276
+ * @param clientOverride - Inject a fake client (tests).
277
+ */
278
+ constructor(config = {}, clientOverride) {
279
+ const apiKey = config.apiKey ?? process.env.E2B_API_KEY ?? '';
280
+ this.config = {
281
+ apiKey,
282
+ templateId: config.templateId ?? process.env.E2B_TEMPLATE_ID ?? 'base',
283
+ defaultPreviewPort: config.defaultPreviewPort ?? DEFAULT_PREVIEW_PORT,
284
+ defaultTimeoutMs: config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
285
+ defaultAllowOut: config.defaultAllowOut ?? [],
286
+ };
287
+ this.clientOverride = clientOverride;
288
+ }
289
+ /**
290
+ * Resolve the SDK client, memoized. Throws when no API key is configured.
291
+ *
292
+ * @returns The E2B client (injected override or real SDK adapter).
293
+ */
294
+ async client() {
295
+ if (this.clientOverride)
296
+ return this.clientOverride;
297
+ if (!this.config.apiKey) {
298
+ throw new Error('E2B provider requires an API key (config.apiKey or E2B_API_KEY)');
299
+ }
300
+ if (!this.clientPromise)
301
+ this.clientPromise = defaultClient(this.config.apiKey);
302
+ return this.clientPromise;
303
+ }
304
+ /**
305
+ * Create a new sandbox from the golden template and apply the egress policy.
306
+ *
307
+ * @param config - Project id, env, optional per-boot templateId + labels.
308
+ * @returns A live sandbox handle.
309
+ */
310
+ async create(config) {
311
+ const client = await this.client();
312
+ const templateId = config.templateId ?? this.config.templateId;
313
+ const sbx = await client.create(templateId, {
314
+ timeoutMs: this.config.defaultTimeoutMs,
315
+ envs: config.env ?? {},
316
+ metadata: { projectId: config.projectId, ...(config.labels ?? {}) },
317
+ });
318
+ const handle = new E2BSandbox(sbx, client, {
319
+ previewPort: this.config.defaultPreviewPort,
320
+ timeoutMs: this.config.defaultTimeoutMs,
321
+ });
322
+ // Apply the egress allow-list immediately, before any user code runs.
323
+ if (this.config.defaultAllowOut.length > 0) {
324
+ await handle.applyNetwork(this.config.defaultAllowOut);
325
+ }
326
+ return handle;
327
+ }
328
+ /**
329
+ * Resolve an existing sandbox by id.
330
+ *
331
+ * @param id - The sandbox id.
332
+ * @returns A live handle, or `null` if no sandbox has that id.
333
+ */
334
+ async get(id) {
335
+ const client = await this.client();
336
+ try {
337
+ const sbx = await client.connect(id, { timeoutMs: this.config.defaultTimeoutMs });
338
+ return new E2BSandbox(sbx, client, {
339
+ previewPort: this.config.defaultPreviewPort,
340
+ timeoutMs: this.config.defaultTimeoutMs,
341
+ });
342
+ }
343
+ catch (_error) {
344
+ // connect throws SandboxNotFoundError for a genuinely absent id → null.
345
+ // A transient error also lands here; get() is used as an existence probe,
346
+ // and the callers re-resolve, so returning null is the safe answer.
347
+ return null;
348
+ }
349
+ }
350
+ /**
351
+ * List the caller's live sandboxes.
352
+ *
353
+ * @param _userId - Unused; E2B scopes by API key, not per-user labels here.
354
+ * @returns Live handles for every running sandbox.
355
+ */
356
+ async list(_userId) {
357
+ const client = await this.client();
358
+ const running = await client.list({});
359
+ const items = Array.isArray(running) ? running : (running.sandboxes ?? []);
360
+ const handles = [];
361
+ for (const it of items) {
362
+ const h = await this.get(it.sandboxId);
363
+ if (h)
364
+ handles.push(h);
365
+ }
366
+ return handles;
367
+ }
368
+ /**
369
+ * Destroy a sandbox, freeing its resources.
370
+ *
371
+ * @param id - The sandbox id.
372
+ */
373
+ async destroy(id) {
374
+ const client = await this.client();
375
+ if (client.kill) {
376
+ await client.kill(id, {});
377
+ return;
378
+ }
379
+ const sbx = await client.connect(id, {}).catch((error) => {
380
+ // Already gone / unreachable — nothing to kill; destroy is idempotent.
381
+ void error;
382
+ return null;
383
+ });
384
+ if (sbx)
385
+ await sbx.kill();
386
+ }
387
+ /**
388
+ * PROVE egress is deny-by-default by OBSERVING it on a throwaway sandbox.
389
+ *
390
+ * Creates a sandbox, applies `{ allowOut: [npm], denyOut: [ALL_TRAFFIC] }`,
391
+ * then curls an allow-listed host, a non-allow-listed host, AND a raw IP from
392
+ * inside. `filtered` requires BOTH the non-allow-listed host and the raw IP to
393
+ * be blocked while the allow-listed host is reachable — never an attestation.
394
+ * Any failure to run the probe is `inconclusive`, never `filtered` ("could not
395
+ * look" must not read as "safe"). Verified live: E2B blocks raw IPs too.
396
+ *
397
+ * @returns The observed egress verdict.
398
+ */
399
+ async verifyEgress() {
400
+ let handle = null;
401
+ try {
402
+ handle = await this.create({ projectId: `egress-probe-${Date.now()}`, env: {} });
403
+ }
404
+ catch (error) {
405
+ return {
406
+ state: 'inconclusive',
407
+ detail: `Could not create a probe sandbox: ${error instanceof Error ? error.message : String(error)}`,
408
+ remediation: 'Check E2B_API_KEY and account capacity.',
409
+ };
410
+ }
411
+ try {
412
+ // Force a KNOWN deny-default policy for the probe regardless of config.
413
+ await handle.applyNetwork([
414
+ EGRESS_PROBE_ALLOW,
415
+ `*.${EGRESS_PROBE_ALLOW.split('.').slice(-2).join('.')}`,
416
+ ]);
417
+ const code = async (host) => (await handle.exec(`curl -s -o /dev/null -m 8 -w '%{http_code}' https://${host}/ || echo 000`)).stdout.trim();
418
+ const [allowed, deniedHost, deniedIp] = await Promise.all([
419
+ code(EGRESS_PROBE_ALLOW),
420
+ code(EGRESS_PROBE_DENY),
421
+ code('1.1.1.1'),
422
+ ]);
423
+ const blocked = (c) => c === '' || c.startsWith('000');
424
+ if (blocked(deniedHost) && blocked(deniedIp) && !blocked(allowed)) {
425
+ return {
426
+ state: 'filtered',
427
+ detail: `deny-by-default verified: ${EGRESS_PROBE_ALLOW}=${allowed} reachable; ${EGRESS_PROBE_DENY}=${deniedHost} and raw IP=${deniedIp} blocked.`,
428
+ };
429
+ }
430
+ return {
431
+ state: 'open',
432
+ detail: `Non-allow-listed egress was reachable (host=${deniedHost}, rawIP=${deniedIp}, allowed=${allowed}).`,
433
+ remediation: 'Ensure updateNetwork applies allowOut + denyOut:[0.0.0.0/0]; check the E2B account supports network policy.',
434
+ };
435
+ }
436
+ catch (error) {
437
+ return {
438
+ state: 'inconclusive',
439
+ detail: `Egress probe could not run: ${error instanceof Error ? error.message : String(error)}`,
440
+ };
441
+ }
442
+ finally {
443
+ // Best-effort cleanup of the throwaway probe sandbox; a failed destroy
444
+ // must not mask the verdict (E2B auto-pauses idle sandboxes regardless).
445
+ if (handle) {
446
+ await this.destroy(handle.id).catch((_error) => {
447
+ // intentional noop — probe teardown is best-effort; the sandbox
448
+ // auto-pauses and the verdict is what matters.
449
+ });
450
+ }
451
+ }
452
+ }
453
+ }
454
+ /**
455
+ * Create an E2B provider with the given configuration.
456
+ *
457
+ * @param config - Bond configuration; API key falls back to `E2B_API_KEY`.
458
+ * @param clientOverride - Inject a fake client (tests).
459
+ * @returns A configured provider ready to `bond('codeSandbox', provider)`.
460
+ */
461
+ export function createProvider(config = {}, clientOverride) {
462
+ return new E2BSandboxProvider(config, clientOverride);
463
+ }
464
+ /** Default provider instance, configured from the environment. */
465
+ export const provider = createProvider();
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Configuration + narrow structural shims for the E2B code-sandbox bond.
3
+ *
4
+ * The bond talks to E2B through the official `e2b` SDK; these types capture only
5
+ * the slice of that SDK surface the provider touches, plus the bond's own
6
+ * configuration. Keeping them structural (rather than importing the SDK's own
7
+ * types) keeps the provider testable with a fake client and documents exactly
8
+ * which SDK methods this bond depends on.
9
+ *
10
+ * @module
11
+ */
12
+ /** Bond configuration. All fields have safe defaults; see {@link createProvider}. */
13
+ export interface E2BConfig {
14
+ /**
15
+ * E2B API key. Falls back to `E2B_API_KEY` in the environment. Required — the
16
+ * provider throws on first use if neither is set.
17
+ */
18
+ apiKey?: string;
19
+ /**
20
+ * The golden template id every sandbox boots from. Falls back to
21
+ * `E2B_TEMPLATE_ID`, then E2B's `base` template. This is the caller's OWN
22
+ * identifier for the superset template (fleet node_modules + postgres +
23
+ * warmed vite deps), built out of band by the template pipeline.
24
+ */
25
+ templateId?: string;
26
+ /**
27
+ * Default port the preview URL points at (the app's Vite dev server).
28
+ * `getPreviewUrl()` uses this when no port is given.
29
+ */
30
+ defaultPreviewPort?: number;
31
+ /**
32
+ * Default sandbox lifetime before E2B auto-pauses it, in milliseconds. The
33
+ * control plane extends this per-heartbeat; this is only the initial ceiling.
34
+ */
35
+ defaultTimeoutMs?: number;
36
+ /**
37
+ * Egress allow-list (domains / CIDRs) applied to every sandbox at create
38
+ * time; everything else is denied (`denyOut: [ALL_TRAFFIC]`). Wildcards like
39
+ * `*.npmjs.org` are supported. Empty/omitted means the bond does NOT
40
+ * constrain egress — prod must supply this or `verifyEgress` observes `open`
41
+ * and the control plane refuses to boot (Rule 18).
42
+ *
43
+ * Verified against a live E2B sandbox: with `denyOut: [ALL_TRAFFIC]`, a
44
+ * non-allowlisted host AND a raw destination IP are both blocked — a stronger
45
+ * boundary than a DNS-only policy.
46
+ */
47
+ defaultAllowOut?: string[];
48
+ }
49
+ /** Result of an E2B command run (subset of the SDK's `CommandResult`). */
50
+ export interface E2BCommandResultLike {
51
+ stdout: string;
52
+ stderr: string;
53
+ exitCode: number;
54
+ }
55
+ /** Subset of the SDK's `Filesystem` the bond uses. */
56
+ export interface E2BFilesystemLike {
57
+ read(path: string): Promise<string>;
58
+ write(path: string, data: string): Promise<unknown>;
59
+ list(path: string): Promise<Array<{
60
+ name: string;
61
+ type?: string;
62
+ size?: number;
63
+ }>>;
64
+ remove(path: string): Promise<void>;
65
+ }
66
+ /** Subset of the SDK's `Commands` the bond uses. */
67
+ export interface E2BCommandsLike {
68
+ run(cmd: string, opts?: {
69
+ cwd?: string;
70
+ timeoutMs?: number;
71
+ envs?: Record<string, string>;
72
+ }): Promise<E2BCommandResultLike>;
73
+ }
74
+ /** Subset of the SDK's `Sandbox` instance the bond uses. */
75
+ export interface E2BSandboxLike {
76
+ sandboxId: string;
77
+ commands: E2BCommandsLike;
78
+ files: E2BFilesystemLike;
79
+ getHost(port: number): string;
80
+ setTimeout(ms: number): Promise<void>;
81
+ kill(): Promise<void>;
82
+ pause?(): Promise<string>;
83
+ betaPause?(): Promise<string>;
84
+ isRunning(): Promise<boolean>;
85
+ updateNetwork?(opts: {
86
+ allowOut?: string[];
87
+ denyOut?: string[];
88
+ }): Promise<void>;
89
+ }
90
+ /** Subset of the SDK's `Sandbox` static surface the bond uses. */
91
+ export interface E2BSandboxClientLike {
92
+ create(templateId: string, opts?: Record<string, unknown>): Promise<E2BSandboxLike>;
93
+ connect(sandboxId: string, opts?: Record<string, unknown>): Promise<E2BSandboxLike>;
94
+ list(opts?: Record<string, unknown>): Promise<Array<{
95
+ sandboxId: string;
96
+ }> | {
97
+ sandboxes?: Array<{
98
+ sandboxId: string;
99
+ }>;
100
+ }>;
101
+ kill?(sandboxId: string, opts?: Record<string, unknown>): Promise<boolean>;
102
+ }
103
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Configuration + narrow structural shims for the E2B code-sandbox bond.
3
+ *
4
+ * The bond talks to E2B through the official `e2b` SDK; these types capture only
5
+ * the slice of that SDK surface the provider touches, plus the bond's own
6
+ * configuration. Keeping them structural (rather than importing the SDK's own
7
+ * types) keeps the provider testable with a fake client and documents exactly
8
+ * which SDK methods this bond depends on.
9
+ *
10
+ * @module
11
+ */
12
+ export {};
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@molecule/api-code-sandbox-e2b",
3
+ "version": "1.0.0",
4
+ "description": "E2B (e2b.dev) code sandbox provider — Firecracker microVMs with golden templates, fork, and pause/resume",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "keywords": [
24
+ "molecule",
25
+ "sandbox",
26
+ "e2b",
27
+ "firecracker",
28
+ "code-execution"
29
+ ],
30
+ "license": "Apache-2.0",
31
+ "devDependencies": {
32
+ "@molecule/api-bond": "1.0.1",
33
+ "@molecule/api-code-sandbox": "1.0.3",
34
+ "@types/node": "26.1.2",
35
+ "typescript": "6.0.3",
36
+ "vitest": "4.1.10"
37
+ },
38
+ "peerDependencies": {
39
+ "@molecule/api-bond": "^1.0.1",
40
+ "@molecule/api-code-sandbox": "^1.0.1",
41
+ "@molecule/api-i18n": "^1.0.1"
42
+ },
43
+ "peerDependenciesMeta": {
44
+ "@molecule/api-code-sandbox": {
45
+ "optional": true
46
+ }
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/molecule-dev/molecule.git",
51
+ "directory": "packages/api/bonds/code-sandbox/e2b"
52
+ },
53
+ "homepage": "https://github.com/molecule-dev/molecule/tree/main/packages/api/bonds/code-sandbox/e2b",
54
+ "bugs": "https://github.com/molecule-dev/molecule/issues",
55
+ "publishConfig": {
56
+ "access": "public"
57
+ },
58
+ "dependencies": {
59
+ "e2b": "2.38.3"
60
+ }
61
+ }