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.
Files changed (118) hide show
  1. package/README.md +31 -5
  2. package/dist/{agent-BHkvYIH9.d.ts → agent-D0W9yClt.d.ts} +114 -27
  3. package/dist/agent-D0W9yClt.d.ts.map +1 -0
  4. package/dist/chat/pure.d.ts +3 -3
  5. package/dist/chat.d.ts +7 -7
  6. package/dist/chat.js +2 -2
  7. package/dist/contexts/docker.d.ts +1 -1
  8. package/dist/contexts/docker.d.ts.map +1 -1
  9. package/dist/contexts/docker.js +53 -14
  10. package/dist/contexts/docker.js.map +1 -1
  11. package/dist/contexts/e2b.d.ts +168 -0
  12. package/dist/contexts/e2b.d.ts.map +1 -0
  13. package/dist/contexts/e2b.js +261 -0
  14. package/dist/contexts/e2b.js.map +1 -0
  15. package/dist/{contexts-BJVgG0LY.js → contexts-DglWSzmR.js} +59 -9
  16. package/dist/contexts-DglWSzmR.js.map +1 -0
  17. package/dist/contexts.d.ts +3 -3
  18. package/dist/contexts.js +1 -1
  19. package/dist/eval.d.ts +1 -1
  20. package/dist/eval.js +5 -5
  21. package/dist/eval.js.map +1 -1
  22. package/dist/{headless-CPaunZsU.js → headless-Bb5gU8AR.js} +6 -6
  23. package/dist/{headless-CPaunZsU.js.map → headless-Bb5gU8AR.js.map} +1 -1
  24. package/dist/headless.d.ts +1 -1
  25. package/dist/headless.js +1 -1
  26. package/dist/{index-C_t8tW_X.d.ts → index-CrMb8jCE.d.ts} +2 -2
  27. package/dist/{index-C_t8tW_X.d.ts.map → index-CrMb8jCE.d.ts.map} +1 -1
  28. package/dist/{index-BIo67xLV.d.ts → index-D60tX5XC.d.ts} +10 -3
  29. package/dist/index-D60tX5XC.d.ts.map +1 -0
  30. package/dist/{index-C4aT2kO_.d.ts → index-DZR99FD4.d.ts} +30 -111
  31. package/dist/index-DZR99FD4.d.ts.map +1 -0
  32. package/dist/index.d.ts +7 -6
  33. package/dist/index.js +11 -10
  34. package/dist/index.js.map +1 -1
  35. package/dist/{interpolate-Dy7Lunvg.js → interpolate-CTfr0GdR.js} +19 -1
  36. package/dist/{interpolate-Dy7Lunvg.js.map → interpolate-CTfr0GdR.js.map} +1 -1
  37. package/dist/logger-Ktm-lj1s.js +300 -0
  38. package/dist/logger-Ktm-lj1s.js.map +1 -0
  39. package/dist/logger-n4LsLISE.d.ts +102 -0
  40. package/dist/logger-n4LsLISE.d.ts.map +1 -0
  41. package/dist/{login-0jP1pnSJ.js → login-BHhOdTp9.js} +4 -301
  42. package/dist/login-BHhOdTp9.js.map +1 -0
  43. package/dist/{mcp-tevNihk_.js → mcp-Cy9mgCcr.js} +22 -9
  44. package/dist/mcp-Cy9mgCcr.js.map +1 -0
  45. package/dist/mcp.d.ts +1 -1
  46. package/dist/mcp.js +1 -1
  47. package/dist/{messages-C_1AmSpk.js → messages-RPKrEPvH.js} +6 -2
  48. package/dist/messages-RPKrEPvH.js.map +1 -0
  49. package/dist/output/stream-json.d.ts +2 -2
  50. package/dist/output/stream-json.js +1 -1
  51. package/dist/output/terminal.d.ts +2 -2
  52. package/dist/output/terminal.js +1 -0
  53. package/dist/output/terminal.js.map +1 -1
  54. package/dist/{presets-Cm2BPJaU.js → presets-D5ibZTml.js} +2 -2
  55. package/dist/{presets-Cm2BPJaU.js.map → presets-D5ibZTml.js.map} +1 -1
  56. package/dist/presets.d.ts +2 -2
  57. package/dist/presets.js +1 -1
  58. package/dist/{providers-BGBB18zz.js → providers-C2cxujp_.js} +85 -20
  59. package/dist/providers-C2cxujp_.js.map +1 -0
  60. package/dist/providers.d.ts +1 -1
  61. package/dist/providers.js +2 -2
  62. package/dist/restate.d.ts +2 -2
  63. package/dist/restate.js +4 -1
  64. package/dist/restate.js.map +1 -1
  65. package/dist/session/sqlite.d.ts +1 -1
  66. package/dist/session/sqlite.d.ts.map +1 -1
  67. package/dist/session/sqlite.js +36 -4
  68. package/dist/session/sqlite.js.map +1 -1
  69. package/dist/{session-CtAWwwkn.js → session-Do_TQV7c.js} +70 -22
  70. package/dist/session-Do_TQV7c.js.map +1 -0
  71. package/dist/session.d.ts +2 -2
  72. package/dist/session.js +3 -3
  73. package/dist/shell-quote-BmnhZmdM.js +33 -0
  74. package/dist/shell-quote-BmnhZmdM.js.map +1 -0
  75. package/dist/skills.d.ts +3 -3
  76. package/dist/skills.js +1 -1
  77. package/dist/skills.js.map +1 -1
  78. package/dist/{tool-formatters-D_fX6FGl.d.ts → tool-formatters-RT5-gyE2.d.ts} +2 -2
  79. package/dist/{tool-formatters-D_fX6FGl.d.ts.map → tool-formatters-RT5-gyE2.d.ts.map} +1 -1
  80. package/dist/tools/fetch-url.d.ts +1 -1
  81. package/dist/tools/web-search.d.ts +1 -1
  82. package/dist/{tools-NxnEmzYg.js → tools-ZHKOh44k.js} +342 -123
  83. package/dist/tools-ZHKOh44k.js.map +1 -0
  84. package/dist/tools.d.ts +2 -2
  85. package/dist/tools.js +1 -1
  86. package/dist/{transcript-anchors-DA6XawEU.d.ts → transcript-anchors-B4FxkG-8.d.ts} +10 -4
  87. package/dist/transcript-anchors-B4FxkG-8.d.ts.map +1 -0
  88. package/dist/{transcript-anchors-B_c7gWot.js → transcript-anchors-CS46ul6X.js} +10 -10
  89. package/dist/transcript-anchors-CS46ul6X.js.map +1 -0
  90. package/dist/tui.d.ts +3 -3
  91. package/dist/tui.d.ts.map +1 -1
  92. package/dist/tui.js +167 -41
  93. package/dist/tui.js.map +1 -1
  94. package/dist/{turn-operations-CCl7rpbT.d.ts → turn-operations-CoRj3mYZ.d.ts} +3 -3
  95. package/dist/{turn-operations-CCl7rpbT.d.ts.map → turn-operations-CoRj3mYZ.d.ts.map} +1 -1
  96. package/dist/{types-BibzMDjX.d.ts → types-B39tBba1.d.ts} +69 -2
  97. package/dist/types-B39tBba1.d.ts.map +1 -0
  98. package/dist/types-BiobHM1D.js.map +1 -1
  99. package/dist/types.d.ts +5 -5
  100. package/docs/ARCHITECTURE.md +1 -1
  101. package/docs/CHAT.md +3 -3
  102. package/docs/EXECUTION_CONTEXT.md +257 -0
  103. package/docs/RUN_IN_BACKGROUND.md +8 -0
  104. package/docs/SKILL.md +3 -3
  105. package/package.json +57 -24
  106. package/dist/agent-BHkvYIH9.d.ts.map +0 -1
  107. package/dist/contexts-BJVgG0LY.js.map +0 -1
  108. package/dist/index-BIo67xLV.d.ts.map +0 -1
  109. package/dist/index-C4aT2kO_.d.ts.map +0 -1
  110. package/dist/login-0jP1pnSJ.js.map +0 -1
  111. package/dist/mcp-tevNihk_.js.map +0 -1
  112. package/dist/messages-C_1AmSpk.js.map +0 -1
  113. package/dist/providers-BGBB18zz.js.map +0 -1
  114. package/dist/session-CtAWwwkn.js.map +0 -1
  115. package/dist/tools-NxnEmzYg.js.map +0 -1
  116. package/dist/transcript-anchors-B_c7gWot.js.map +0 -1
  117. package/dist/transcript-anchors-DA6XawEU.d.ts.map +0 -1
  118. package/dist/types-BibzMDjX.d.ts.map +0 -1
