@termbridge/sandbox-e2b 1.0.7

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,54 @@
1
+ import type { ExecResult, SandboxProvider } from "@termbridge/core";
2
+ interface SandboxCreateOpts {
3
+ template?: string;
4
+ envs?: Record<string, string>;
5
+ apiKey?: string;
6
+ timeoutMs?: number;
7
+ metadata?: Record<string, string>;
8
+ }
9
+ interface E2BSandboxLike {
10
+ readonly sandboxId: string;
11
+ commands: {
12
+ run(cmd: string, opts?: {
13
+ timeoutMs?: number;
14
+ }): Promise<{
15
+ exitCode: number;
16
+ stdout: string;
17
+ stderr: string;
18
+ error?: string;
19
+ }>;
20
+ };
21
+ kill(): Promise<boolean>;
22
+ }
23
+ export interface E2BSandboxProviderOptions {
24
+ /** E2B API key. Defaults to E2B_API_KEY env. Required for a real cloud call. */
25
+ apiKey?: string;
26
+ /** E2B sandbox template (must permit tmux). Defaults to "base". */
27
+ template?: string;
28
+ /** Sandbox lifetime in ms. Defaults to 3_600_000 (1h). */
29
+ timeoutMs?: number;
30
+ /** Injectable sandbox factory (for tests). Defaults to the real E2B Sandbox.create. */
31
+ sandboxFactory?: (opts: SandboxCreateOpts) => Promise<E2BSandboxLike>;
32
+ }
33
+ export declare class E2BSandboxProvider implements SandboxProvider {
34
+ readonly name = "e2b";
35
+ private readonly apiKey;
36
+ private readonly template;
37
+ private readonly timeoutMs;
38
+ private readonly sandboxFactory;
39
+ private sandbox;
40
+ constructor(opts?: E2BSandboxProviderOptions);
41
+ ensure(opts: {
42
+ name: string;
43
+ cwd: string;
44
+ image?: string;
45
+ env?: Record<string, string>;
46
+ }): Promise<void>;
47
+ exec(args: string[]): Promise<ExecResult>;
48
+ /** Run a raw shell command string; map CommandExitError → non-zero result. */
49
+ private execRaw;
50
+ destroy(): Promise<void>;
51
+ /** Cloud sandbox id if one is currently provisioned (for smoke/logging). */
52
+ get sandboxId(): string | undefined;
53
+ }
54
+ export {};
@@ -0,0 +1,114 @@
1
+ // E2BSandboxProvider — a concrete SandboxProvider backed by the E2B cloud sandbox
2
+ // SDK. One instance provisions/execs/destroys exactly one E2B sandbox (one sandbox
3
+ // == one termbridge session). Every SDK call is behind an injectable
4
+ // `sandboxFactory` so the unit is tested with a recording mock and never touches a
5
+ // real cloud (D3: this package owns the SDK; @termbridge/core stays dependency-free).
6
+ //
7
+ // exec() maps an argv to the SDK's commands.run, which takes a SHELL COMMAND STRING
8
+ // (not an argv) — so each arg is shell-quoted. The SDK throws CommandExitError on
9
+ // non-zero exit; we catch it and return {stdout, stderr, code} verbatim so exec
10
+ // NEVER rejects on non-zero (the ExecFn contract DockerEnvironment + tmux helpers
11
+ // honour). Genuine SDK errors (network/auth) propagate.
12
+ import { Sandbox } from "e2b";
13
+ const DEFAULT_TIMEOUT_MS = 3_600_000;
14
+ /** Shell-quote a single argv token (single-quote wrap if it has spaces/metachars). */
15
+ function shellQuote(arg) {
16
+ if (arg.length === 0)
17
+ return "''";
18
+ if (/^[\w@%+=:,./-]+$/.test(arg))
19
+ return arg;
20
+ return `'${arg.replace(/'/g, "'\\''")}'`;
21
+ }
22
+ export class E2BSandboxProvider {
23
+ name = "e2b";
24
+ apiKey;
25
+ template;
26
+ timeoutMs;
27
+ sandboxFactory;
28
+ sandbox;
29
+ constructor(opts = {}) {
30
+ this.apiKey = opts.apiKey ?? process.env.E2B_API_KEY;
31
+ this.template = opts.template ?? "base";
32
+ this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
33
+ this.sandboxFactory =
34
+ opts.sandboxFactory ??
35
+ ((o) => Sandbox.create(o));
36
+ }
37
+ async ensure(opts) {
38
+ // Tear down any previous sandbox this instance owned (one instance == one sandbox).
39
+ await this.destroy();
40
+ this.sandbox = await this.sandboxFactory({
41
+ ...(opts.image ? { template: opts.image } : { template: this.template }),
42
+ envs: opts.env,
43
+ apiKey: this.apiKey,
44
+ timeoutMs: this.timeoutMs,
45
+ metadata: { name: opts.name },
46
+ });
47
+ try {
48
+ // The E2B `base` template does NOT ship tmux, and the default user is
49
+ // non-root (uid 1000, in the sudo group). Install with passwordless sudo
50
+ // so SandboxEnvironment.ensureSession can run tmux new-session.
51
+ const install = await this.execRaw("command -v tmux >/dev/null 2>&1 || (sudo -n apt-get update -y && sudo -n apt-get install -y tmux)");
52
+ if (install.code !== 0) {
53
+ throw new Error(`E2BSandboxProvider: failed to install tmux (exit ${install.code}): ${install.stderr || install.stdout}`);
54
+ }
55
+ const probe = await this.execRaw("command -v tmux");
56
+ if (probe.code !== 0) {
57
+ throw new Error("E2BSandboxProvider: tmux still missing after install");
58
+ }
59
+ }
60
+ catch (err) {
61
+ // Cloud sandbox is already billed/running — always kill on setup failure
62
+ // so a failed smoke/open cannot leave orphans on the E2B dashboard.
63
+ await this.destroy();
64
+ throw err;
65
+ }
66
+ }
67
+ async exec(args) {
68
+ return this.execRaw(args.map(shellQuote).join(" "));
69
+ }
70
+ /** Run a raw shell command string; map CommandExitError → non-zero result. */
71
+ async execRaw(cmd) {
72
+ if (!this.sandbox) {
73
+ throw new Error("E2BSandboxProvider.exec called before ensure");
74
+ }
75
+ try {
76
+ const r = await this.sandbox.commands.run(cmd, { timeoutMs: this.timeoutMs });
77
+ return { stdout: r.stdout, stderr: r.stderr, code: r.exitCode };
78
+ }
79
+ catch (err) {
80
+ const e = err;
81
+ if (typeof e.exitCode === "number") {
82
+ return { stdout: e.stdout ?? "", stderr: e.stderr ?? "", code: e.exitCode };
83
+ }
84
+ throw err;
85
+ }
86
+ }
87
+ async destroy() {
88
+ const sb = this.sandbox;
89
+ this.sandbox = undefined;
90
+ if (!sb)
91
+ return;
92
+ const id = sb.sandboxId;
93
+ // Prefer instance kill, then static Sandbox.kill as a belt-and-suspenders
94
+ // path so a hung instance handle cannot leave a dashboard orphan.
95
+ try {
96
+ await sb.kill();
97
+ }
98
+ catch {
99
+ /* try static kill below */
100
+ }
101
+ if (id) {
102
+ try {
103
+ await Sandbox.kill(id, this.apiKey ? { apiKey: this.apiKey } : undefined);
104
+ }
105
+ catch {
106
+ // Swallow: destroy must never throw (mirrors SandboxEnvironment.destroySession).
107
+ }
108
+ }
109
+ }
110
+ /** Cloud sandbox id if one is currently provisioned (for smoke/logging). */
111
+ get sandboxId() {
112
+ return this.sandbox?.sandboxId;
113
+ }
114
+ }
@@ -0,0 +1,8 @@
1
+ import type { SandboxProvider } from "@termbridge/core";
2
+ /**
3
+ * Returns an `E2BSandboxProvider` when `E2B_API_KEY` is set (or `apiKey` is
4
+ * passed), otherwise `undefined`. Never throws for a missing key.
5
+ */
6
+ export declare function sandboxProviderFromEnv(opts?: {
7
+ apiKey?: string;
8
+ }): SandboxProvider | undefined;
@@ -0,0 +1,11 @@
1
+ import { E2BSandboxProvider } from "./e2b-provider.js";
2
+ /**
3
+ * Returns an `E2BSandboxProvider` when `E2B_API_KEY` is set (or `apiKey` is
4
+ * passed), otherwise `undefined`. Never throws for a missing key.
5
+ */
6
+ export function sandboxProviderFromEnv(opts) {
7
+ const apiKey = opts?.apiKey ?? process.env.E2B_API_KEY;
8
+ if (!apiKey)
9
+ return undefined;
10
+ return new E2BSandboxProvider({ apiKey });
11
+ }
@@ -0,0 +1,2 @@
1
+ export { E2BSandboxProvider, type E2BSandboxProviderOptions } from "./e2b-provider.js";
2
+ export { sandboxProviderFromEnv } from "./from-env.js";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ // Public barrel for @termbridge/sandbox-e2b — a concrete SandboxProvider backed
2
+ // by the E2B cloud sandbox SDK. @termbridge/core stays dependency-free (D3);
3
+ // this package owns the only `e2b` import.
4
+ export { E2BSandboxProvider } from "./e2b-provider.js";
5
+ export { sandboxProviderFromEnv } from "./from-env.js";
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@termbridge/sandbox-e2b",
3
+ "version": "1.0.7",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "description": "E2B SandboxProvider for termbridge — env:\"sandbox\" isolation via the E2B cloud SDK",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "scripts": {
17
+ "build": "tsc -p tsconfig.build.json",
18
+ "test": "bun test",
19
+ "typecheck": "tsc --noEmit",
20
+ "lint": "biome check ."
21
+ },
22
+ "dependencies": {
23
+ "@termbridge/core": "^1.0.7",
24
+ "e2b": "^2.32.0"
25
+ },
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts"
28
+ }