zidane 5.10.13 → 5.11.1
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 +31 -5
- package/dist/{agent-BHkvYIH9.d.ts → agent-D0W9yClt.d.ts} +114 -27
- package/dist/agent-D0W9yClt.d.ts.map +1 -0
- package/dist/chat/pure.d.ts +3 -3
- package/dist/chat.d.ts +7 -7
- package/dist/chat.js +2 -2
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +53 -14
- package/dist/contexts/docker.js.map +1 -1
- package/dist/contexts/e2b.d.ts +168 -0
- package/dist/contexts/e2b.d.ts.map +1 -0
- package/dist/contexts/e2b.js +261 -0
- package/dist/contexts/e2b.js.map +1 -0
- package/dist/{contexts-BJVgG0LY.js → contexts-DglWSzmR.js} +59 -9
- package/dist/contexts-DglWSzmR.js.map +1 -0
- package/dist/contexts.d.ts +3 -3
- package/dist/contexts.js +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +5 -5
- package/dist/eval.js.map +1 -1
- package/dist/{headless-CPaunZsU.js → headless-Bb5gU8AR.js} +6 -6
- package/dist/{headless-CPaunZsU.js.map → headless-Bb5gU8AR.js.map} +1 -1
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +1 -1
- package/dist/{index-C_t8tW_X.d.ts → index-CrMb8jCE.d.ts} +2 -2
- package/dist/{index-C_t8tW_X.d.ts.map → index-CrMb8jCE.d.ts.map} +1 -1
- package/dist/{index-BIo67xLV.d.ts → index-D60tX5XC.d.ts} +10 -3
- package/dist/index-D60tX5XC.d.ts.map +1 -0
- package/dist/{index-C4aT2kO_.d.ts → index-DZR99FD4.d.ts} +30 -111
- package/dist/index-DZR99FD4.d.ts.map +1 -0
- package/dist/index.d.ts +7 -6
- package/dist/index.js +11 -10
- package/dist/index.js.map +1 -1
- package/dist/{interpolate-Dy7Lunvg.js → interpolate-CTfr0GdR.js} +19 -1
- package/dist/{interpolate-Dy7Lunvg.js.map → interpolate-CTfr0GdR.js.map} +1 -1
- package/dist/logger-Ktm-lj1s.js +300 -0
- package/dist/logger-Ktm-lj1s.js.map +1 -0
- package/dist/logger-n4LsLISE.d.ts +102 -0
- package/dist/logger-n4LsLISE.d.ts.map +1 -0
- package/dist/{login-0jP1pnSJ.js → login-BHhOdTp9.js} +4 -301
- package/dist/login-BHhOdTp9.js.map +1 -0
- package/dist/{mcp-tevNihk_.js → mcp-Cy9mgCcr.js} +22 -9
- package/dist/mcp-Cy9mgCcr.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{messages-C_1AmSpk.js → messages-RPKrEPvH.js} +6 -2
- package/dist/messages-RPKrEPvH.js.map +1 -0
- package/dist/output/stream-json.d.ts +2 -2
- package/dist/output/stream-json.js +1 -1
- package/dist/output/terminal.d.ts +2 -2
- package/dist/output/terminal.js +1 -0
- package/dist/output/terminal.js.map +1 -1
- package/dist/{presets-Cm2BPJaU.js → presets-D5ibZTml.js} +2 -2
- package/dist/{presets-Cm2BPJaU.js.map → presets-D5ibZTml.js.map} +1 -1
- package/dist/presets.d.ts +2 -2
- package/dist/presets.js +1 -1
- package/dist/{providers-BGBB18zz.js → providers-C2cxujp_.js} +85 -20
- package/dist/providers-C2cxujp_.js.map +1 -0
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +2 -2
- package/dist/restate.d.ts +2 -2
- package/dist/restate.js +4 -1
- package/dist/restate.js.map +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/session/sqlite.d.ts.map +1 -1
- package/dist/session/sqlite.js +36 -4
- package/dist/session/sqlite.js.map +1 -1
- package/dist/{session-CtAWwwkn.js → session-Do_TQV7c.js} +70 -22
- package/dist/session-Do_TQV7c.js.map +1 -0
- package/dist/session.d.ts +2 -2
- package/dist/session.js +3 -3
- package/dist/shell-quote-BmnhZmdM.js +33 -0
- package/dist/shell-quote-BmnhZmdM.js.map +1 -0
- package/dist/skills.d.ts +3 -3
- package/dist/skills.js +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/{tool-formatters-D_fX6FGl.d.ts → tool-formatters-RT5-gyE2.d.ts} +2 -2
- package/dist/{tool-formatters-D_fX6FGl.d.ts.map → tool-formatters-RT5-gyE2.d.ts.map} +1 -1
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/{tools-NxnEmzYg.js → tools-ZHKOh44k.js} +342 -123
- package/dist/tools-ZHKOh44k.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-DA6XawEU.d.ts → transcript-anchors-B4FxkG-8.d.ts} +10 -4
- package/dist/transcript-anchors-B4FxkG-8.d.ts.map +1 -0
- package/dist/{transcript-anchors-B_c7gWot.js → transcript-anchors-CS46ul6X.js} +10 -10
- package/dist/transcript-anchors-CS46ul6X.js.map +1 -0
- package/dist/tui.d.ts +3 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +167 -41
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-CCl7rpbT.d.ts → turn-operations-CoRj3mYZ.d.ts} +3 -3
- package/dist/{turn-operations-CCl7rpbT.d.ts.map → turn-operations-CoRj3mYZ.d.ts.map} +1 -1
- package/dist/{types-BibzMDjX.d.ts → types-B39tBba1.d.ts} +69 -2
- package/dist/types-B39tBba1.d.ts.map +1 -0
- package/dist/types-BiobHM1D.js.map +1 -1
- package/dist/types.d.ts +5 -5
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CHAT.md +3 -3
- package/docs/EXECUTION_CONTEXT.md +257 -0
- package/docs/RUN_IN_BACKGROUND.md +8 -0
- package/docs/SKILL.md +3 -3
- package/package.json +57 -24
- package/dist/agent-BHkvYIH9.d.ts.map +0 -1
- package/dist/contexts-BJVgG0LY.js.map +0 -1
- package/dist/index-BIo67xLV.d.ts.map +0 -1
- package/dist/index-C4aT2kO_.d.ts.map +0 -1
- package/dist/login-0jP1pnSJ.js.map +0 -1
- package/dist/mcp-tevNihk_.js.map +0 -1
- package/dist/messages-C_1AmSpk.js.map +0 -1
- package/dist/providers-BGBB18zz.js.map +0 -1
- package/dist/session-CtAWwwkn.js.map +0 -1
- package/dist/tools-NxnEmzYg.js.map +0 -1
- package/dist/transcript-anchors-B_c7gWot.js.map +0 -1
- package/dist/transcript-anchors-DA6XawEU.d.ts.map +0 -1
- package/dist/types-BibzMDjX.d.ts.map +0 -1
package/dist/contexts/docker.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import { t as alwaysQuote } from "../shell-quote-BmnhZmdM.js";
|
|
1
2
|
//#region src/contexts/docker.ts
|
|
2
3
|
const SINGLE_QUOTE_RE = /'/g;
|
|
4
|
+
/**
|
|
5
|
+
* dockerode surfaces HTTP errors with a `statusCode`. 304 = container
|
|
6
|
+
* already stopped, 404 = already gone — both are fine during teardown.
|
|
7
|
+
*/
|
|
8
|
+
function isGoneOrAlreadyStopped(err) {
|
|
9
|
+
const code = err?.statusCode;
|
|
10
|
+
return code === 304 || code === 404;
|
|
11
|
+
}
|
|
3
12
|
function createDockerContext(config) {
|
|
4
13
|
let counter = 0;
|
|
5
14
|
const containers = /* @__PURE__ */ new Map();
|
|
@@ -51,6 +60,14 @@ function createDockerContext(config) {
|
|
|
51
60
|
const hostConfig = {};
|
|
52
61
|
if (limits?.memory) hostConfig.Memory = limits.memory * 1024 * 1024;
|
|
53
62
|
if (limits?.cpu) hostConfig.NanoCpus = Number.parseFloat(limits.cpu) * 1e9;
|
|
63
|
+
const hardening = {
|
|
64
|
+
...config?.hardening,
|
|
65
|
+
...overrides?.hardening
|
|
66
|
+
};
|
|
67
|
+
if (hardening.dropAllCapabilities) hostConfig.CapDrop = ["ALL"];
|
|
68
|
+
if (hardening.noNewPrivileges) hostConfig.SecurityOpt = ["no-new-privileges"];
|
|
69
|
+
if (hardening.readonlyRootfs) hostConfig.ReadonlyRootfs = true;
|
|
70
|
+
if (hardening.pidsLimit != null) hostConfig.PidsLimit = hardening.pidsLimit;
|
|
54
71
|
const env = {
|
|
55
72
|
...defaultEnv,
|
|
56
73
|
...overrides?.env
|
|
@@ -86,7 +103,12 @@ function createDockerContext(config) {
|
|
|
86
103
|
...user ? { User: user } : {},
|
|
87
104
|
...Object.keys(labels).length ? { Labels: labels } : {}
|
|
88
105
|
});
|
|
89
|
-
|
|
106
|
+
try {
|
|
107
|
+
await container.start();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
await container.remove({ force: true }).catch(() => {});
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
90
112
|
const handle = {
|
|
91
113
|
id,
|
|
92
114
|
type: "docker",
|
|
@@ -104,8 +126,11 @@ function createDockerContext(config) {
|
|
|
104
126
|
if (!ref) throw new Error(`Container ${handle.id} not found`);
|
|
105
127
|
const execCwd = options?.cwd ?? handle.cwd;
|
|
106
128
|
const env = options?.env ? Object.entries(options.env).map(([k, v]) => `${k}=${v}`) : [];
|
|
129
|
+
const timeout = options?.timeout ?? defaultLimits?.timeout ?? 30;
|
|
107
130
|
const exec = await ref.container.exec({
|
|
108
131
|
Cmd: [
|
|
132
|
+
"timeout",
|
|
133
|
+
String(timeout),
|
|
109
134
|
"sh",
|
|
110
135
|
"-c",
|
|
111
136
|
command
|
|
@@ -151,7 +176,6 @@ function createDockerContext(config) {
|
|
|
151
176
|
stdout += chunk.toString("utf-8");
|
|
152
177
|
});
|
|
153
178
|
}
|
|
154
|
-
const timeout = options?.timeout ?? defaultLimits?.timeout ?? 30;
|
|
155
179
|
const timer = setTimeout(() => {
|
|
156
180
|
if (resolved) return;
|
|
157
181
|
resolved = true;
|
|
@@ -163,18 +187,26 @@ function createDockerContext(config) {
|
|
|
163
187
|
stderr: stderr ? `${stderr}\n[timeout]` : "[timeout]",
|
|
164
188
|
exitCode: 124
|
|
165
189
|
});
|
|
166
|
-
}, timeout * 1e3);
|
|
190
|
+
}, timeout * 1e3 + 1e3);
|
|
167
191
|
stream.on("end", async () => {
|
|
168
192
|
if (resolved) return;
|
|
169
193
|
resolved = true;
|
|
170
194
|
clearTimeout(timer);
|
|
171
|
-
let exitCode
|
|
195
|
+
let exitCode;
|
|
196
|
+
let inspectNote = "";
|
|
172
197
|
try {
|
|
173
|
-
|
|
174
|
-
|
|
198
|
+
const inspect = await exec.inspect();
|
|
199
|
+
if (inspect.ExitCode == null) {
|
|
200
|
+
exitCode = -1;
|
|
201
|
+
inspectNote = "[exit code unavailable: exec still marked running by daemon]";
|
|
202
|
+
} else exitCode = inspect.ExitCode;
|
|
203
|
+
} catch (err) {
|
|
204
|
+
exitCode = -1;
|
|
205
|
+
inspectNote = `[exit code unavailable: exec inspect failed: ${err?.message ?? String(err)}]`;
|
|
206
|
+
}
|
|
175
207
|
resolve({
|
|
176
208
|
stdout,
|
|
177
|
-
stderr,
|
|
209
|
+
stderr: inspectNote ? stderr ? `${stderr}\n${inspectNote}` : inspectNote : stderr,
|
|
178
210
|
exitCode
|
|
179
211
|
});
|
|
180
212
|
});
|
|
@@ -191,17 +223,17 @@ function createDockerContext(config) {
|
|
|
191
223
|
});
|
|
192
224
|
},
|
|
193
225
|
async readFile(handle, path) {
|
|
194
|
-
const result = await ctx.exec(handle, `cat ${
|
|
226
|
+
const result = await ctx.exec(handle, `cat ${alwaysQuote(path)}`);
|
|
195
227
|
if (result.exitCode !== 0) throw new Error(`Failed to read file: ${result.stderr}`);
|
|
196
228
|
return result.stdout;
|
|
197
229
|
},
|
|
198
230
|
async writeFile(handle, path, content) {
|
|
199
231
|
const escaped = content.replace(SINGLE_QUOTE_RE, String.raw`'\''`);
|
|
200
|
-
const result = await ctx.exec(handle, `mkdir -p "$(dirname ${
|
|
232
|
+
const result = await ctx.exec(handle, `mkdir -p "$(dirname ${alwaysQuote(path)})" && printf '%s' '${escaped}' > ${alwaysQuote(path)}`);
|
|
201
233
|
if (result.exitCode !== 0) throw new Error(`Failed to write file: ${result.stderr}`);
|
|
202
234
|
},
|
|
203
235
|
async listFiles(handle, path) {
|
|
204
|
-
const result = await ctx.exec(handle, `ls -1 ${
|
|
236
|
+
const result = await ctx.exec(handle, `ls -1 ${alwaysQuote(path)}`);
|
|
205
237
|
if (result.exitCode !== 0) return [];
|
|
206
238
|
return result.stdout.trim().split("\n").filter(Boolean);
|
|
207
239
|
},
|
|
@@ -219,10 +251,17 @@ function createDockerContext(config) {
|
|
|
219
251
|
const ref = containers.get(handle.id);
|
|
220
252
|
if (!ref) return;
|
|
221
253
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
254
|
+
try {
|
|
255
|
+
await ref.container.stop({ t: 5 });
|
|
256
|
+
} catch {}
|
|
257
|
+
try {
|
|
258
|
+
await ref.container.remove({ force: true });
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (!isGoneOrAlreadyStopped(err)) throw err;
|
|
261
|
+
}
|
|
262
|
+
} finally {
|
|
263
|
+
containers.delete(handle.id);
|
|
264
|
+
}
|
|
226
265
|
}
|
|
227
266
|
};
|
|
228
267
|
return ctx;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docker.js","names":[],"sources":["../../src/contexts/docker.ts"],"sourcesContent":["/**\n * Docker execution context.\n *\n * Runs tools inside a Docker container via dockerode.\n * Full isolation with configurable resource limits.\n *\n * Requires `dockerode` as an optional peer dependency.\n */\n\nimport type { Buffer } from 'node:buffer'\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, SpawnConfig } from './types'\n\nconst SINGLE_QUOTE_RE = /'/g\n\ninterface ContainerRef {\n handle: ExecutionHandle\n container: any\n docker: any\n}\n\nexport function createDockerContext(config?: SpawnConfig): ExecutionContext {\n let counter = 0\n const containers = new Map<string, ContainerRef>()\n const defaultImage = config?.image ?? 'oven/bun:latest'\n const defaultCwd = config?.cwd ?? '/workspace'\n const defaultEnv = config?.env\n const defaultLimits = config?.limits\n const defaultMounts = config?.mounts ?? []\n const defaultName = config?.name\n\n async function getDockerode() {\n try {\n const Dockerode = (await import('dockerode')).default\n return new Dockerode()\n }\n catch {\n throw new Error('dockerode is required for Docker execution context. Install it with: bun add dockerode')\n }\n }\n\n const ctx: ExecutionContext = {\n type: 'docker',\n\n capabilities: {\n shell: true,\n filesystem: true,\n network: true,\n gpu: false,\n } satisfies ContextCapabilities,\n\n async spawn(overrides?: SpawnConfig): Promise<ExecutionHandle> {\n const docker = await getDockerode()\n const id = `docker-${++counter}`\n const image = overrides?.image ?? defaultImage\n const cwd = overrides?.cwd ?? defaultCwd\n const mounts = [...defaultMounts, ...(overrides?.mounts ?? [])]\n const name = overrides?.name ?? defaultName\n\n // Pull image if not available\n try {\n await docker.getImage(image).inspect()\n }\n catch {\n await new Promise<void>((resolve, reject) => {\n docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => {\n if (err)\n return reject(err)\n docker.modem.followProgress(stream, (err2: Error | null) => {\n err2 ? reject(err2) : resolve()\n })\n })\n })\n }\n\n const limits = { ...defaultLimits, ...overrides?.limits }\n const hostConfig: Record<string, unknown> = {}\n\n if (limits?.memory) {\n hostConfig.Memory = limits.memory * 1024 * 1024\n }\n if (limits?.cpu) {\n hostConfig.NanoCpus = Number.parseFloat(limits.cpu) * 1e9\n }\n\n const env = { ...defaultEnv, ...overrides?.env }\n\n const bindSpecs = buildBindSpecs(mounts)\n if (bindSpecs.length > 0) {\n hostConfig.Binds = bindSpecs\n }\n\n const portSpec = overrides?.ports ?? config?.ports\n const exposedPorts: Record<string, Record<string, never>> = {}\n const portBindings: Record<string, Array<{ HostPort: string }>> = {}\n\n if (portSpec?.length) {\n for (const p of portSpec) {\n const key = `${p.container}/${p.proto ?? 'tcp'}`\n exposedPorts[key] = {}\n portBindings[key] = [{ HostPort: p.host == null ? '' : String(p.host) }]\n }\n hostConfig.PortBindings = portBindings\n }\n\n const user = overrides?.user ?? config?.user\n const network = overrides?.network ?? config?.network\n if (network) {\n hostConfig.NetworkMode = network\n }\n\n const labels = { ...config?.labels, ...overrides?.labels }\n\n const container = await docker.createContainer({\n // Suffix with a short random token so a crashed run's leftover container\n // (same prefix) never blocks a fresh one with HTTP 409 name conflict.\n ...(name ? { name: `${name}-${counter}-${Math.random().toString(36).slice(2, 8)}` } : {}),\n Image: image,\n Cmd: ['sleep', 'infinity'],\n WorkingDir: cwd,\n Env: Object.entries(env).map(([k, v]) => `${k}=${v}`),\n HostConfig: hostConfig,\n ...(Object.keys(exposedPorts).length ? { ExposedPorts: exposedPorts } : {}),\n ...(user ? { User: user } : {}),\n ...(Object.keys(labels).length ? { Labels: labels } : {}),\n })\n\n await container.start()\n\n const handle: ExecutionHandle = { id, type: 'docker', cwd }\n containers.set(id, { handle, container, docker })\n\n return handle\n },\n\n async exec(handle: ExecutionHandle, command: string, options?: { cwd?: string, env?: Record<string, string>, timeout?: number }): Promise<ExecResult> {\n const ref = containers.get(handle.id)\n if (!ref)\n throw new Error(`Container ${handle.id} not found`)\n\n const execCwd = options?.cwd ?? handle.cwd\n const env = options?.env\n ? Object.entries(options.env).map(([k, v]) => `${k}=${v}`)\n : []\n\n const exec = await ref.container.exec({\n Cmd: ['sh', '-c', command],\n WorkingDir: execCwd,\n Env: env,\n AttachStdout: true,\n AttachStderr: true,\n })\n\n const stream = await exec.start({ Detach: false })\n\n return new Promise<ExecResult>((resolve) => {\n let stdout = ''\n let stderr = ''\n let resolved = false\n\n // Docker's exec stream is a multiplexed protocol — a single byte\n // stream that interleaves stdout/stderr frames. `demuxStream`\n // splits the two onto separate writable sinks. Without it, every\n // byte landed on `stdout` and `stderr` was permanently empty.\n const stdoutSink = {\n write(chunk: Buffer | string) {\n stdout += typeof chunk === 'string' ? chunk : chunk.toString('utf-8')\n return true\n },\n end() {},\n on() {},\n once() {},\n emit() { return true },\n }\n const stderrSink = {\n write(chunk: Buffer | string) {\n stderr += typeof chunk === 'string' ? chunk : chunk.toString('utf-8')\n return true\n },\n end() {},\n on() {},\n once() {},\n emit() { return true },\n }\n try {\n ref.docker.modem.demuxStream(stream, stdoutSink, stderrSink)\n }\n catch {\n // Older / different transports may not be multiplexed (TTY mode);\n // fall back to a raw stdout reader so we don't lose output entirely.\n stream.on('data', (chunk: Buffer) => {\n stdout += chunk.toString('utf-8')\n })\n }\n\n const timeout = options?.timeout ?? defaultLimits?.timeout ?? 30\n const timer = setTimeout(() => {\n if (resolved)\n return\n resolved = true\n // Best-effort: detach the stream so we stop accumulating bytes\n // from a runaway exec. The exec itself can't be killed cleanly\n // through the dockerode exec API; the container's destroy() path\n // (or a manual `docker exec kill`) is the real cleanup.\n try { stream.destroy?.() }\n catch { /* swallow */ }\n resolve({ stdout, stderr: stderr ? `${stderr}\\n[timeout]` : '[timeout]', exitCode: 124 })\n }, timeout * 1000)\n\n stream.on('end', async () => {\n if (resolved)\n return\n resolved = true\n clearTimeout(timer)\n let exitCode = 0\n try {\n const inspect = await exec.inspect()\n exitCode = inspect.ExitCode ?? 0\n }\n catch {\n // If inspect fails after a clean stream end, treat as success\n // — we already received the full output stream.\n }\n resolve({ stdout, stderr, exitCode })\n })\n\n stream.on('error', (err: Error) => {\n if (resolved)\n return\n resolved = true\n clearTimeout(timer)\n resolve({ stdout, stderr: stderr ? `${stderr}\\n${err.message}` : err.message, exitCode: 1 })\n })\n })\n },\n\n async readFile(handle: ExecutionHandle, path: string): Promise<string> {\n const result = await ctx.exec(handle, `cat ${JSON.stringify(path)}`)\n if (result.exitCode !== 0)\n throw new Error(`Failed to read file: ${result.stderr}`)\n return result.stdout\n },\n\n async writeFile(handle: ExecutionHandle, path: string, content: string): Promise<void> {\n const escaped = content.replace(SINGLE_QUOTE_RE, String.raw`'\\''`)\n const result = await ctx.exec(handle, `mkdir -p \"$(dirname ${JSON.stringify(path)})\" && printf '%s' '${escaped}' > ${JSON.stringify(path)}`)\n if (result.exitCode !== 0)\n throw new Error(`Failed to write file: ${result.stderr}`)\n },\n\n async listFiles(handle: ExecutionHandle, path: string): Promise<string[]> {\n const result = await ctx.exec(handle, `ls -1 ${JSON.stringify(path)}`)\n if (result.exitCode !== 0)\n return []\n return result.stdout.trim().split('\\n').filter(Boolean)\n },\n\n async getMappedPort(handle: ExecutionHandle, containerPort: number): Promise<number | null> {\n const ref = containers.get(handle.id)\n if (!ref)\n return null\n\n const info = await ref.container.inspect()\n const bindings = info?.NetworkSettings?.Ports ?? {}\n\n for (const proto of ['tcp', 'udp']) {\n const list = bindings[`${containerPort}/${proto}`]\n if (Array.isArray(list) && list.length > 0 && list[0]?.HostPort) {\n return Number.parseInt(list[0].HostPort, 10)\n }\n }\n\n return null\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n const ref = containers.get(handle.id)\n if (!ref)\n return\n\n try {\n await ref.container.stop({ t: 5 })\n await ref.container.remove({ force: true })\n }\n catch {\n // Container may already be stopped\n }\n\n containers.delete(handle.id)\n },\n }\n\n return ctx\n}\n\n/**\n * Format one {@link ContextMount} as a dockerode `HostConfig.Binds` entry.\n * Supports read-only (`:ro`) and SELinux shared-label (`:z`) options; the two\n * are mutually exclusive (a read-only mount can't also be a shared rw mount).\n */\nfunction formatBindMount(mount: NonNullable<SpawnConfig['mounts']>[number]): string {\n if (!mount.source)\n throw new Error('Docker mount source is required')\n if (!mount.target || !mount.target.startsWith('/'))\n throw new Error(`Docker mount target must be an absolute path: ${mount.target}`)\n if (mount.readonly && mount.shared)\n throw new Error(`Docker mount cannot be both readonly and shared: ${mount.target}`)\n const opts = mount.readonly ? ':ro' : mount.shared ? ':z' : ''\n return `${mount.source}:${mount.target}${opts}`\n}\n\n/** Assemble dockerode `HostConfig.Binds` from the context's `mounts`. */\nexport function buildBindSpecs(mounts: NonNullable<SpawnConfig['mounts']>): string[] {\n return mounts.map(formatBindMount)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"docker.js","names":[],"sources":["../../src/contexts/docker.ts"],"sourcesContent":["/**\n * Docker execution context.\n *\n * Runs tools inside a Docker container via dockerode.\n * Full isolation with configurable resource limits.\n *\n * Requires `dockerode` as an optional peer dependency.\n */\n\nimport type { Buffer } from 'node:buffer'\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, SpawnConfig } from './types'\nimport { alwaysQuote } from '../tools/shell-quote'\n\nconst SINGLE_QUOTE_RE = /'/g\n\n/**\n * dockerode surfaces HTTP errors with a `statusCode`. 304 = container\n * already stopped, 404 = already gone — both are fine during teardown.\n */\nfunction isGoneOrAlreadyStopped(err: unknown): boolean {\n const code = (err as { statusCode?: number } | null)?.statusCode\n return code === 304 || code === 404\n}\n\ninterface ContainerRef {\n handle: ExecutionHandle\n container: any\n docker: any\n}\n\nexport function createDockerContext(config?: SpawnConfig): ExecutionContext {\n let counter = 0\n const containers = new Map<string, ContainerRef>()\n const defaultImage = config?.image ?? 'oven/bun:latest'\n const defaultCwd = config?.cwd ?? '/workspace'\n const defaultEnv = config?.env\n const defaultLimits = config?.limits\n const defaultMounts = config?.mounts ?? []\n const defaultName = config?.name\n\n async function getDockerode() {\n try {\n const Dockerode = (await import('dockerode')).default\n return new Dockerode()\n }\n catch {\n throw new Error('dockerode is required for Docker execution context. Install it with: bun add dockerode')\n }\n }\n\n const ctx: ExecutionContext = {\n type: 'docker',\n\n capabilities: {\n shell: true,\n filesystem: true,\n network: true,\n gpu: false,\n } satisfies ContextCapabilities,\n\n async spawn(overrides?: SpawnConfig): Promise<ExecutionHandle> {\n const docker = await getDockerode()\n const id = `docker-${++counter}`\n const image = overrides?.image ?? defaultImage\n const cwd = overrides?.cwd ?? defaultCwd\n const mounts = [...defaultMounts, ...(overrides?.mounts ?? [])]\n const name = overrides?.name ?? defaultName\n\n // Pull image if not available\n try {\n await docker.getImage(image).inspect()\n }\n catch {\n await new Promise<void>((resolve, reject) => {\n docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => {\n if (err)\n return reject(err)\n docker.modem.followProgress(stream, (err2: Error | null) => {\n err2 ? reject(err2) : resolve()\n })\n })\n })\n }\n\n const limits = { ...defaultLimits, ...overrides?.limits }\n const hostConfig: Record<string, unknown> = {}\n\n if (limits?.memory) {\n hostConfig.Memory = limits.memory * 1024 * 1024\n }\n if (limits?.cpu) {\n hostConfig.NanoCpus = Number.parseFloat(limits.cpu) * 1e9\n }\n\n // Opt-in hardening (all OFF by default — see ContextHardening).\n const hardening = { ...config?.hardening, ...overrides?.hardening }\n if (hardening.dropAllCapabilities)\n hostConfig.CapDrop = ['ALL']\n if (hardening.noNewPrivileges)\n hostConfig.SecurityOpt = ['no-new-privileges']\n if (hardening.readonlyRootfs)\n hostConfig.ReadonlyRootfs = true\n if (hardening.pidsLimit != null)\n hostConfig.PidsLimit = hardening.pidsLimit\n\n const env = { ...defaultEnv, ...overrides?.env }\n\n const bindSpecs = buildBindSpecs(mounts)\n if (bindSpecs.length > 0) {\n hostConfig.Binds = bindSpecs\n }\n\n const portSpec = overrides?.ports ?? config?.ports\n const exposedPorts: Record<string, Record<string, never>> = {}\n const portBindings: Record<string, Array<{ HostPort: string }>> = {}\n\n if (portSpec?.length) {\n for (const p of portSpec) {\n const key = `${p.container}/${p.proto ?? 'tcp'}`\n exposedPorts[key] = {}\n portBindings[key] = [{ HostPort: p.host == null ? '' : String(p.host) }]\n }\n hostConfig.PortBindings = portBindings\n }\n\n const user = overrides?.user ?? config?.user\n const network = overrides?.network ?? config?.network\n if (network) {\n hostConfig.NetworkMode = network\n }\n\n const labels = { ...config?.labels, ...overrides?.labels }\n\n const container = await docker.createContainer({\n // Suffix with a short random token so a crashed run's leftover container\n // (same prefix) never blocks a fresh one with HTTP 409 name conflict.\n ...(name ? { name: `${name}-${counter}-${Math.random().toString(36).slice(2, 8)}` } : {}),\n Image: image,\n Cmd: ['sleep', 'infinity'],\n WorkingDir: cwd,\n Env: Object.entries(env).map(([k, v]) => `${k}=${v}`),\n HostConfig: hostConfig,\n ...(Object.keys(exposedPorts).length ? { ExposedPorts: exposedPorts } : {}),\n ...(user ? { User: user } : {}),\n ...(Object.keys(labels).length ? { Labels: labels } : {}),\n })\n\n try {\n await container.start()\n }\n catch (err) {\n // The container was created but never started — remove it so a\n // failed spawn doesn't leak a stopped container on the daemon.\n await container.remove({ force: true }).catch(() => {})\n throw err\n }\n\n const handle: ExecutionHandle = { id, type: 'docker', cwd }\n containers.set(id, { handle, container, docker })\n\n return handle\n },\n\n async exec(handle: ExecutionHandle, command: string, options?: { cwd?: string, env?: Record<string, string>, timeout?: number }): Promise<ExecResult> {\n const ref = containers.get(handle.id)\n if (!ref)\n throw new Error(`Container ${handle.id} not found`)\n\n const execCwd = options?.cwd ?? handle.cwd\n const env = options?.env\n ? Object.entries(options.env).map(([k, v]) => `${k}=${v}`)\n : []\n\n const timeout = options?.timeout ?? defaultLimits?.timeout ?? 30\n\n const exec = await ref.container.exec({\n // Wrap with coreutils `timeout` so a runaway command is killed\n // server-side too — the host-side timer below only detaches the\n // stream; without this the process would keep running in the\n // container until destroy(). Exec-array form avoids re-quoting the\n // command. `timeout` exits 124 on expiry, matching the host\n // sentinel. (Default image oven/bun is Debian-based → coreutils.)\n Cmd: ['timeout', String(timeout), 'sh', '-c', command],\n WorkingDir: execCwd,\n Env: env,\n AttachStdout: true,\n AttachStderr: true,\n })\n\n const stream = await exec.start({ Detach: false })\n\n return new Promise<ExecResult>((resolve) => {\n let stdout = ''\n let stderr = ''\n let resolved = false\n\n // Docker's exec stream is a multiplexed protocol — a single byte\n // stream that interleaves stdout/stderr frames. `demuxStream`\n // splits the two onto separate writable sinks. Without it, every\n // byte landed on `stdout` and `stderr` was permanently empty.\n const stdoutSink = {\n write(chunk: Buffer | string) {\n stdout += typeof chunk === 'string' ? chunk : chunk.toString('utf-8')\n return true\n },\n end() {},\n on() {},\n once() {},\n emit() { return true },\n }\n const stderrSink = {\n write(chunk: Buffer | string) {\n stderr += typeof chunk === 'string' ? chunk : chunk.toString('utf-8')\n return true\n },\n end() {},\n on() {},\n once() {},\n emit() { return true },\n }\n try {\n ref.docker.modem.demuxStream(stream, stdoutSink, stderrSink)\n }\n catch {\n // Older / different transports may not be multiplexed (TTY mode);\n // fall back to a raw stdout reader so we don't lose output entirely.\n stream.on('data', (chunk: Buffer) => {\n stdout += chunk.toString('utf-8')\n })\n }\n\n // Host-side fallback timer. The server-side `timeout` wrapper is\n // the primary kill mechanism; give it a grace second so the\n // stream-end path (which carries the real exit code) usually wins.\n const timer = setTimeout(() => {\n if (resolved)\n return\n resolved = true\n // Best-effort: detach the stream so we stop accumulating bytes.\n // The server-side `timeout` wrapper kills the process itself.\n try { stream.destroy?.() }\n catch { /* swallow */ }\n resolve({ stdout, stderr: stderr ? `${stderr}\\n[timeout]` : '[timeout]', exitCode: 124 })\n }, timeout * 1000 + 1000)\n\n stream.on('end', async () => {\n if (resolved)\n return\n resolved = true\n clearTimeout(timer)\n let exitCode: number\n let inspectNote = ''\n try {\n const inspect = await exec.inspect()\n // `ExitCode: null` means Docker hasn't recorded an exit yet —\n // the outcome is unknown, not success. Use a sentinel so\n // callers don't mistake it for a clean exit.\n if (inspect.ExitCode == null) {\n exitCode = -1\n inspectNote = '[exit code unavailable: exec still marked running by daemon]'\n }\n else {\n exitCode = inspect.ExitCode\n }\n }\n catch (err) {\n exitCode = -1\n inspectNote = `[exit code unavailable: exec inspect failed: ${(err as Error)?.message ?? String(err)}]`\n }\n resolve({\n stdout,\n stderr: inspectNote ? (stderr ? `${stderr}\\n${inspectNote}` : inspectNote) : stderr,\n exitCode,\n })\n })\n\n stream.on('error', (err: Error) => {\n if (resolved)\n return\n resolved = true\n clearTimeout(timer)\n resolve({ stdout, stderr: stderr ? `${stderr}\\n${err.message}` : err.message, exitCode: 1 })\n })\n })\n },\n\n // Paths are model-supplied — single-quote them (`alwaysQuote`) rather\n // than JSON.stringify: `$(…)` and backticks still expand inside the\n // double quotes JSON produces, which was a command-injection vector.\n async readFile(handle: ExecutionHandle, path: string): Promise<string> {\n const result = await ctx.exec(handle, `cat ${alwaysQuote(path)}`)\n if (result.exitCode !== 0)\n throw new Error(`Failed to read file: ${result.stderr}`)\n return result.stdout\n },\n\n async writeFile(handle: ExecutionHandle, path: string, content: string): Promise<void> {\n const escaped = content.replace(SINGLE_QUOTE_RE, String.raw`'\\''`)\n const result = await ctx.exec(handle, `mkdir -p \"$(dirname ${alwaysQuote(path)})\" && printf '%s' '${escaped}' > ${alwaysQuote(path)}`)\n if (result.exitCode !== 0)\n throw new Error(`Failed to write file: ${result.stderr}`)\n },\n\n async listFiles(handle: ExecutionHandle, path: string): Promise<string[]> {\n const result = await ctx.exec(handle, `ls -1 ${alwaysQuote(path)}`)\n if (result.exitCode !== 0)\n return []\n return result.stdout.trim().split('\\n').filter(Boolean)\n },\n\n async getMappedPort(handle: ExecutionHandle, containerPort: number): Promise<number | null> {\n const ref = containers.get(handle.id)\n if (!ref)\n return null\n\n const info = await ref.container.inspect()\n const bindings = info?.NetworkSettings?.Ports ?? {}\n\n for (const proto of ['tcp', 'udp']) {\n const list = bindings[`${containerPort}/${proto}`]\n if (Array.isArray(list) && list.length > 0 && list[0]?.HostPort) {\n return Number.parseInt(list[0].HostPort, 10)\n }\n }\n\n return null\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n const ref = containers.get(handle.id)\n if (!ref)\n return\n\n try {\n try {\n await ref.container.stop({ t: 5 })\n }\n catch {\n // 304 (already stopped) / 404 (already gone) are expected here.\n // Any other stop failure must not prevent the remove({force})\n // below — force removal also works on running containers.\n }\n try {\n await ref.container.remove({ force: true })\n }\n catch (err) {\n if (!isGoneOrAlreadyStopped(err))\n throw err\n }\n }\n finally {\n containers.delete(handle.id)\n }\n },\n }\n\n return ctx\n}\n\n/**\n * Format one {@link ContextMount} as a dockerode `HostConfig.Binds` entry.\n * Supports read-only (`:ro`) and SELinux shared-label (`:z`) options; the two\n * are mutually exclusive (a read-only mount can't also be a shared rw mount).\n */\nfunction formatBindMount(mount: NonNullable<SpawnConfig['mounts']>[number]): string {\n if (!mount.source)\n throw new Error('Docker mount source is required')\n if (!mount.target || !mount.target.startsWith('/'))\n throw new Error(`Docker mount target must be an absolute path: ${mount.target}`)\n if (mount.readonly && mount.shared)\n throw new Error(`Docker mount cannot be both readonly and shared: ${mount.target}`)\n const opts = mount.readonly ? ':ro' : mount.shared ? ':z' : ''\n return `${mount.source}:${mount.target}${opts}`\n}\n\n/** Assemble dockerode `HostConfig.Binds` from the context's `mounts`. */\nexport function buildBindSpecs(mounts: NonNullable<SpawnConfig['mounts']>): string[] {\n return mounts.map(formatBindMount)\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { t as SandboxProvider } from "../index-CrMb8jCE.js";
|
|
2
|
+
import { a as Logger } from "../logger-n4LsLISE.js";
|
|
3
|
+
|
|
4
|
+
//#region src/contexts/e2b.d.ts
|
|
5
|
+
interface E2BCommandResult {
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
exitCode: number;
|
|
9
|
+
}
|
|
10
|
+
interface E2BEntry {
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
interface E2BSandbox {
|
|
14
|
+
sandboxId: string;
|
|
15
|
+
commands: {
|
|
16
|
+
run: (cmd: string, opts?: {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
envs?: Record<string, string>;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
}) => Promise<E2BCommandResult>;
|
|
21
|
+
};
|
|
22
|
+
files: {
|
|
23
|
+
read: (path: string) => Promise<string>;
|
|
24
|
+
write: (path: string, data: string) => Promise<unknown>;
|
|
25
|
+
list: (path: string) => Promise<E2BEntry[]>;
|
|
26
|
+
};
|
|
27
|
+
kill: () => Promise<unknown>;
|
|
28
|
+
}
|
|
29
|
+
interface E2BProviderOptions {
|
|
30
|
+
/** E2B API key. Falls back to the `E2B_API_KEY` env var when omitted. */
|
|
31
|
+
apiKey?: string;
|
|
32
|
+
/**
|
|
33
|
+
* E2B API domain. Set this to point at a self-hosted / on-prem cluster
|
|
34
|
+
* (e.g. `e2b.my-company.internal`). Falls back to the `E2B_DOMAIN` env var,
|
|
35
|
+
* then to E2B's hosted default.
|
|
36
|
+
*/
|
|
37
|
+
domain?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Sandbox template id / name to launch. Falls back to the `E2B_TEMPLATE`
|
|
40
|
+
* env var, then to E2B's hosted `base` template. Note that `base` does not
|
|
41
|
+
* exist on self-hosted clusters — supply a template that does.
|
|
42
|
+
*/
|
|
43
|
+
template?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Connect to a pre-existing sandbox by id instead of creating a fresh one
|
|
46
|
+
* (E2B's `Sandbox.connect`). When set, `template` is ignored — you're
|
|
47
|
+
* attaching to a sandbox that already exists. The provider treats such a
|
|
48
|
+
* sandbox as externally owned: it is NOT killed on `destroy`, nor torn down
|
|
49
|
+
* if the readiness probe / pregame fails, since the caller manages its
|
|
50
|
+
* lifecycle. Per-spawn `SpawnConfig.sandbox.sandboxId` overrides this.
|
|
51
|
+
*/
|
|
52
|
+
sandboxId?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Default working directory, applied when a spawn doesn't set its own
|
|
55
|
+
* `SpawnConfig.cwd`. Created on spawn. Defaults to E2B's `/home/user`.
|
|
56
|
+
*/
|
|
57
|
+
cwd?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Sandbox lifetime in seconds before E2B auto-kills it. Defaults to the
|
|
60
|
+
* SDK default (300s). Per-spawn `SpawnConfig.limits.timeout` overrides this.
|
|
61
|
+
*/
|
|
62
|
+
timeoutSeconds?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Environment variables baked into every sandbox at create time (passed to
|
|
65
|
+
* E2B's `Sandbox.create({ envs })`), so every command run in the sandbox
|
|
66
|
+
* sees them. Per-spawn `SpawnConfig.env` is merged over these.
|
|
67
|
+
*/
|
|
68
|
+
env?: Record<string, string>;
|
|
69
|
+
/**
|
|
70
|
+
* Logger for provider-level lifecycle lines (notably the readiness wait).
|
|
71
|
+
* Defaults to a {@link consoleSink}-backed logger so the messages reach
|
|
72
|
+
* stderr without the caller wiring one up.
|
|
73
|
+
*/
|
|
74
|
+
logger?: Logger;
|
|
75
|
+
/**
|
|
76
|
+
* Total deadline, in seconds, for the post-create readiness probe (see
|
|
77
|
+
* {@link waitForE2BReady}). A freshly-created sandbox can return its id
|
|
78
|
+
* before `envd` accepts commands; `spawn` blocks until a trivial probe
|
|
79
|
+
* succeeds so the first real command never absorbs the cold start. Defaults
|
|
80
|
+
* to `60`. Set to `0` to disable the gate.
|
|
81
|
+
*/
|
|
82
|
+
readinessTimeoutSeconds?: number;
|
|
83
|
+
/**
|
|
84
|
+
* A setup script to upload and run once the sandbox is ready, before any
|
|
85
|
+
* prompting begins (see {@link runE2BPregame}). `name` is the basename used
|
|
86
|
+
* for the uploaded file; `content` is the script body. The script is run
|
|
87
|
+
* directly so its shebang selects the interpreter. A non-zero exit aborts
|
|
88
|
+
* `spawn` (and tears the sandbox down) — a broken setup must not silently
|
|
89
|
+
* hand back a half-provisioned sandbox.
|
|
90
|
+
*/
|
|
91
|
+
pregame?: {
|
|
92
|
+
name: string;
|
|
93
|
+
content: string;
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Upload a setup ("pregame") script into a ready sandbox and execute it.
|
|
98
|
+
*
|
|
99
|
+
* Writes the script to `/tmp/<name>`, then `chmod +x`'s and runs it directly
|
|
100
|
+
* (so a `#!/bin/bash` / `#!/usr/bin/env python3` shebang chooses the
|
|
101
|
+
* interpreter) in `cwd` with `envs` applied — the same env the sandbox was
|
|
102
|
+
* created with, so the script sees the run's `--env` / `--pass-env`. A
|
|
103
|
+
* generous {@link PREGAME_TIMEOUT_MS} ceiling is used so long `apt`/`pip`
|
|
104
|
+
* installs aren't killed by E2B's 60s command default. A non-zero exit
|
|
105
|
+
* (whether E2B returns it or raises on it) throws, so the caller can tear the
|
|
106
|
+
* sandbox down rather than prompt against a broken environment.
|
|
107
|
+
*/
|
|
108
|
+
declare function runE2BPregame(sandbox: E2BSandbox, logger: Logger, opts: {
|
|
109
|
+
name: string;
|
|
110
|
+
content: string;
|
|
111
|
+
cwd?: string;
|
|
112
|
+
envs?: Record<string, string>;
|
|
113
|
+
}): Promise<void>;
|
|
114
|
+
interface E2BReadinessOptions {
|
|
115
|
+
/** Total deadline in seconds. `<= 0` disables the probe entirely. */
|
|
116
|
+
timeoutSeconds: number;
|
|
117
|
+
/**
|
|
118
|
+
* Whether to kill the sandbox if it never becomes ready. Defaults to `true`
|
|
119
|
+
* (we created it, so a dead sandbox is ours to clean up). Pass `false` when
|
|
120
|
+
* attaching to an externally-owned sandbox — failing the probe shouldn't tear
|
|
121
|
+
* down a sandbox the caller is managing.
|
|
122
|
+
*/
|
|
123
|
+
killOnTimeout?: boolean;
|
|
124
|
+
/** Clock source — injectable so tests can drive the deadline deterministically. */
|
|
125
|
+
now?: () => number;
|
|
126
|
+
/** Sleep between probes — injectable so tests can advance the clock without waiting. */
|
|
127
|
+
sleep?: (ms: number) => Promise<void>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Block until an E2B sandbox is ready to run commands, or throw if it never
|
|
131
|
+
* becomes ready within the deadline.
|
|
132
|
+
*
|
|
133
|
+
* The E2B control plane can hand back a sandbox id before the sandbox's `envd`
|
|
134
|
+
* is accepting commands; the first real command then absorbs that cold start
|
|
135
|
+
* and may time out. This polls a trivial `true` command on a short backoff,
|
|
136
|
+
* logging a `waiting…` line up front and a `ready` line (with `elapsedMs`) on
|
|
137
|
+
* success. On deadline exhaustion it best-effort kills the sandbox and throws.
|
|
138
|
+
*
|
|
139
|
+
* `now`/`sleep` are injectable purely for deterministic tests; in production
|
|
140
|
+
* they default to `Date.now` and a real timer.
|
|
141
|
+
*/
|
|
142
|
+
declare function waitForE2BReady(sandbox: E2BSandbox, logger: Logger, opts: E2BReadinessOptions): Promise<void>;
|
|
143
|
+
/**
|
|
144
|
+
* Resolve the effective E2B template, highest precedence first: the per-spawn
|
|
145
|
+
* `SpawnConfig.sandbox.template`, then the factory `template` option, then the
|
|
146
|
+
* `E2B_TEMPLATE` env var. Returns `undefined` (→ SDK's `base` default) when
|
|
147
|
+
* none is set. The E2B SDK reads `E2B_API_KEY`/`E2B_DOMAIN` from the env but
|
|
148
|
+
* NOT `E2B_TEMPLATE`, so this provider has to honor it itself.
|
|
149
|
+
*/
|
|
150
|
+
declare function resolveE2BTemplate(perSpawnTemplate: string | undefined, optionTemplate: string | undefined, env?: Record<string, string | undefined>): string | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* Merge the factory-level {@link E2BProviderOptions.env} with the per-spawn
|
|
153
|
+
* `SpawnConfig.env`, per-spawn winning on key conflicts. Returns `undefined`
|
|
154
|
+
* when the merge is empty so callers can skip setting `envs` on the create
|
|
155
|
+
* options entirely (rather than handing the SDK an empty object).
|
|
156
|
+
*/
|
|
157
|
+
declare function resolveE2BEnv(optionEnv: Record<string, string> | undefined, perSpawnEnv: Record<string, string> | undefined): Record<string, string> | undefined;
|
|
158
|
+
/**
|
|
159
|
+
* Build a {@link SandboxProvider} backed by E2B.
|
|
160
|
+
*
|
|
161
|
+
* The provider owns a registry of live {@link E2BSandbox} instances keyed by
|
|
162
|
+
* sandbox id, so the per-call `exec`/`readFile`/… hooks reuse the same warm
|
|
163
|
+
* connection rather than reconnecting each time.
|
|
164
|
+
*/
|
|
165
|
+
declare function createE2BProvider(options?: E2BProviderOptions): SandboxProvider;
|
|
166
|
+
//#endregion
|
|
167
|
+
export { E2BProviderOptions, E2BReadinessOptions, createE2BProvider, resolveE2BEnv, resolveE2BTemplate, runE2BPregame, waitForE2BReady };
|
|
168
|
+
//# sourceMappingURL=e2b.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"e2b.d.ts","names":[],"sources":["../../src/contexts/e2b.ts"],"mappings":";;;;UAmDU,gBAAA;EACR,MAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGQ,QAAA;EACR,IAAI;AAAA;AAAA,UAGI,UAAA;EACR,SAAA;EACA,QAAA;IACE,GAAA,GAAM,GAAA,UAAa,IAAA;MAAS,GAAA;MAAc,IAAA,GAAO,MAAA;MAAwB,SAAA;IAAA,MAAyB,OAAA,CAAQ,gBAAA;EAAA;EAE5G,KAAA;IACE,IAAA,GAAO,IAAA,aAAiB,OAAA;IACxB,KAAA,GAAQ,IAAA,UAAc,IAAA,aAAiB,OAAA;IACvC,IAAA,GAAO,IAAA,aAAiB,OAAA,CAAQ,QAAA;EAAA;EAElC,IAAA,QAAY,OAAA;AAAA;AAAA,UA0BG,kBAAA;EAEf;EAAA,MAAA;EAYA;;;;;EANA,MAAA;EAqCA;;;;;EA/BA,QAAA;EAgDiC;AAAA;AA4BnC;;;;;;EAnEE,SAAA;EAuEQ;;;;EAlER,GAAA;EAgEA;;;;EA3DA,cAAA;EA4D4D;;;;AACpD;EAvDR,GAAA,GAAM,MAAA;EAqF4B;;;;;EA/ElC,MAAA,GAAS,MAAM;EA4Ff;;;;AAA+B;AAgBjC;;EApGE,uBAAA;EAqGS;;;;;;;;EA5FT,OAAA;IAAY,IAAA;IAAc,OAAA;EAAA;AAAA;;AA+FlB;AAmCV;;;;;;;;;AAGuD;iBAzGjC,aAAA,CACpB,OAAA,EAAS,UAAA,EACT,MAAA,EAAQ,MAAA,EACR,IAAA;EAAQ,IAAA;EAAc,OAAA;EAAiB,GAAA;EAAc,IAAA,GAAO,MAAA;AAAA,IAC3D,OAAA;AAAA,UA8Bc,mBAAA;EAqFR;EAnFP,cAAA;EAiFA;;;;;AAEO;EA5EP,aAAA;EA4F+B;EA1F/B,GAAA;EA0FkF;EAxFlF,KAAA,IAAS,EAAA,aAAe,OAAO;AAAA;;;AAwFmD;;;;;;;;;;;iBAxE9D,eAAA,CACpB,OAAA,EAAS,UAAA,EACT,MAAA,EAAQ,MAAA,EACR,IAAA,EAAM,mBAAA,GACL,OAAA;;;;;;;;iBAmCa,kBAAA,CACd,gBAAA,sBACA,cAAA,sBACA,GAAA,GAAK,MAAM;;;;;;;iBAWG,aAAA,CACd,SAAA,EAAW,MAAA,8BACX,WAAA,EAAa,MAAA,+BACZ,MAAA;;;;;;;;iBAgBa,iBAAA,CAAkB,OAAA,GAAS,kBAAA,GAA0B,eAAe"}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { n as createLogger, t as consoleSink } from "../logger-Ktm-lj1s.js";
|
|
2
|
+
//#region src/contexts/e2b.ts
|
|
3
|
+
/** Per-probe timeout for the readiness `true` command. */
|
|
4
|
+
const READINESS_PROBE_TIMEOUT_MS = 5e3;
|
|
5
|
+
/** Pause between readiness probes while the sandbox is still warming up. */
|
|
6
|
+
const READINESS_BACKOFF_MS = 250;
|
|
7
|
+
/**
|
|
8
|
+
* Command timeout for the pregame setup script. E2B's `commands.run` defaults
|
|
9
|
+
* to 60s and offers no true "unlimited" — a `timeoutMs` of 0 is a 0ms deadline
|
|
10
|
+
* (instant timeout), not "off". Setup scripts (`apt`/`pip` installs, builds)
|
|
11
|
+
* routinely exceed 60s, so we pass a deliberately generous 30-minute ceiling.
|
|
12
|
+
* The sandbox's own lifetime independently bounds anything longer.
|
|
13
|
+
*/
|
|
14
|
+
const PREGAME_TIMEOUT_MS = 1800 * 1e3;
|
|
15
|
+
/**
|
|
16
|
+
* Upload a setup ("pregame") script into a ready sandbox and execute it.
|
|
17
|
+
*
|
|
18
|
+
* Writes the script to `/tmp/<name>`, then `chmod +x`'s and runs it directly
|
|
19
|
+
* (so a `#!/bin/bash` / `#!/usr/bin/env python3` shebang chooses the
|
|
20
|
+
* interpreter) in `cwd` with `envs` applied — the same env the sandbox was
|
|
21
|
+
* created with, so the script sees the run's `--env` / `--pass-env`. A
|
|
22
|
+
* generous {@link PREGAME_TIMEOUT_MS} ceiling is used so long `apt`/`pip`
|
|
23
|
+
* installs aren't killed by E2B's 60s command default. A non-zero exit
|
|
24
|
+
* (whether E2B returns it or raises on it) throws, so the caller can tear the
|
|
25
|
+
* sandbox down rather than prompt against a broken environment.
|
|
26
|
+
*/
|
|
27
|
+
async function runE2BPregame(sandbox, logger, opts) {
|
|
28
|
+
const path = `/tmp/${opts.name}`;
|
|
29
|
+
logger.info("running E2B pregame script", {
|
|
30
|
+
sandboxId: sandbox.sandboxId,
|
|
31
|
+
path
|
|
32
|
+
});
|
|
33
|
+
await sandbox.files.write(path, opts.content);
|
|
34
|
+
const quoted = JSON.stringify(path);
|
|
35
|
+
let result;
|
|
36
|
+
try {
|
|
37
|
+
result = await sandbox.commands.run(`chmod +x ${quoted} && ${quoted}`, {
|
|
38
|
+
cwd: opts.cwd,
|
|
39
|
+
envs: opts.envs,
|
|
40
|
+
timeoutMs: PREGAME_TIMEOUT_MS
|
|
41
|
+
});
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const exitCode = typeof err?.exitCode === "number" ? err.exitCode : 124;
|
|
44
|
+
logger.error("E2B pregame script failed", {
|
|
45
|
+
sandboxId: sandbox.sandboxId,
|
|
46
|
+
exitCode,
|
|
47
|
+
stdout: err?.stdout ?? "",
|
|
48
|
+
stderr: err?.stderr ?? err?.message ?? ""
|
|
49
|
+
});
|
|
50
|
+
throw new Error(`E2B pregame script ${opts.name} failed with exit code ${exitCode}`);
|
|
51
|
+
}
|
|
52
|
+
if (result.exitCode !== 0) {
|
|
53
|
+
logger.error("E2B pregame script failed", {
|
|
54
|
+
sandboxId: sandbox.sandboxId,
|
|
55
|
+
exitCode: result.exitCode,
|
|
56
|
+
stdout: result.stdout,
|
|
57
|
+
stderr: result.stderr
|
|
58
|
+
});
|
|
59
|
+
throw new Error(`E2B pregame script ${opts.name} failed with exit code ${result.exitCode}`);
|
|
60
|
+
}
|
|
61
|
+
logger.info("E2B pregame ready", {
|
|
62
|
+
sandboxId: sandbox.sandboxId,
|
|
63
|
+
stdout: result.stdout,
|
|
64
|
+
stderr: result.stderr
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Block until an E2B sandbox is ready to run commands, or throw if it never
|
|
69
|
+
* becomes ready within the deadline.
|
|
70
|
+
*
|
|
71
|
+
* The E2B control plane can hand back a sandbox id before the sandbox's `envd`
|
|
72
|
+
* is accepting commands; the first real command then absorbs that cold start
|
|
73
|
+
* and may time out. This polls a trivial `true` command on a short backoff,
|
|
74
|
+
* logging a `waiting…` line up front and a `ready` line (with `elapsedMs`) on
|
|
75
|
+
* success. On deadline exhaustion it best-effort kills the sandbox and throws.
|
|
76
|
+
*
|
|
77
|
+
* `now`/`sleep` are injectable purely for deterministic tests; in production
|
|
78
|
+
* they default to `Date.now` and a real timer.
|
|
79
|
+
*/
|
|
80
|
+
async function waitForE2BReady(sandbox, logger, opts) {
|
|
81
|
+
if (opts.timeoutSeconds <= 0) return;
|
|
82
|
+
const now = opts.now ?? Date.now;
|
|
83
|
+
const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
84
|
+
const start = now();
|
|
85
|
+
const deadline = start + opts.timeoutSeconds * 1e3;
|
|
86
|
+
logger.info("waiting for E2B sandbox to initialize", { sandboxId: sandbox.sandboxId });
|
|
87
|
+
while (true) try {
|
|
88
|
+
await sandbox.commands.run("true", { timeoutMs: READINESS_PROBE_TIMEOUT_MS });
|
|
89
|
+
logger.info("E2B sandbox ready", {
|
|
90
|
+
sandboxId: sandbox.sandboxId,
|
|
91
|
+
elapsedMs: now() - start
|
|
92
|
+
});
|
|
93
|
+
return;
|
|
94
|
+
} catch {
|
|
95
|
+
if (now() >= deadline) {
|
|
96
|
+
if (opts.killOnTimeout !== false) await sandbox.kill().catch(() => {});
|
|
97
|
+
throw new Error(`E2B sandbox ${sandbox.sandboxId} did not become ready within ${opts.timeoutSeconds}s`);
|
|
98
|
+
}
|
|
99
|
+
await sleep(READINESS_BACKOFF_MS);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the effective E2B template, highest precedence first: the per-spawn
|
|
104
|
+
* `SpawnConfig.sandbox.template`, then the factory `template` option, then the
|
|
105
|
+
* `E2B_TEMPLATE` env var. Returns `undefined` (→ SDK's `base` default) when
|
|
106
|
+
* none is set. The E2B SDK reads `E2B_API_KEY`/`E2B_DOMAIN` from the env but
|
|
107
|
+
* NOT `E2B_TEMPLATE`, so this provider has to honor it itself.
|
|
108
|
+
*/
|
|
109
|
+
function resolveE2BTemplate(perSpawnTemplate, optionTemplate, env = process.env) {
|
|
110
|
+
return perSpawnTemplate || optionTemplate || env.E2B_TEMPLATE || void 0;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Merge the factory-level {@link E2BProviderOptions.env} with the per-spawn
|
|
114
|
+
* `SpawnConfig.env`, per-spawn winning on key conflicts. Returns `undefined`
|
|
115
|
+
* when the merge is empty so callers can skip setting `envs` on the create
|
|
116
|
+
* options entirely (rather than handing the SDK an empty object).
|
|
117
|
+
*/
|
|
118
|
+
function resolveE2BEnv(optionEnv, perSpawnEnv) {
|
|
119
|
+
const merged = {
|
|
120
|
+
...optionEnv,
|
|
121
|
+
...perSpawnEnv
|
|
122
|
+
};
|
|
123
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build a {@link SandboxProvider} backed by E2B.
|
|
127
|
+
*
|
|
128
|
+
* The provider owns a registry of live {@link E2BSandbox} instances keyed by
|
|
129
|
+
* sandbox id, so the per-call `exec`/`readFile`/… hooks reuse the same warm
|
|
130
|
+
* connection rather than reconnecting each time.
|
|
131
|
+
*/
|
|
132
|
+
function createE2BProvider(options = {}) {
|
|
133
|
+
const live = /* @__PURE__ */ new Map();
|
|
134
|
+
const logger = options.logger ?? createLogger(consoleSink());
|
|
135
|
+
const readinessTimeoutSeconds = options.readinessTimeoutSeconds ?? 60;
|
|
136
|
+
async function loadSdk() {
|
|
137
|
+
try {
|
|
138
|
+
return (await import("e2b")).Sandbox;
|
|
139
|
+
} catch {
|
|
140
|
+
throw new Error("e2b is required for the E2B sandbox provider. Install it with: bun add e2b");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function get(sandboxId) {
|
|
144
|
+
const entry = live.get(sandboxId);
|
|
145
|
+
if (!entry) throw new Error(`E2B sandbox ${sandboxId} is not tracked by this provider`);
|
|
146
|
+
return entry;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
name: "e2b",
|
|
150
|
+
async spawn(config) {
|
|
151
|
+
const Sandbox = await loadSdk();
|
|
152
|
+
const sb = config.sandbox ?? {};
|
|
153
|
+
const apiKey = sb.apiKey ?? options.apiKey;
|
|
154
|
+
const domain = sb.domain ?? options.domain;
|
|
155
|
+
const template = resolveE2BTemplate(sb.template, options.template);
|
|
156
|
+
const timeoutSeconds = config.limits?.timeout ?? options.timeoutSeconds;
|
|
157
|
+
const connectId = sb.sandboxId ?? options.sandboxId;
|
|
158
|
+
const envs = resolveE2BEnv(options.env, config.env);
|
|
159
|
+
let sandbox;
|
|
160
|
+
const owned = connectId === void 0;
|
|
161
|
+
if (connectId !== void 0) {
|
|
162
|
+
const connectOpts = {};
|
|
163
|
+
if (apiKey !== void 0) connectOpts.apiKey = apiKey;
|
|
164
|
+
if (domain !== void 0) connectOpts.domain = domain;
|
|
165
|
+
if (timeoutSeconds !== void 0) connectOpts.timeoutMs = timeoutSeconds * 1e3;
|
|
166
|
+
sandbox = await Sandbox.connect(connectId, connectOpts);
|
|
167
|
+
} else {
|
|
168
|
+
const createOpts = {};
|
|
169
|
+
if (apiKey !== void 0) createOpts.apiKey = apiKey;
|
|
170
|
+
if (domain !== void 0) createOpts.domain = domain;
|
|
171
|
+
if (envs) createOpts.envs = envs;
|
|
172
|
+
if (timeoutSeconds !== void 0) createOpts.timeoutMs = timeoutSeconds * 1e3;
|
|
173
|
+
sandbox = template !== void 0 ? await Sandbox.create(template, createOpts) : await Sandbox.create(createOpts);
|
|
174
|
+
}
|
|
175
|
+
live.set(sandbox.sandboxId, {
|
|
176
|
+
sandbox,
|
|
177
|
+
env: envs,
|
|
178
|
+
owned
|
|
179
|
+
});
|
|
180
|
+
try {
|
|
181
|
+
await waitForE2BReady(sandbox, logger, {
|
|
182
|
+
timeoutSeconds: readinessTimeoutSeconds,
|
|
183
|
+
killOnTimeout: owned
|
|
184
|
+
});
|
|
185
|
+
} catch (err) {
|
|
186
|
+
live.delete(sandbox.sandboxId);
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
const requestedCwd = config.cwd ?? options.cwd;
|
|
190
|
+
let cwd;
|
|
191
|
+
if (requestedCwd) {
|
|
192
|
+
cwd = requestedCwd;
|
|
193
|
+
await sandbox.commands.run(`mkdir -p ${JSON.stringify(cwd)}`).catch(() => {});
|
|
194
|
+
} else cwd = await sandbox.commands.run("pwd").then((r) => r.stdout.trim()).catch(() => "") || "/home/user";
|
|
195
|
+
if (options.pregame) try {
|
|
196
|
+
await runE2BPregame(sandbox, logger, {
|
|
197
|
+
name: options.pregame.name,
|
|
198
|
+
content: options.pregame.content,
|
|
199
|
+
cwd,
|
|
200
|
+
envs
|
|
201
|
+
});
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (owned) await sandbox.kill().catch(() => {});
|
|
204
|
+
live.delete(sandbox.sandboxId);
|
|
205
|
+
throw err;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
id: sandbox.sandboxId,
|
|
209
|
+
cwd
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
async exec(sandboxId, command, opts) {
|
|
213
|
+
const { sandbox, env } = get(sandboxId);
|
|
214
|
+
const timeoutMs = (opts?.timeout ?? 30) * 1e3;
|
|
215
|
+
try {
|
|
216
|
+
const result = await sandbox.commands.run(command, {
|
|
217
|
+
cwd: opts?.cwd,
|
|
218
|
+
envs: resolveE2BEnv(env, opts?.env),
|
|
219
|
+
timeoutMs: timeoutMs > 0 ? timeoutMs : void 0
|
|
220
|
+
});
|
|
221
|
+
return {
|
|
222
|
+
stdout: result.stdout,
|
|
223
|
+
stderr: result.stderr,
|
|
224
|
+
exitCode: result.exitCode
|
|
225
|
+
};
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (typeof err?.exitCode === "number") return {
|
|
228
|
+
stdout: err.stdout ?? "",
|
|
229
|
+
stderr: err.stderr ?? err.message ?? "",
|
|
230
|
+
exitCode: err.exitCode
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
stdout: "",
|
|
234
|
+
stderr: err?.message ?? String(err),
|
|
235
|
+
exitCode: 124
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
async readFile(sandboxId, path) {
|
|
240
|
+
return get(sandboxId).sandbox.files.read(path);
|
|
241
|
+
},
|
|
242
|
+
async writeFile(sandboxId, path, content) {
|
|
243
|
+
await get(sandboxId).sandbox.files.write(path, content);
|
|
244
|
+
},
|
|
245
|
+
async listFiles(sandboxId, path) {
|
|
246
|
+
return (await get(sandboxId).sandbox.files.list(path)).map((e) => e.name);
|
|
247
|
+
},
|
|
248
|
+
async destroy(sandboxId) {
|
|
249
|
+
const entry = live.get(sandboxId);
|
|
250
|
+
if (!entry) return;
|
|
251
|
+
if (entry.owned) try {
|
|
252
|
+
await entry.sandbox.kill();
|
|
253
|
+
} catch {}
|
|
254
|
+
live.delete(sandboxId);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
export { createE2BProvider, resolveE2BEnv, resolveE2BTemplate, runE2BPregame, waitForE2BReady };
|
|
260
|
+
|
|
261
|
+
//# sourceMappingURL=e2b.js.map
|