openlattice-e2b 0.0.3

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.
@@ -0,0 +1,10 @@
1
+ export interface E2BProviderConfig {
2
+ /** E2B API key. Default: reads E2B_API_KEY env var. */
3
+ apiKey?: string;
4
+ /** Default template ID. Used when spec.runtime.image matches no known template. */
5
+ defaultTemplate?: string;
6
+ /** Map of image strings to E2B template IDs. */
7
+ templateMap?: Record<string, string>;
8
+ /** Default sandbox timeout in ms. Default: 300_000 (5 min). */
9
+ defaultTimeoutMs?: number;
10
+ }
package/dist/config.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,20 @@
1
+ import type { ComputeProvider, ComputeSpec, ExecOpts, ExecResult, ExtensionMap, HealthStatus, ProviderCapabilities, ProviderNode, ProviderNodeStatus } from "openlattice";
2
+ import type { E2BProviderConfig } from "./config";
3
+ export declare class E2BProvider implements ComputeProvider {
4
+ readonly name = "e2b";
5
+ readonly capabilities: ProviderCapabilities;
6
+ private readonly config;
7
+ constructor(config?: E2BProviderConfig);
8
+ provision(spec: ComputeSpec): Promise<ProviderNode>;
9
+ exec(externalId: string, command: string[], opts?: ExecOpts): Promise<ExecResult>;
10
+ destroy(externalId: string): Promise<void>;
11
+ inspect(externalId: string): Promise<ProviderNodeStatus>;
12
+ pause(externalId: string): Promise<void>;
13
+ resume(externalId: string): Promise<void>;
14
+ healthCheck(): Promise<HealthStatus>;
15
+ getExtension<K extends keyof ExtensionMap>(externalId: string, extension: K): ExtensionMap[K] | undefined;
16
+ private tailscaleUp;
17
+ private resolveTemplate;
18
+ private createFileExtension;
19
+ private createNetworkExtension;
20
+ }
@@ -0,0 +1,255 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.E2BProvider = void 0;
4
+ const e2b_1 = require("e2b");
5
+ const MAX_LIFETIME_SECONDS = 86400; // 24h Pro tier
6
+ const E2B_API_DOMAIN = "api.e2b.dev";
7
+ class E2BProvider {
8
+ constructor(config = {}) {
9
+ this.name = "e2b";
10
+ this.config = config;
11
+ this.capabilities = {
12
+ restart: false,
13
+ pause: true, // via REST API
14
+ snapshot: false,
15
+ gpu: false, // Firecracker lacks PCIe passthrough
16
+ logs: false, // per-command only
17
+ tailscale: true, // microVM has its own kernel with TUN support
18
+ coldStartMs: 150,
19
+ maxConcurrent: 100, // Pro tier
20
+ architectures: ["x86_64"],
21
+ persistentStorage: false,
22
+ constraints: {
23
+ maxLifetimeSeconds: MAX_LIFETIME_SECONDS,
24
+ },
25
+ };
26
+ }
27
+ // ── Required methods ────────────────────────────────────────────
28
+ async provision(spec) {
29
+ if (spec.gpu && spec.gpu.count > 0) {
30
+ throw new Error("[e2b] GPU is not supported (Firecracker limitation)");
31
+ }
32
+ if (spec.duration?.maxSeconds && spec.duration.maxSeconds > MAX_LIFETIME_SECONDS) {
33
+ throw new Error(`[e2b] max lifetime is ${MAX_LIFETIME_SECONDS}s (24h), ` +
34
+ `requested ${spec.duration.maxSeconds}s`);
35
+ }
36
+ const template = this.resolveTemplate(spec.runtime.image);
37
+ const timeoutMs = spec.duration?.maxSeconds != null
38
+ ? spec.duration.maxSeconds * 1000
39
+ : this.config.defaultTimeoutMs ?? 300000;
40
+ const sandbox = await e2b_1.Sandbox.create(template, {
41
+ apiKey: this.config.apiKey,
42
+ timeoutMs,
43
+ envs: spec.runtime.env,
44
+ metadata: spec.labels,
45
+ });
46
+ // Run initial command if specified
47
+ if (spec.runtime.command && spec.runtime.command.length > 0) {
48
+ const cmd = spec.runtime.command.join(" ");
49
+ await sandbox.commands.run(cmd, { background: true });
50
+ }
51
+ // Join Tailscale network if auth key provided
52
+ if (spec.network?.tailscaleAuthKey) {
53
+ await this.tailscaleUp(sandbox, spec.network.tailscaleAuthKey);
54
+ }
55
+ return {
56
+ externalId: sandbox.sandboxId,
57
+ endpoints: [],
58
+ metadata: { templateId: template },
59
+ };
60
+ }
61
+ async exec(externalId, command, opts) {
62
+ const sandbox = await e2b_1.Sandbox.connect(externalId, {
63
+ apiKey: this.config.apiKey,
64
+ });
65
+ const cmd = command.join(" ");
66
+ const result = await sandbox.commands.run(cmd, {
67
+ cwd: opts?.cwd,
68
+ envs: opts?.env,
69
+ timeoutMs: opts?.timeoutMs ?? 0,
70
+ onStdout: opts?.onStdout
71
+ ? (data) => opts.onStdout(data)
72
+ : undefined,
73
+ onStderr: opts?.onStderr
74
+ ? (data) => opts.onStderr(data)
75
+ : undefined,
76
+ });
77
+ return {
78
+ exitCode: result.exitCode,
79
+ stdout: result.stdout,
80
+ stderr: result.stderr,
81
+ };
82
+ }
83
+ async destroy(externalId) {
84
+ try {
85
+ await e2b_1.Sandbox.kill(externalId, {
86
+ apiKey: this.config.apiKey,
87
+ });
88
+ }
89
+ catch (err) {
90
+ // If the sandbox is already gone, treat as idempotent
91
+ if (isNotFoundError(err))
92
+ return;
93
+ throw new Error(`[e2b] destroy failed: ${err instanceof Error ? err.message : String(err)}`);
94
+ }
95
+ }
96
+ async inspect(externalId) {
97
+ try {
98
+ const sandboxes = await e2b_1.Sandbox.list({
99
+ apiKey: this.config.apiKey,
100
+ });
101
+ const match = sandboxes.find((s) => s.sandboxId === externalId);
102
+ if (!match) {
103
+ return { status: "terminated" };
104
+ }
105
+ const status = match.state === "paused" ? "paused" : "running";
106
+ return {
107
+ status,
108
+ startedAt: match.startedAt ? new Date(match.startedAt) : undefined,
109
+ resources: {
110
+ cpuPercent: undefined,
111
+ memoryUsedMiB: match.memoryMB,
112
+ },
113
+ };
114
+ }
115
+ catch (err) {
116
+ if (isNotFoundError(err)) {
117
+ return { status: "terminated" };
118
+ }
119
+ throw err;
120
+ }
121
+ }
122
+ // ── Optional: pause / resume ────────────────────────────────────
123
+ async pause(externalId) {
124
+ // The e2b SDK v1.13 doesn't expose pause() on Sandbox instances,
125
+ // so we call the REST API directly.
126
+ const apiKey = this.config.apiKey ?? process.env.E2B_API_KEY;
127
+ if (!apiKey) {
128
+ throw new Error("[e2b] API key required for pause");
129
+ }
130
+ const response = await fetch(`https://${E2B_API_DOMAIN}/sandboxes/${externalId}/pause`, {
131
+ method: "POST",
132
+ headers: {
133
+ "X-API-Key": apiKey,
134
+ },
135
+ });
136
+ if (!response.ok && response.status !== 204) {
137
+ throw new Error(`[e2b] pause failed: ${response.status} ${response.statusText}`);
138
+ }
139
+ }
140
+ async resume(externalId) {
141
+ // Connecting to a paused sandbox auto-resumes it
142
+ await e2b_1.Sandbox.connect(externalId, {
143
+ apiKey: this.config.apiKey,
144
+ });
145
+ }
146
+ // ── Optional: healthCheck ───────────────────────────────────────
147
+ async healthCheck() {
148
+ const start = Date.now();
149
+ try {
150
+ await e2b_1.Sandbox.list({ apiKey: this.config.apiKey });
151
+ return {
152
+ healthy: true,
153
+ latencyMs: Date.now() - start,
154
+ };
155
+ }
156
+ catch (err) {
157
+ return {
158
+ healthy: false,
159
+ message: err instanceof Error ? err.message : String(err),
160
+ latencyMs: Date.now() - start,
161
+ };
162
+ }
163
+ }
164
+ // ── Optional: extensions ────────────────────────────────────────
165
+ getExtension(externalId, extension) {
166
+ if (extension === "files") {
167
+ return this.createFileExtension(externalId);
168
+ }
169
+ if (extension === "network") {
170
+ return this.createNetworkExtension(externalId);
171
+ }
172
+ return undefined;
173
+ }
174
+ // ── Private helpers ─────────────────────────────────────────────
175
+ async tailscaleUp(sandbox, authKey) {
176
+ const result = await sandbox.commands.run(`pgrep tailscaled >/dev/null 2>&1 || tailscaled --state=/var/lib/tailscale/tailscaled.state & sleep 1 && tailscale up --authkey=${authKey}`, { timeoutMs: 30000 });
177
+ if (result.exitCode !== 0) {
178
+ throw new Error(`[e2b] tailscale up failed: ${result.stderr.trim()}`);
179
+ }
180
+ }
181
+ resolveTemplate(image) {
182
+ // 1. Check explicit template map
183
+ if (this.config.templateMap && image in this.config.templateMap) {
184
+ return this.config.templateMap[image];
185
+ }
186
+ // 2. If it looks like an E2B template ID (no / or :), use directly
187
+ if (!image.includes("/") && !image.includes(":")) {
188
+ return image;
189
+ }
190
+ // 3. Fall back to default template
191
+ if (this.config.defaultTemplate) {
192
+ return this.config.defaultTemplate;
193
+ }
194
+ throw new Error(`[e2b] cannot resolve template for image "${image}". ` +
195
+ "Provide a templateMap entry, a valid E2B template ID, or set defaultTemplate.");
196
+ }
197
+ createFileExtension(externalId) {
198
+ const apiKey = this.config.apiKey;
199
+ return {
200
+ async read(path) {
201
+ const sandbox = await e2b_1.Sandbox.connect(externalId, { apiKey });
202
+ return await sandbox.files.read(path, { format: "text" });
203
+ },
204
+ async write(path, content) {
205
+ const sandbox = await e2b_1.Sandbox.connect(externalId, { apiKey });
206
+ await sandbox.files.write(path, content.toString());
207
+ },
208
+ async list(dirPath) {
209
+ const sandbox = await e2b_1.Sandbox.connect(externalId, { apiKey });
210
+ const entries = await sandbox.files.list(dirPath);
211
+ return entries.map((e) => ({
212
+ name: e.name,
213
+ path: e.path ?? `${dirPath}/${e.name}`,
214
+ type: e.type === "dir" ? "directory" : "file",
215
+ size: e.size,
216
+ }));
217
+ },
218
+ async remove(path) {
219
+ const sandbox = await e2b_1.Sandbox.connect(externalId, { apiKey });
220
+ await sandbox.files.remove(path);
221
+ },
222
+ async mkdir(path) {
223
+ const sandbox = await e2b_1.Sandbox.connect(externalId, { apiKey });
224
+ await sandbox.files.makeDir(path);
225
+ },
226
+ };
227
+ }
228
+ createNetworkExtension(externalId) {
229
+ const apiKey = this.config.apiKey;
230
+ return {
231
+ async getUrl(port) {
232
+ const sandbox = await e2b_1.Sandbox.connect(externalId, { apiKey });
233
+ const host = sandbox.getHost(port);
234
+ return `https://${host}`;
235
+ },
236
+ };
237
+ }
238
+ }
239
+ exports.E2BProvider = E2BProvider;
240
+ // ── Utility functions ───────────────────────────────────────────────
241
+ function isNotFoundError(err) {
242
+ if (typeof err !== "object" || err === null)
243
+ return false;
244
+ // E2B SDK throws NotFoundError for missing sandboxes
245
+ if ("statusCode" in err && err.statusCode === 404) {
246
+ return true;
247
+ }
248
+ if (err instanceof Error && err.constructor.name === "NotFoundError") {
249
+ return true;
250
+ }
251
+ if (err instanceof Error && /not found|does not exist/i.test(err.message)) {
252
+ return true;
253
+ }
254
+ return false;
255
+ }
@@ -0,0 +1,2 @@
1
+ export { E2BProvider } from "./e2b-provider";
2
+ export type { E2BProviderConfig } from "./config";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.E2BProvider = void 0;
4
+ var e2b_provider_1 = require("./e2b-provider");
5
+ Object.defineProperty(exports, "E2BProvider", { enumerable: true, get: function () { return e2b_provider_1.E2BProvider; } });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "openlattice-e2b",
3
+ "version": "0.0.3",
4
+ "description": "E2B compute provider for OpenLattice",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "test": "vitest run",
16
+ "test:integration": "E2B_API_KEY=$E2B_API_KEY vitest run tests/integration.test.ts tests/conformance.test.ts"
17
+ },
18
+ "keywords": [
19
+ "compute",
20
+ "e2b",
21
+ "openlattice",
22
+ "provider",
23
+ "sandbox"
24
+ ],
25
+ "license": "ISC",
26
+ "peerDependencies": {
27
+ "openlattice": "^0.0.3"
28
+ },
29
+ "dependencies": {
30
+ "e2b": "^1.13.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^25.2.2",
34
+ "openlattice": "^0.0.3",
35
+ "typescript": "^5.9.3",
36
+ "vitest": "^4.0.18"
37
+ }
38
+ }
package/src/config.ts ADDED
@@ -0,0 +1,13 @@
1
+ export interface E2BProviderConfig {
2
+ /** E2B API key. Default: reads E2B_API_KEY env var. */
3
+ apiKey?: string;
4
+
5
+ /** Default template ID. Used when spec.runtime.image matches no known template. */
6
+ defaultTemplate?: string;
7
+
8
+ /** Map of image strings to E2B template IDs. */
9
+ templateMap?: Record<string, string>;
10
+
11
+ /** Default sandbox timeout in ms. Default: 300_000 (5 min). */
12
+ defaultTimeoutMs?: number;
13
+ }
@@ -0,0 +1,332 @@
1
+ import { Sandbox } from "e2b";
2
+ import type {
3
+ ComputeProvider,
4
+ ComputeSpec,
5
+ ExecOpts,
6
+ ExecResult,
7
+ ExtensionMap,
8
+ FileEntry,
9
+ FileExtension,
10
+ HealthStatus,
11
+ NetworkExtension,
12
+ ProviderCapabilities,
13
+ ProviderNode,
14
+ ProviderNodeStatus,
15
+ } from "openlattice";
16
+ import type { E2BProviderConfig } from "./config";
17
+
18
+ const MAX_LIFETIME_SECONDS = 86_400; // 24h Pro tier
19
+ const E2B_API_DOMAIN = "api.e2b.dev";
20
+
21
+ export class E2BProvider implements ComputeProvider {
22
+ readonly name = "e2b";
23
+ readonly capabilities: ProviderCapabilities;
24
+ private readonly config: E2BProviderConfig;
25
+
26
+ constructor(config: E2BProviderConfig = {}) {
27
+ this.config = config;
28
+ this.capabilities = {
29
+ restart: false,
30
+ pause: true, // via REST API
31
+ snapshot: false,
32
+ gpu: false, // Firecracker lacks PCIe passthrough
33
+ logs: false, // per-command only
34
+ tailscale: true, // microVM has its own kernel with TUN support
35
+ coldStartMs: 150,
36
+ maxConcurrent: 100, // Pro tier
37
+ architectures: ["x86_64"],
38
+ persistentStorage: false,
39
+ constraints: {
40
+ maxLifetimeSeconds: MAX_LIFETIME_SECONDS,
41
+ },
42
+ };
43
+ }
44
+
45
+ // ── Required methods ────────────────────────────────────────────
46
+
47
+ async provision(spec: ComputeSpec): Promise<ProviderNode> {
48
+ if (spec.gpu && spec.gpu.count > 0) {
49
+ throw new Error("[e2b] GPU is not supported (Firecracker limitation)");
50
+ }
51
+
52
+ if (spec.duration?.maxSeconds && spec.duration.maxSeconds > MAX_LIFETIME_SECONDS) {
53
+ throw new Error(
54
+ `[e2b] max lifetime is ${MAX_LIFETIME_SECONDS}s (24h), ` +
55
+ `requested ${spec.duration.maxSeconds}s`
56
+ );
57
+ }
58
+
59
+ const template = this.resolveTemplate(spec.runtime.image);
60
+ const timeoutMs =
61
+ spec.duration?.maxSeconds != null
62
+ ? spec.duration.maxSeconds * 1000
63
+ : this.config.defaultTimeoutMs ?? 300_000;
64
+
65
+ const sandbox = await Sandbox.create(template, {
66
+ apiKey: this.config.apiKey,
67
+ timeoutMs,
68
+ envs: spec.runtime.env,
69
+ metadata: spec.labels,
70
+ });
71
+
72
+ // Run initial command if specified
73
+ if (spec.runtime.command && spec.runtime.command.length > 0) {
74
+ const cmd = spec.runtime.command.join(" ");
75
+ await sandbox.commands.run(cmd, { background: true });
76
+ }
77
+
78
+ // Join Tailscale network if auth key provided
79
+ if (spec.network?.tailscaleAuthKey) {
80
+ await this.tailscaleUp(sandbox, spec.network.tailscaleAuthKey);
81
+ }
82
+
83
+ return {
84
+ externalId: sandbox.sandboxId,
85
+ endpoints: [],
86
+ metadata: { templateId: template },
87
+ };
88
+ }
89
+
90
+ async exec(
91
+ externalId: string,
92
+ command: string[],
93
+ opts?: ExecOpts
94
+ ): Promise<ExecResult> {
95
+ const sandbox = await Sandbox.connect(externalId, {
96
+ apiKey: this.config.apiKey,
97
+ });
98
+
99
+ const cmd = command.join(" ");
100
+
101
+ const result = await sandbox.commands.run(cmd, {
102
+ cwd: opts?.cwd,
103
+ envs: opts?.env,
104
+ timeoutMs: opts?.timeoutMs ?? 0,
105
+ onStdout: opts?.onStdout
106
+ ? (data: string) => opts.onStdout!(data)
107
+ : undefined,
108
+ onStderr: opts?.onStderr
109
+ ? (data: string) => opts.onStderr!(data)
110
+ : undefined,
111
+ });
112
+
113
+ return {
114
+ exitCode: result.exitCode,
115
+ stdout: result.stdout,
116
+ stderr: result.stderr,
117
+ };
118
+ }
119
+
120
+ async destroy(externalId: string): Promise<void> {
121
+ try {
122
+ await Sandbox.kill(externalId, {
123
+ apiKey: this.config.apiKey,
124
+ });
125
+ } catch (err: unknown) {
126
+ // If the sandbox is already gone, treat as idempotent
127
+ if (isNotFoundError(err)) return;
128
+ throw new Error(
129
+ `[e2b] destroy failed: ${err instanceof Error ? err.message : String(err)}`
130
+ );
131
+ }
132
+ }
133
+
134
+ async inspect(externalId: string): Promise<ProviderNodeStatus> {
135
+ try {
136
+ const sandboxes = await Sandbox.list({
137
+ apiKey: this.config.apiKey,
138
+ });
139
+
140
+ const match = sandboxes.find(
141
+ (s) => s.sandboxId === externalId
142
+ );
143
+
144
+ if (!match) {
145
+ return { status: "terminated" };
146
+ }
147
+
148
+ const status = match.state === "paused" ? "paused" : "running";
149
+
150
+ return {
151
+ status,
152
+ startedAt: match.startedAt ? new Date(match.startedAt) : undefined,
153
+ resources: {
154
+ cpuPercent: undefined,
155
+ memoryUsedMiB: match.memoryMB,
156
+ },
157
+ };
158
+ } catch (err: unknown) {
159
+ if (isNotFoundError(err)) {
160
+ return { status: "terminated" };
161
+ }
162
+ throw err;
163
+ }
164
+ }
165
+
166
+ // ── Optional: pause / resume ────────────────────────────────────
167
+
168
+ async pause(externalId: string): Promise<void> {
169
+ // The e2b SDK v1.13 doesn't expose pause() on Sandbox instances,
170
+ // so we call the REST API directly.
171
+ const apiKey = this.config.apiKey ?? process.env.E2B_API_KEY;
172
+ if (!apiKey) {
173
+ throw new Error("[e2b] API key required for pause");
174
+ }
175
+
176
+ const response = await fetch(
177
+ `https://${E2B_API_DOMAIN}/sandboxes/${externalId}/pause`,
178
+ {
179
+ method: "POST",
180
+ headers: {
181
+ "X-API-Key": apiKey,
182
+ },
183
+ }
184
+ );
185
+
186
+ if (!response.ok && response.status !== 204) {
187
+ throw new Error(
188
+ `[e2b] pause failed: ${response.status} ${response.statusText}`
189
+ );
190
+ }
191
+ }
192
+
193
+ async resume(externalId: string): Promise<void> {
194
+ // Connecting to a paused sandbox auto-resumes it
195
+ await Sandbox.connect(externalId, {
196
+ apiKey: this.config.apiKey,
197
+ });
198
+ }
199
+
200
+ // ── Optional: healthCheck ───────────────────────────────────────
201
+
202
+ async healthCheck(): Promise<HealthStatus> {
203
+ const start = Date.now();
204
+ try {
205
+ await Sandbox.list({ apiKey: this.config.apiKey });
206
+ return {
207
+ healthy: true,
208
+ latencyMs: Date.now() - start,
209
+ };
210
+ } catch (err: unknown) {
211
+ return {
212
+ healthy: false,
213
+ message: err instanceof Error ? err.message : String(err),
214
+ latencyMs: Date.now() - start,
215
+ };
216
+ }
217
+ }
218
+
219
+ // ── Optional: extensions ────────────────────────────────────────
220
+
221
+ getExtension<K extends keyof ExtensionMap>(
222
+ externalId: string,
223
+ extension: K
224
+ ): ExtensionMap[K] | undefined {
225
+ if (extension === "files") {
226
+ return this.createFileExtension(externalId) as ExtensionMap[K];
227
+ }
228
+ if (extension === "network") {
229
+ return this.createNetworkExtension(externalId) as ExtensionMap[K];
230
+ }
231
+ return undefined;
232
+ }
233
+
234
+ // ── Private helpers ─────────────────────────────────────────────
235
+
236
+ private async tailscaleUp(
237
+ sandbox: InstanceType<typeof Sandbox>,
238
+ authKey: string
239
+ ): Promise<void> {
240
+ const result = await sandbox.commands.run(
241
+ `pgrep tailscaled >/dev/null 2>&1 || tailscaled --state=/var/lib/tailscale/tailscaled.state & sleep 1 && tailscale up --authkey=${authKey}`,
242
+ { timeoutMs: 30_000 }
243
+ );
244
+ if (result.exitCode !== 0) {
245
+ throw new Error(
246
+ `[e2b] tailscale up failed: ${result.stderr.trim()}`
247
+ );
248
+ }
249
+ }
250
+
251
+ private resolveTemplate(image: string): string {
252
+ // 1. Check explicit template map
253
+ if (this.config.templateMap && image in this.config.templateMap) {
254
+ return this.config.templateMap[image];
255
+ }
256
+
257
+ // 2. If it looks like an E2B template ID (no / or :), use directly
258
+ if (!image.includes("/") && !image.includes(":")) {
259
+ return image;
260
+ }
261
+
262
+ // 3. Fall back to default template
263
+ if (this.config.defaultTemplate) {
264
+ return this.config.defaultTemplate;
265
+ }
266
+
267
+ throw new Error(
268
+ `[e2b] cannot resolve template for image "${image}". ` +
269
+ "Provide a templateMap entry, a valid E2B template ID, or set defaultTemplate."
270
+ );
271
+ }
272
+
273
+ private createFileExtension(externalId: string): FileExtension {
274
+ const apiKey = this.config.apiKey;
275
+ return {
276
+ async read(path: string): Promise<string | Buffer> {
277
+ const sandbox = await Sandbox.connect(externalId, { apiKey });
278
+ return await sandbox.files.read(path, { format: "text" });
279
+ },
280
+ async write(path: string, content: string | Buffer): Promise<void> {
281
+ const sandbox = await Sandbox.connect(externalId, { apiKey });
282
+ await sandbox.files.write(path, content.toString());
283
+ },
284
+ async list(dirPath: string): Promise<FileEntry[]> {
285
+ const sandbox = await Sandbox.connect(externalId, { apiKey });
286
+ const entries = await sandbox.files.list(dirPath);
287
+ return entries.map((e) => ({
288
+ name: e.name,
289
+ path: e.path ?? `${dirPath}/${e.name}`,
290
+ type: e.type === "dir" ? ("directory" as const) : ("file" as const),
291
+ size: e.size,
292
+ }));
293
+ },
294
+ async remove(path: string): Promise<void> {
295
+ const sandbox = await Sandbox.connect(externalId, { apiKey });
296
+ await sandbox.files.remove(path);
297
+ },
298
+ async mkdir(path: string): Promise<void> {
299
+ const sandbox = await Sandbox.connect(externalId, { apiKey });
300
+ await sandbox.files.makeDir(path);
301
+ },
302
+ };
303
+ }
304
+
305
+ private createNetworkExtension(externalId: string): NetworkExtension {
306
+ const apiKey = this.config.apiKey;
307
+ return {
308
+ async getUrl(port: number): Promise<string> {
309
+ const sandbox = await Sandbox.connect(externalId, { apiKey });
310
+ const host = sandbox.getHost(port);
311
+ return `https://${host}`;
312
+ },
313
+ };
314
+ }
315
+ }
316
+
317
+ // ── Utility functions ───────────────────────────────────────────────
318
+
319
+ function isNotFoundError(err: unknown): boolean {
320
+ if (typeof err !== "object" || err === null) return false;
321
+ // E2B SDK throws NotFoundError for missing sandboxes
322
+ if ("statusCode" in err && (err as { statusCode: number }).statusCode === 404) {
323
+ return true;
324
+ }
325
+ if (err instanceof Error && err.constructor.name === "NotFoundError") {
326
+ return true;
327
+ }
328
+ if (err instanceof Error && /not found|does not exist/i.test(err.message)) {
329
+ return true;
330
+ }
331
+ return false;
332
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { E2BProvider } from "./e2b-provider";
2
+ export type { E2BProviderConfig } from "./config";