@@ -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
- await container.start();
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 = 0;
195
+ let exitCode;
196
+ let inspectNote = "";
172
197
  try {
173
- exitCode = (await exec.inspect()).ExitCode ?? 0;
174
- } catch {}
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 ${JSON.stringify(path)}`);
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 ${JSON.stringify(path)})" && printf '%s' '${escaped}' > ${JSON.stringify(path)}`);
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 ${JSON.stringify(path)}`);
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
- await ref.container.stop({ t: 5 });
223
- await ref.container.remove({ force: true });
224
- } catch {}
225
- containers.delete(handle.id);
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":";;AAaA,MAAM,kBAAkB;;;;;AAMxB,SAAS,uBAAuB,KAAuB;CACrD,MAAM,OAAQ,KAAwC;CACtD,OAAO,SAAS,OAAO,SAAS;AAClC;AAQA,SAAgB,oBAAoB,QAAwC;CAC1E,IAAI,UAAU;CACd,MAAM,6BAAa,IAAI,IAA0B;CACjD,MAAM,eAAe,QAAQ,SAAS;CACtC,MAAM,aAAa,QAAQ,OAAO;CAClC,MAAM,aAAa,QAAQ;CAC3B,MAAM,gBAAgB,QAAQ;CAC9B,MAAM,gBAAgB,QAAQ,UAAU,CAAC;CACzC,MAAM,cAAc,QAAQ;CAE5B,eAAe,eAAe;EAC5B,IAAI;GACF,MAAM,aAAa,MAAM,OAAO,cAAc;GAC9C,OAAO,IAAI,UAAU;EACvB,QACM;GACJ,MAAM,IAAI,MAAM,wFAAwF;EAC1G;CACF;CAEA,MAAM,MAAwB;EAC5B,MAAM;EAEN,cAAc;GACZ,OAAO;GACP,YAAY;GACZ,SAAS;GACT,KAAK;EACP;EAEA,MAAM,MAAM,WAAmD;GAC7D,MAAM,SAAS,MAAM,aAAa;GAClC,MAAM,KAAK,UAAU,EAAE;GACvB,MAAM,QAAQ,WAAW,SAAS;GAClC,MAAM,MAAM,WAAW,OAAO;GAC9B,MAAM,SAAS,CAAC,GAAG,eAAe,GAAI,WAAW,UAAU,CAAC,CAAE;GAC9D,MAAM,OAAO,WAAW,QAAQ;GAGhC,IAAI;IACF,MAAM,OAAO,SAAS,KAAK,EAAE,QAAQ;GACvC,QACM;IACJ,MAAM,IAAI,SAAe,SAAS,WAAW;KAC3C,OAAO,KAAK,QAAQ,KAAmB,WAAkC;MACvE,IAAI,KACF,OAAO,OAAO,GAAG;MACnB,OAAO,MAAM,eAAe,SAAS,SAAuB;OAC1D,OAAO,OAAO,IAAI,IAAI,QAAQ;MAChC,CAAC;KACH,CAAC;IACH,CAAC;GACH;GAEA,MAAM,SAAS;IAAE,GAAG;IAAe,GAAG,WAAW;GAAO;GACxD,MAAM,aAAsC,CAAC;GAE7C,IAAI,QAAQ,QACV,WAAW,SAAS,OAAO,SAAS,OAAO;GAE7C,IAAI,QAAQ,KACV,WAAW,WAAW,OAAO,WAAW,OAAO,GAAG,IAAI;GAIxD,MAAM,YAAY;IAAE,GAAG,QAAQ;IAAW,GAAG,WAAW;GAAU;GAClE,IAAI,UAAU,qBACZ,WAAW,UAAU,CAAC,KAAK;GAC7B,IAAI,UAAU,iBACZ,WAAW,cAAc,CAAC,mBAAmB;GAC/C,IAAI,UAAU,gBACZ,WAAW,iBAAiB;GAC9B,IAAI,UAAU,aAAa,MACzB,WAAW,YAAY,UAAU;GAEnC,MAAM,MAAM;IAAE,GAAG;IAAY,GAAG,WAAW;GAAI;GAE/C,MAAM,YAAY,eAAe,MAAM;GACvC,IAAI,UAAU,SAAS,GACrB,WAAW,QAAQ;GAGrB,MAAM,WAAW,WAAW,SAAS,QAAQ;GAC7C,MAAM,eAAsD,CAAC;GAC7D,MAAM,eAA4D,CAAC;GAEnE,IAAI,UAAU,QAAQ;IACpB,KAAK,MAAM,KAAK,UAAU;KACxB,MAAM,MAAM,GAAG,EAAE,UAAU,GAAG,EAAE,SAAS;KACzC,aAAa,OAAO,CAAC;KACrB,aAAa,OAAO,CAAC,EAAE,UAAU,EAAE,QAAQ,OAAO,KAAK,OAAO,EAAE,IAAI,EAAE,CAAC;IACzE;IACA,WAAW,eAAe;GAC5B;GAEA,MAAM,OAAO,WAAW,QAAQ,QAAQ;GACxC,MAAM,UAAU,WAAW,WAAW,QAAQ;GAC9C,IAAI,SACF,WAAW,cAAc;GAG3B,MAAM,SAAS;IAAE,GAAG,QAAQ;IAAQ,GAAG,WAAW;GAAO;GAEzD,MAAM,YAAY,MAAM,OAAO,gBAAgB;IAG7C,GAAI,OAAO,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC;IACvF,OAAO;IACP,KAAK,CAAC,SAAS,UAAU;IACzB,YAAY;IACZ,KAAK,OAAO,QAAQ,GAAG,EAAE,KAAK,CAAC,GAAG,OAAO,GAAG,EAAE,GAAG,GAAG;IACpD,YAAY;IACZ,GAAI,OAAO,KAAK,YAAY,EAAE,SAAS,EAAE,cAAc,aAAa,IAAI,CAAC;IACzE,GAAI,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAI,OAAO,KAAK,MAAM,EAAE,SAAS,EAAE,QAAQ,OAAO,IAAI,CAAC;GACzD,CAAC;GAED,IAAI;IACF,MAAM,UAAU,MAAM;GACxB,SACO,KAAK;IAGV,MAAM,UAAU,OAAO,EAAE,OAAO,KAAK,CAAC,EAAE,YAAY,CAAC,CAAC;IACtD,MAAM;GACR;GAEA,MAAM,SAA0B;IAAE;IAAI,MAAM;IAAU;GAAI;GAC1D,WAAW,IAAI,IAAI;IAAE;IAAQ;IAAW;GAAO,CAAC;GAEhD,OAAO;EACT;EAEA,MAAM,KAAK,QAAyB,SAAiB,SAAiG;GACpJ,MAAM,MAAM,WAAW,IAAI,OAAO,EAAE;GACpC,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,aAAa,OAAO,GAAG,WAAW;GAEpD,MAAM,UAAU,SAAS,OAAO,OAAO;GACvC,MAAM,MAAM,SAAS,MACjB,OAAO,QAAQ,QAAQ,GAAG,EAAE,KAAK,CAAC,GAAG,OAAO,GAAG,EAAE,GAAG,GAAG,IACvD,CAAC;GAEL,MAAM,UAAU,SAAS,WAAW,eAAe,WAAW;GAE9D,MAAM,OAAO,MAAM,IAAI,UAAU,KAAK;IAOpC,KAAK;KAAC;KAAW,OAAO,OAAO;KAAG;KAAM;KAAM;IAAO;IACrD,YAAY;IACZ,KAAK;IACL,cAAc;IACd,cAAc;GAChB,CAAC;GAED,MAAM,SAAS,MAAM,KAAK,MAAM,EAAE,QAAQ,MAAM,CAAC;GAEjD,OAAO,IAAI,SAAqB,YAAY;IAC1C,IAAI,SAAS;IACb,IAAI,SAAS;IACb,IAAI,WAAW;IAMf,MAAM,aAAa;KACjB,MAAM,OAAwB;MAC5B,UAAU,OAAO,UAAU,WAAW,QAAQ,MAAM,SAAS,OAAO;MACpE,OAAO;KACT;KACA,MAAM,CAAC;KACP,KAAK,CAAC;KACN,OAAO,CAAC;KACR,OAAO;MAAE,OAAO;KAAK;IACvB;IACA,MAAM,aAAa;KACjB,MAAM,OAAwB;MAC5B,UAAU,OAAO,UAAU,WAAW,QAAQ,MAAM,SAAS,OAAO;MACpE,OAAO;KACT;KACA,MAAM,CAAC;KACP,KAAK,CAAC;KACN,OAAO,CAAC;KACR,OAAO;MAAE,OAAO;KAAK;IACvB;IACA,IAAI;KACF,IAAI,OAAO,MAAM,YAAY,QAAQ,YAAY,UAAU;IAC7D,QACM;KAGJ,OAAO,GAAG,SAAS,UAAkB;MACnC,UAAU,MAAM,SAAS,OAAO;KAClC,CAAC;IACH;IAKA,MAAM,QAAQ,iBAAiB;KAC7B,IAAI,UACF;KACF,WAAW;KAGX,IAAI;MAAE,OAAO,UAAU;KAAE,QACnB,CAAgB;KACtB,QAAQ;MAAE;MAAQ,QAAQ,SAAS,GAAG,OAAO,eAAe;MAAa,UAAU;KAAI,CAAC;IAC1F,GAAG,UAAU,MAAO,GAAI;IAExB,OAAO,GAAG,OAAO,YAAY;KAC3B,IAAI,UACF;KACF,WAAW;KACX,aAAa,KAAK;KAClB,IAAI;KACJ,IAAI,cAAc;KAClB,IAAI;MACF,MAAM,UAAU,MAAM,KAAK,QAAQ;MAInC,IAAI,QAAQ,YAAY,MAAM;OAC5B,WAAW;OACX,cAAc;MAChB,OAEE,WAAW,QAAQ;KAEvB,SACO,KAAK;MACV,WAAW;MACX,cAAc,gDAAiD,KAAe,WAAW,OAAO,GAAG,EAAE;KACvG;KACA,QAAQ;MACN;MACA,QAAQ,cAAe,SAAS,GAAG,OAAO,IAAI,gBAAgB,cAAe;MAC7E;KACF,CAAC;IACH,CAAC;IAED,OAAO,GAAG,UAAU,QAAe;KACjC,IAAI,UACF;KACF,WAAW;KACX,aAAa,KAAK;KAClB,QAAQ;MAAE;MAAQ,QAAQ,SAAS,GAAG,OAAO,IAAI,IAAI,YAAY,IAAI;MAAS,UAAU;KAAE,CAAC;IAC7F,CAAC;GACH,CAAC;EACH;EAKA,MAAM,SAAS,QAAyB,MAA+B;GACrE,MAAM,SAAS,MAAM,IAAI,KAAK,QAAQ,OAAO,YAAY,IAAI,GAAG;GAChE,IAAI,OAAO,aAAa,GACtB,MAAM,IAAI,MAAM,wBAAwB,OAAO,QAAQ;GACzD,OAAO,OAAO;EAChB;EAEA,MAAM,UAAU,QAAyB,MAAc,SAAgC;GACrF,MAAM,UAAU,QAAQ,QAAQ,iBAAiB,OAAO,GAAG,MAAM;GACjE,MAAM,SAAS,MAAM,IAAI,KAAK,QAAQ,uBAAuB,YAAY,IAAI,EAAE,qBAAqB,QAAQ,MAAM,YAAY,IAAI,GAAG;GACrI,IAAI,OAAO,aAAa,GACtB,MAAM,IAAI,MAAM,yBAAyB,OAAO,QAAQ;EAC5D;EAEA,MAAM,UAAU,QAAyB,MAAiC;GACxE,MAAM,SAAS,MAAM,IAAI,KAAK,QAAQ,SAAS,YAAY,IAAI,GAAG;GAClE,IAAI,OAAO,aAAa,GACtB,OAAO,CAAC;GACV,OAAO,OAAO,OAAO,KAAK,EAAE,MAAM,IAAI,EAAE,OAAO,OAAO;EACxD;EAEA,MAAM,cAAc,QAAyB,eAA+C;GAC1F,MAAM,MAAM,WAAW,IAAI,OAAO,EAAE;GACpC,IAAI,CAAC,KACH,OAAO;GAGT,MAAM,YAAW,MADE,IAAI,UAAU,QAAQ,IAClB,iBAAiB,SAAS,CAAC;GAElD,KAAK,MAAM,SAAS,CAAC,OAAO,KAAK,GAAG;IAClC,MAAM,OAAO,SAAS,GAAG,cAAc,GAAG;IAC1C,IAAI,MAAM,QAAQ,IAAI,KAAK,KAAK,SAAS,KAAK,KAAK,IAAI,UACrD,OAAO,OAAO,SAAS,KAAK,GAAG,UAAU,EAAE;GAE/C;GAEA,OAAO;EACT;EAEA,MAAM,QAAQ,QAAwC;GACpD,MAAM,MAAM,WAAW,IAAI,OAAO,EAAE;GACpC,IAAI,CAAC,KACH;GAEF,IAAI;IACF,IAAI;KACF,MAAM,IAAI,UAAU,KAAK,EAAE,GAAG,EAAE,CAAC;IACnC,QACM,CAIN;IACA,IAAI;KACF,MAAM,IAAI,UAAU,OAAO,EAAE,OAAO,KAAK,CAAC;IAC5C,SACO,KAAK;KACV,IAAI,CAAC,uBAAuB,GAAG,GAC7B,MAAM;IACV;GACF,UACQ;IACN,WAAW,OAAO,OAAO,EAAE;GAC7B;EACF;CACF;CAEA,OAAO;AACT;;;;;;AAOA,SAAS,gBAAgB,OAA2D;CAClF,IAAI,CAAC,MAAM,QACT,MAAM,IAAI,MAAM,iCAAiC;CACnD,IAAI,CAAC,MAAM,UAAU,CAAC,MAAM,OAAO,WAAW,GAAG,GAC/C,MAAM,IAAI,MAAM,iDAAiD,MAAM,QAAQ;CACjF,IAAI,MAAM,YAAY,MAAM,QAC1B,MAAM,IAAI,MAAM,oDAAoD,MAAM,QAAQ;CACpF,MAAM,OAAO,MAAM,WAAW,QAAQ,MAAM,SAAS,OAAO;CAC5D,OAAO,GAAG,MAAM,OAAO,GAAG,MAAM,SAAS;AAC3C;;AAGA,SAAgB,eAAe,QAAsD;CACnF,OAAO,OAAO,IAAI,eAAe;AACnC"}
@@ -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