zidane 6.5.7 → 6.5.9

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 (91) hide show
  1. package/dist/{acp-Dm95NwNM.js → acp-EFzMJPxa.js} +3 -3
  2. package/dist/{acp-Dm95NwNM.js.map → acp-EFzMJPxa.js.map} +1 -1
  3. package/dist/acp-cli.js +4 -4
  4. package/dist/acp.d.ts +2 -2
  5. package/dist/acp.js +1 -1
  6. package/dist/{agent-CcTWL-Z6.js → agent-ut6nQRnI.js} +2 -2
  7. package/dist/{agent-CcTWL-Z6.js.map → agent-ut6nQRnI.js.map} +1 -1
  8. package/dist/agent.d.ts +1 -1
  9. package/dist/agent.js +1 -1
  10. package/dist/{auth-C-Zx5zBo.js → auth-yAckG81N.js} +2 -2
  11. package/dist/{auth-C-Zx5zBo.js.map → auth-yAckG81N.js.map} +1 -1
  12. package/dist/chat/pure.d.ts +3 -3
  13. package/dist/chat.d.ts +4 -4
  14. package/dist/chat.js +3 -3
  15. package/dist/contexts/daytona.d.ts +14 -6
  16. package/dist/contexts/daytona.d.ts.map +1 -1
  17. package/dist/contexts/daytona.js +4 -9
  18. package/dist/contexts/daytona.js.map +1 -1
  19. package/dist/contexts/docker.d.ts +10 -2
  20. package/dist/contexts/docker.d.ts.map +1 -1
  21. package/dist/contexts/docker.js +44 -6
  22. package/dist/contexts/docker.js.map +1 -1
  23. package/dist/contexts/e2b.d.ts +10 -3
  24. package/dist/contexts/e2b.d.ts.map +1 -1
  25. package/dist/contexts/e2b.js +4 -2
  26. package/dist/contexts/e2b.js.map +1 -1
  27. package/dist/contexts/sandbox.d.ts +1 -1
  28. package/dist/contexts/sandbox.js +2 -2
  29. package/dist/contexts/sandbox.js.map +1 -1
  30. package/dist/{contexts-DWVt7MvF.js → contexts-BSFQVclW.js} +4 -3
  31. package/dist/contexts-BSFQVclW.js.map +1 -0
  32. package/dist/contexts.d.ts +3 -3
  33. package/dist/contexts.js +1 -1
  34. package/dist/eval.d.ts +1 -1
  35. package/dist/eval.js +1 -1
  36. package/dist/extensions.d.ts +1 -1
  37. package/dist/extensions.js +1 -1
  38. package/dist/headless.d.ts +1 -1
  39. package/dist/headless.js +2 -2
  40. package/dist/{index-ZWibV230.d.ts → index-A1qz0l55.d.ts} +2 -2
  41. package/dist/{index-ZWibV230.d.ts.map → index-A1qz0l55.d.ts.map} +1 -1
  42. package/dist/{index-C8TwH34B.d.ts → index-B6FkK6ZN.d.ts} +2 -2
  43. package/dist/index-B6FkK6ZN.d.ts.map +1 -0
  44. package/dist/index.d.ts +4 -4
  45. package/dist/index.js +2 -2
  46. package/dist/mcp.d.ts +1 -1
  47. package/dist/output/stream-json.d.ts +1 -1
  48. package/dist/output/terminal.d.ts +1 -1
  49. package/dist/plugins.d.ts +1 -1
  50. package/dist/presets.d.ts +1 -1
  51. package/dist/presets.js +1 -1
  52. package/dist/providers/anthropic.d.ts +1 -1
  53. package/dist/providers/arcee.d.ts +1 -1
  54. package/dist/providers/baseten.d.ts +1 -1
  55. package/dist/providers/cerebras.d.ts +1 -1
  56. package/dist/providers/local.d.ts +1 -1
  57. package/dist/providers/openai-compat.d.ts +1 -1
  58. package/dist/providers/openai.d.ts +1 -1
  59. package/dist/providers/openrouter.d.ts +1 -1
  60. package/dist/providers/xai.d.ts +1 -1
  61. package/dist/providers.d.ts +1 -1
  62. package/dist/restate.d.ts +2 -2
  63. package/dist/restate.js +1 -1
  64. package/dist/{sandbox-DLY6uah7.d.ts → sandbox-CDl9tfTo.d.ts} +4 -3
  65. package/dist/sandbox-CDl9tfTo.d.ts.map +1 -0
  66. package/dist/session/sqlite.d.ts +1 -1
  67. package/dist/session.d.ts +1 -1
  68. package/dist/skills.d.ts +1 -1
  69. package/dist/{tool-formatters-D3wSS2zz.d.ts → tool-formatters-OobhXAK1.d.ts} +2 -2
  70. package/dist/{tool-formatters-D3wSS2zz.d.ts.map → tool-formatters-OobhXAK1.d.ts.map} +1 -1
  71. package/dist/tools/fetch-url.d.ts +1 -1
  72. package/dist/tools/web-search.d.ts +1 -1
  73. package/dist/tools.d.ts +1 -1
  74. package/dist/tools.js +1 -1
  75. package/dist/{transcript-anchors-llwMrLRG.d.ts → transcript-anchors-0midxNxf.d.ts} +2 -2
  76. package/dist/{transcript-anchors-llwMrLRG.d.ts.map → transcript-anchors-0midxNxf.d.ts.map} +1 -1
  77. package/dist/{transcript-anchors-DGUqw92H.js → transcript-anchors-BYmStvQP.js} +3 -3
  78. package/dist/{transcript-anchors-DGUqw92H.js.map → transcript-anchors-BYmStvQP.js.map} +1 -1
  79. package/dist/tui.d.ts +3 -3
  80. package/dist/tui.js +4 -4
  81. package/dist/{turn-operations-C_XvfBU3.d.ts → turn-operations-JtKQfSFC.d.ts} +3 -3
  82. package/dist/{turn-operations-C_XvfBU3.d.ts.map → turn-operations-JtKQfSFC.d.ts.map} +1 -1
  83. package/dist/{types-M2DauK2D.d.ts → types-U-rIVUDw.d.ts} +11 -11
  84. package/dist/types-U-rIVUDw.d.ts.map +1 -0
  85. package/dist/types.d.ts +3 -3
  86. package/docs/EXECUTION_CONTEXT.md +5 -2
  87. package/package.json +7 -1
  88. package/dist/contexts-DWVt7MvF.js.map +0 -1
  89. package/dist/index-C8TwH34B.d.ts.map +0 -1
  90. package/dist/sandbox-DLY6uah7.d.ts.map +0 -1
  91. package/dist/types-M2DauK2D.d.ts.map +0 -1
@@ -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 { CappedSlot } from './output-cap'\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, ListFilesEntry, SpawnConfig, StreamHandle } from './types'\nimport { Buffer } from 'node:buffer'\nimport { alwaysQuote } from '../tools/shell-quote'\nimport { fileSizeViaExec } from './exec-file-size'\nimport { globFilesViaShell } from './glob'\nimport { appendCapped, DEFAULT_MAX_OUTPUT_BUFFER, outputExceededMessage } from './output-cap'\n\n/**\n * `readFile` transport cap: headroom above the whole-read tool ceiling\n * (256MB in tools/whole-read-guard.ts) so tool policy — not the docker\n * transport — decides read limits, keeping refusal behavior consistent\n * across contexts. Still bounded: a probe-failure path can't buffer\n * more than this into host memory.\n */\nexport const READ_FILE_MAX_BUFFER = 512 * 1024 * 1024\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 /**\n * The exec transport with an explicit output cap. The public `exec`\n * uses the shared 10MB default; `readFile` passes headroom above the\n * whole-read tool ceiling so docker's EFFECTIVE read limit matches\n * every other context (the fileSize probe refuses oversized files\n * before this ever runs — the transport cap is the safety net for\n * probe-failure paths, not the primary bound).\n */\n async function execCapped(handle: ExecutionHandle, command: string, options: { cwd?: string, env?: Record<string, string>, timeout?: number } | undefined, maxBuffer: 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 // Byte-capped accumulation via the shared exec-transport primitive\n // (contexts/output-cap.ts) — the docker `readFile` is `cat` through\n // this very path, so an uncapped sink would buffer a container's\n // multi-GB file straight into HOST memory. On overflow the stream\n // is destroyed and the exec resolves exit 143 with a message\n // (mirroring the process context), so `readFile` THROWS instead of\n // silently returning truncated content — silent truncation would\n // corrupt `edit`'s read-modify-write into data loss.\n const stdoutSlot: CappedSlot = { text: '', bytes: 0 }\n const stderrSlot: CappedSlot = { text: '', bytes: 0 }\n let resolved = false\n\n const overflow = (): void => {\n if (resolved)\n return\n resolved = true\n try {\n stream.destroy?.()\n }\n catch { /* swallow */ }\n resolve({\n stdout: stdoutSlot.text,\n stderr: `${stderrSlot.text ? `${stderrSlot.text}\\n` : ''}${outputExceededMessage(maxBuffer, 'stream detached (server-side timeout reaps the process)')}`,\n exitCode: 143,\n })\n }\n\n const cappedWrite = (slot: CappedSlot) => (chunk: Buffer | string): boolean => {\n const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : chunk\n if (appendCapped(slot, buf, maxBuffer) !== 'ok')\n overflow()\n return true\n }\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: cappedWrite(stdoutSlot),\n end() {},\n on() {},\n once() {},\n emit() { return true },\n }\n const stderrSink = {\n write: cappedWrite(stderrSlot),\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 // Same cap: TTY mode is still a host-memory sink.\n stream.on('data', (chunk: Buffer) => {\n cappedWrite(stdoutSlot)(chunk)\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: stdoutSlot.text, stderr: stderrSlot.text ? `${stderrSlot.text}\\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: stdoutSlot.text,\n stderr: inspectNote ? (stderrSlot.text ? `${stderrSlot.text}\\n${inspectNote}` : inspectNote) : stderrSlot.text,\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: stdoutSlot.text, stderr: stderrSlot.text ? `${stderrSlot.text}\\n${err.message}` : err.message, exitCode: 1 })\n })\n })\n }\n\n const ctx: ExecutionContext = {\n type: 'docker',\n\n capabilities: {\n shell: true,\n filesystem: true,\n glob: true,\n network: true,\n gpu: false,\n stream: true,\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 return execCapped(handle, command, options, DEFAULT_MAX_OUTPUT_BUFFER)\n },\n\n /**\n * Live bidirectional stream to a process in the container — see\n * `ExecutionContext.execStream` for the seam contract.\n *\n * Spawns the `docker exec -i` CLI (see the runtime note in the body for\n * why dockerode's hijack API is off the table), which adds a requirement\n * the rest of this context doesn't have: the docker CLI must be on\n * PATH — everything else here talks straight to the daemon socket.\n * Deliberately NOT wrapped in coreutils `timeout` the way `execCapped`\n * is — this seam's whole purpose is processes that outlive a single\n * request.\n */\n async execStream(\n handle: ExecutionHandle,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, signal?: AbortSignal },\n ): Promise<StreamHandle> {\n const ref = containers.get(handle.id)\n if (!ref)\n throw new Error(`Container ${handle.id} not found`)\n\n options?.signal?.throwIfAborted()\n\n // Deliberately the `docker exec -i` CLI rather than dockerode, which\n // every other method here uses. Dockerode's hijacked exec (the only\n // way to get a writable stdin out of the HTTP API) rides on\n // docker-modem's HTTP 101 connection-upgrade handling, and that does\n // not work under Bun: the modem either reports the 101 as an\n // unexpected status or its start callback never fires at all. Same\n // code path is fine under Node, so this is a runtime incompatibility\n // rather than a misuse. The CLI gives real OS pipes, split streams,\n // and identical behaviour on both runtimes; it honours DOCKER_HOST\n // and friends, so remote daemons still work.\n const spawnChild = (await import('node:child_process')).spawn\n const { randomBytes } = await import('node:crypto')\n\n // `docker exec` does not propagate a signal to the in-container\n // process when the CLI dies, so killing the local child would leave\n // the server running in the container. Park the real PID in a file\n // and signal it from a second exec. A file rather than a stdout\n // marker, because stdout here is a protocol stream that must not\n // carry our bookkeeping.\n //\n // `setsid -w` puts the command in a fresh session: the inner shell\n // calls setsid() so its `$` IS the process-group id, and signalling\n // `-$pid` (see `kill` below) reaches the whole tree. Without a fresh group,\n // signalling the recorded pid alone hits only the sh that exec'd\n // into the server — wrapper launchers like `npx` fork the real\n // server as a child, which would survive the kill and pile up in a\n // long-lived container across MCP reconnects.\n //\n // The `-w` matters: bare `setsid` forks and its parent exits\n // immediately, so `docker exec` sees its process die and tears the\n // stream down (observed: instant exit 0, stdin closed). `set -m`\n // job control is no alternative either — dash turns it off without\n // a tty and reroutes the background job's stdin to /dev/null. The\n // runtime probe covers images whose setsid lacks `-w`: they fall\n // back to no process group, where the pid-only kill fallback below\n // still reaches the direct child.\n const pidFile = `/tmp/.zidane-stream-${randomBytes(6).toString('hex')}.pid`\n const envPrefix = Object.entries(options?.env ?? {})\n .map(([k, v]) => `${k}=${alwaysQuote(v)} `)\n .join('')\n const leaderScript = `echo $$ > ${pidFile}; exec ${envPrefix ? `env ${envPrefix}` : ''}sh -c ${alwaysQuote(command)}`\n const quotedLeader = alwaysQuote(leaderScript)\n const inner = `if setsid -w true 2>/dev/null; then exec setsid -w sh -c ${quotedLeader}; else exec sh -c ${quotedLeader}; fi`\n\n const child = spawnChild('docker', [\n 'exec',\n '-i',\n '-w',\n options?.cwd ?? handle.cwd,\n ref.container.id,\n 'sh',\n '-c',\n inner,\n ], { stdio: ['pipe', 'pipe', 'pipe'] })\n\n let spawnError: Error | null = null\n child.on('error', (err) => { spawnError = err })\n\n const exited = new Promise<number>((resolveExit) => {\n child.once('close', (code, signalName) => {\n resolveExit(code ?? (signalName === 'SIGTERM' ? 143 : signalName ? 128 : 1))\n })\n })\n\n // Read back the in-container PID the wrapper just recorded. The seam\n // promises a PID that is meaningful INSIDE the context, so returning\n // the local CLI's pid would be a lie.\n let pid = -1\n for (let attempt = 0; attempt < 20 && pid <= 0; attempt++) {\n const probe = await execCapped(handle, `cat ${pidFile} 2>/dev/null || true`, undefined, 4096)\n .catch(() => null)\n const parsed = Number.parseInt(probe?.stdout.trim() ?? '', 10)\n if (Number.isFinite(parsed) && parsed > 0) {\n pid = parsed\n break\n }\n if (child.exitCode !== null)\n break\n await new Promise(r => setTimeout(r, 50))\n }\n\n const kill = async (): Promise<void> => {\n if (pid > 0) {\n // Group first (`setsid -w` above makes `pid` the PGID), pid-only\n // as a fallback for images that took the no-setsid launch branch.\n // No `--` before the negative pid: dash's kill builtin rejects it\n // (\"Illegal number: -\"), while both dash and busybox ash accept\n // the bare `-pgid` after an explicit signal (verified on oven/bun\n // and alpine).\n await execCapped(handle, `kill -TERM -${pid} 2>/dev/null || kill -TERM ${pid} 2>/dev/null || true`, undefined, 4096)\n .catch(() => {})\n }\n await execCapped(handle, `rm -f ${pidFile}`, undefined, 4096).catch(() => {})\n if (child.exitCode === null && child.signalCode === null)\n child.kill('SIGTERM')\n }\n\n if (options?.signal) {\n if (options.signal.aborted) {\n void kill()\n }\n else {\n // Detach on exit — the (typically run-long) signal otherwise\n // retains the `kill` closure and the child handle for the rest\n // of the run, one dead closure per exec.\n const onAbort = () => { void kill() }\n const detach = () => options.signal!.removeEventListener('abort', onAbort)\n options.signal.addEventListener('abort', onAbort, { once: true })\n exited.then(detach, detach)\n }\n }\n\n return {\n pid,\n stdout: child.stdout!,\n stderr: child.stderr!,\n write: async (data: Uint8Array): Promise<void> => {\n if (spawnError)\n throw spawnError\n const stdin = child.stdin\n if (!stdin || stdin.destroyed)\n throw new Error(`execStream: stdin is closed (container ${handle.id})`)\n await new Promise<void>((resolveWrite, rejectWrite) => {\n stdin.write(data, err => err ? rejectWrite(err) : resolveWrite())\n })\n },\n kill,\n exited,\n }\n },\n\n async fileSize(handle: ExecutionHandle, path: string): Promise<number | null> {\n // Shared exec-probed capability (see exec-file-size.ts) — the same\n // implementation as the sandbox contexts, for a CONSISTENT\n // whole-read experience: tools refuse >ceiling files with the same\n // message everywhere, instead of docker surfacing a transport\n // error at a different threshold.\n return fileSizeViaExec(command => execCapped(handle, command, undefined, DEFAULT_MAX_OUTPUT_BUFFER), path)\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 // Transport headroom ABOVE the whole-read tool ceiling (256MB,\n // enforced via the fileSize probe BEFORE this runs): without it,\n // docker's effective read limit would be the 10MB exec default —\n // inconsistent with every other context and surfaced as a\n // confusing transport error instead of the uniform refusal.\n const result = await execCapped(handle, `cat ${alwaysQuote(path)}`, undefined, READ_FILE_MAX_BUFFER)\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, options): Promise<ListFilesEntry[]> {\n if (options?.glob)\n return globFilesViaShell(path, command => ctx.exec(handle, command), options?.limit ?? 1000)\n // `-p` suffixes directories with `/` — parity with the process\n // context's dirent-aware listing.\n const result = await ctx.exec(handle, `ls -1p ${alwaysQuote(path)}`)\n // Surface `ls` stderr instead of masking failures as an empty dir —\n // the `list_files` tool differentiates \"No such file\" / \"Not a\n // directory\" / \"Permission denied\" from its catch handler.\n if (result.exitCode !== 0)\n throw new Error(result.stderr.trim() || `ls failed with exit ${result.exitCode}`)\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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"}
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 { CappedSlot } from './output-cap'\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, ListFilesEntry, SpawnConfig, StreamHandle } from './types'\nimport { Buffer } from 'node:buffer'\nimport { posix } from 'node:path'\nimport { alwaysQuote } from '../tools/shell-quote'\nimport { fileSizeViaExec } from './exec-file-size'\nimport { globFilesViaShell } from './glob'\nimport { appendCapped, DEFAULT_MAX_OUTPUT_BUFFER, outputExceededMessage } from './output-cap'\n\n/**\n * `readFile` transport cap: headroom above the whole-read tool ceiling\n * (256MB in tools/whole-read-guard.ts) so tool policy — not the docker\n * transport — decides read limits, keeping refusal behavior consistent\n * across contexts. Still bounded: a probe-failure path can't buffer\n * more than this into host memory.\n */\nexport const READ_FILE_MAX_BUFFER = 512 * 1024 * 1024\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 /**\n * The exec transport with an explicit output cap. The public `exec`\n * uses the shared 10MB default; `readFile` passes headroom above the\n * whole-read tool ceiling so docker's EFFECTIVE read limit matches\n * every other context (the fileSize probe refuses oversized files\n * before this ever runs — the transport cap is the safety net for\n * probe-failure paths, not the primary bound).\n */\n async function execCapped(handle: ExecutionHandle, command: string, options: { cwd?: string, env?: Record<string, string>, timeout?: number } | undefined, maxBuffer: 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 // Byte-capped accumulation via the shared exec-transport primitive\n // (contexts/output-cap.ts) — the docker `readFile` is `cat` through\n // this very path, so an uncapped sink would buffer a container's\n // multi-GB file straight into HOST memory. On overflow the stream\n // is destroyed and the exec resolves exit 143 with a message\n // (mirroring the process context), so `readFile` THROWS instead of\n // silently returning truncated content — silent truncation would\n // corrupt `edit`'s read-modify-write into data loss.\n const stdoutSlot: CappedSlot = { text: '', bytes: 0 }\n const stderrSlot: CappedSlot = { text: '', bytes: 0 }\n let resolved = false\n\n const overflow = (): void => {\n if (resolved)\n return\n resolved = true\n try {\n stream.destroy?.()\n }\n catch { /* swallow */ }\n resolve({\n stdout: stdoutSlot.text,\n stderr: `${stderrSlot.text ? `${stderrSlot.text}\\n` : ''}${outputExceededMessage(maxBuffer, 'stream detached (server-side timeout reaps the process)')}`,\n exitCode: 143,\n })\n }\n\n const cappedWrite = (slot: CappedSlot) => (chunk: Buffer | string): boolean => {\n const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : chunk\n if (appendCapped(slot, buf, maxBuffer) !== 'ok')\n overflow()\n return true\n }\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: cappedWrite(stdoutSlot),\n end() {},\n on() {},\n once() {},\n emit() { return true },\n }\n const stderrSink = {\n write: cappedWrite(stderrSlot),\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 // Same cap: TTY mode is still a host-memory sink.\n stream.on('data', (chunk: Buffer) => {\n cappedWrite(stdoutSlot)(chunk)\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: stdoutSlot.text, stderr: stderrSlot.text ? `${stderrSlot.text}\\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: stdoutSlot.text,\n stderr: inspectNote ? (stderrSlot.text ? `${stderrSlot.text}\\n${inspectNote}` : inspectNote) : stderrSlot.text,\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: stdoutSlot.text, stderr: stderrSlot.text ? `${stderrSlot.text}\\n${err.message}` : err.message, exitCode: 1 })\n })\n })\n }\n\n const ctx: ExecutionContext = {\n type: 'docker',\n\n capabilities: {\n shell: true,\n filesystem: true,\n glob: true,\n network: true,\n gpu: false,\n stream: true,\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 return execCapped(handle, command, options, DEFAULT_MAX_OUTPUT_BUFFER)\n },\n\n /**\n * Live bidirectional stream to a process in the container — see\n * `ExecutionContext.execStream` for the seam contract.\n *\n * Spawns the `docker exec -i` CLI (see the runtime note in the body for\n * why dockerode's hijack API is off the table), which adds a requirement\n * the rest of this context doesn't have: the docker CLI must be on\n * PATH — everything else here talks straight to the daemon socket.\n * Deliberately NOT wrapped in coreutils `timeout` the way `execCapped`\n * is — this seam's whole purpose is processes that outlive a single\n * request.\n */\n async execStream(\n handle: ExecutionHandle,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, signal?: AbortSignal },\n ): Promise<StreamHandle> {\n const ref = containers.get(handle.id)\n if (!ref)\n throw new Error(`Container ${handle.id} not found`)\n\n options?.signal?.throwIfAborted()\n\n // Deliberately the `docker exec -i` CLI rather than dockerode, which\n // every other method here uses. Dockerode's hijacked exec (the only\n // way to get a writable stdin out of the HTTP API) rides on\n // docker-modem's HTTP 101 connection-upgrade handling, and that does\n // not work under Bun: the modem either reports the 101 as an\n // unexpected status or its start callback never fires at all. Same\n // code path is fine under Node, so this is a runtime incompatibility\n // rather than a misuse. The CLI gives real OS pipes, split streams,\n // and identical behaviour on both runtimes; it honours DOCKER_HOST\n // and friends, so remote daemons still work.\n const spawnChild = (await import('node:child_process')).spawn\n const { randomBytes } = await import('node:crypto')\n\n // `docker exec` does not propagate a signal to the in-container\n // process when the CLI dies, so killing the local child would leave\n // the server running in the container. Park the real PID in a file\n // and signal it from a second exec. A file rather than a stdout\n // marker, because stdout here is a protocol stream that must not\n // carry our bookkeeping.\n //\n // `setsid -w` puts the command in a fresh session: the inner shell\n // calls setsid() so its `$` IS the process-group id, and signalling\n // `-$pid` (see `kill` below) reaches the whole tree. Without a fresh group,\n // signalling the recorded pid alone hits only the sh that exec'd\n // into the server — wrapper launchers like `npx` fork the real\n // server as a child, which would survive the kill and pile up in a\n // long-lived container across MCP reconnects.\n //\n // The `-w` matters: bare `setsid` forks and its parent exits\n // immediately, so `docker exec` sees its process die and tears the\n // stream down (observed: instant exit 0, stdin closed). `set -m`\n // job control is no alternative either — dash turns it off without\n // a tty and reroutes the background job's stdin to /dev/null. The\n // runtime probe covers images whose setsid lacks `-w`: they fall\n // back to no process group, where the pid-only kill fallback below\n // still reaches the direct child.\n const pidFile = `/tmp/.zidane-stream-${randomBytes(6).toString('hex')}.pid`\n const envPrefix = Object.entries(options?.env ?? {})\n .map(([k, v]) => `${k}=${alwaysQuote(v)} `)\n .join('')\n const leaderScript = `echo $$ > ${pidFile}; exec ${envPrefix ? `env ${envPrefix}` : ''}sh -c ${alwaysQuote(command)}`\n const quotedLeader = alwaysQuote(leaderScript)\n const inner = `if setsid -w true 2>/dev/null; then exec setsid -w sh -c ${quotedLeader}; else exec sh -c ${quotedLeader}; fi`\n\n const child = spawnChild('docker', [\n 'exec',\n '-i',\n '-w',\n options?.cwd ?? handle.cwd,\n ref.container.id,\n 'sh',\n '-c',\n inner,\n ], { stdio: ['pipe', 'pipe', 'pipe'] })\n\n let spawnError: Error | null = null\n child.on('error', (err) => { spawnError = err })\n\n const exited = new Promise<number>((resolveExit) => {\n child.once('close', (code, signalName) => {\n resolveExit(code ?? (signalName === 'SIGTERM' ? 143 : signalName ? 128 : 1))\n })\n })\n\n // Read back the in-container PID the wrapper just recorded. The seam\n // promises a PID that is meaningful INSIDE the context, so returning\n // the local CLI's pid would be a lie.\n let pid = -1\n for (let attempt = 0; attempt < 20 && pid <= 0; attempt++) {\n const probe = await execCapped(handle, `cat ${pidFile} 2>/dev/null || true`, undefined, 4096)\n .catch(() => null)\n const parsed = Number.parseInt(probe?.stdout.trim() ?? '', 10)\n if (Number.isFinite(parsed) && parsed > 0) {\n pid = parsed\n break\n }\n if (child.exitCode !== null)\n break\n await new Promise(r => setTimeout(r, 50))\n }\n\n const kill = async (): Promise<void> => {\n if (pid > 0) {\n // Group first (`setsid -w` above makes `pid` the PGID), pid-only\n // as a fallback for images that took the no-setsid launch branch.\n // No `--` before the negative pid: dash's kill builtin rejects it\n // (\"Illegal number: -\"), while both dash and busybox ash accept\n // the bare `-pgid` after an explicit signal (verified on oven/bun\n // and alpine).\n await execCapped(handle, `kill -TERM -${pid} 2>/dev/null || kill -TERM ${pid} 2>/dev/null || true`, undefined, 4096)\n .catch(() => {})\n }\n await execCapped(handle, `rm -f ${pidFile}`, undefined, 4096).catch(() => {})\n if (child.exitCode === null && child.signalCode === null)\n child.kill('SIGTERM')\n }\n\n if (options?.signal) {\n if (options.signal.aborted) {\n void kill()\n }\n else {\n // Detach on exit — the (typically run-long) signal otherwise\n // retains the `kill` closure and the child handle for the rest\n // of the run, one dead closure per exec.\n const onAbort = () => { void kill() }\n const detach = () => options.signal!.removeEventListener('abort', onAbort)\n options.signal.addEventListener('abort', onAbort, { once: true })\n exited.then(detach, detach)\n }\n }\n\n return {\n pid,\n stdout: child.stdout!,\n stderr: child.stderr!,\n write: async (data: Uint8Array): Promise<void> => {\n if (spawnError)\n throw spawnError\n const stdin = child.stdin\n if (!stdin || stdin.destroyed)\n throw new Error(`execStream: stdin is closed (container ${handle.id})`)\n await new Promise<void>((resolveWrite, rejectWrite) => {\n stdin.write(data, err => err ? rejectWrite(err) : resolveWrite())\n })\n },\n kill,\n exited,\n }\n },\n\n async fileSize(handle: ExecutionHandle, path: string): Promise<number | null> {\n // Shared exec-probed capability (see exec-file-size.ts) — the same\n // implementation as the sandbox contexts, for a CONSISTENT\n // whole-read experience: tools refuse >ceiling files with the same\n // message everywhere, instead of docker surfacing a transport\n // error at a different threshold.\n return fileSizeViaExec(command => execCapped(handle, command, undefined, DEFAULT_MAX_OUTPUT_BUFFER), path)\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 // Transport headroom ABOVE the whole-read tool ceiling (256MB,\n // enforced via the fileSize probe BEFORE this runs): without it,\n // docker's effective read limit would be the 10MB exec default —\n // inconsistent with every other context and surfaced as a\n // confusing transport error instead of the uniform refusal.\n const result = await execCapped(handle, `cat ${alwaysQuote(path)}`, undefined, READ_FILE_MAX_BUFFER)\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 | Buffer, encoding: BufferEncoding = 'utf-8'): Promise<void> {\n // ONE path for text and bytes: docker `putArchive` with an in-memory\n // ustar. A printf-through-exec text path was tried and removed: it\n // ships the whole payload inside the `sh -c` argv, which Linux caps\n // at MAX_ARG_STRLEN (128KB) — a larger text file died with\n // \"argument list too long\" (live-verified). putArchive has no size\n // ceiling, needs no shell escaping of the content, and preserves\n // image / PDF bytes that a utf-8 shell write would mangle.\n const bytes = typeof content === 'string' ? Buffer.from(content, encoding) : content\n\n const ref = containers.get(handle.id)\n if (!ref)\n throw new Error(`Container ${handle.id} not found`)\n\n // Resolve relative (model-supplied) paths against the handle cwd —\n // putArchive has no per-call WorkingDir the way exec does. Container\n // paths are POSIX whatever the HOST platform is, hence `posix.isAbsolute`\n // rather than the platform-flavored one (which calls `C:/x` absolute).\n const dest = posix.isAbsolute(path) ? path : posix.join(handle.cwd, path)\n const dir = posix.dirname(dest)\n const name = posix.basename(dest)\n // A trailing slash names a directory, and `basename` would silently\n // retarget it at the parent's last segment — refuse instead, the way\n // the shell refused `> 'dir/'`.\n if (!name || name === '.' || name === '..' || path.endsWith('/'))\n throw new Error(`Failed to write file: invalid path ${path}`)\n\n const mkdir = await ctx.exec(handle, `mkdir -p ${alwaysQuote(dir)}`)\n if (mkdir.exitCode !== 0)\n throw new Error(`Failed to write file: ${mkdir.stderr}`)\n\n // putArchive extracts AS THE DAEMON, so it never touches the target\n // directly: it would land root-owned with the tar header's mode (0644,\n // silently dropping an existing `+x`), and with the API's default\n // `noOverwriteDirNonDir=false` a DIRECTORY at the target gets\n // RemoveAll'd and replaced by the file. Instead the bytes go to a\n // staging file and the SHELL performs the final write: `cat > dest`\n // truncates in place, so the target keeps its mode, its owner and its\n // symlink target, a directory target still fails with \"Is a\n // directory\", and the container user's own permissions still apply.\n // (`mv` would defeat the point — replacing the inode is exactly what\n // loses mode and owner.)\n //\n // Staged in `dir` rather than /tmp so it shares the target's\n // writability domain (readonly rootfs + writable volume mount), and\n // dot-prefixed to stay out of `ls -1p` (this context's own listFiles)\n // and of default shell glob expansion while it exists.\n const stagedName = `.zidane-write-${Math.random().toString(36).slice(2, 10)}.tmp`\n const staged = posix.join(dir, stagedName)\n\n try {\n await ref.container.putArchive(await packTarFile(stagedName, bytes), { path: dir })\n }\n catch (err) {\n throw new Error(`Failed to write file: ${err instanceof Error ? err.message : String(err)}`)\n }\n\n // `rc` is captured BEFORE the cleanup so a failed `rm` cannot mask the\n // real write outcome. That cleanup is BEST EFFORT in both directions:\n // `rm` needs write permission on `dir`, which the exec user can lack\n // even when the write itself succeeded (root-owned parent, or a bind\n // mount owned by another uid), and a server-side timeout kills the shell\n // before it runs at all. Worst case is a stray `.zidane-write-*.tmp`\n // that no exec can unlink — only the daemon could write there.\n const write = await ctx.exec(handle, `cat ${alwaysQuote(staged)} > ${alwaysQuote(dest)}; rc=$?; rm -f ${alwaysQuote(staged)}; exit $rc`)\n if (write.exitCode !== 0) {\n // The shell may have died before reaching its own cleanup, so a failed\n // write retries it once in a fresh exec.\n await ctx.exec(handle, `rm -f ${alwaysQuote(staged)}`).catch(() => {})\n throw new Error(`Failed to write file: ${write.stderr}`)\n }\n },\n\n async listFiles(handle: ExecutionHandle, path: string, options): Promise<ListFilesEntry[]> {\n if (options?.glob)\n return globFilesViaShell(path, command => ctx.exec(handle, command), options?.limit ?? 1000)\n // `-p` suffixes directories with `/` — parity with the process\n // context's dirent-aware listing.\n const result = await ctx.exec(handle, `ls -1p ${alwaysQuote(path)}`)\n // Surface `ls` stderr instead of masking failures as an empty dir —\n // the `list_files` tool differentiates \"No such file\" / \"Not a\n // directory\" / \"Permission denied\" from its catch handler.\n if (result.exitCode !== 0)\n throw new Error(result.stderr.trim() || `ls failed with exit ${result.exitCode}`)\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\n/**\n * Pack a single file into an in-memory tar archive for `putArchive`, via\n * `tar-stream` (the same battle-tested packer the dockerode ecosystem uses\n * for build contexts). Optional peer dependency, loaded lazily like\n * dockerode itself so the module graph stays lean for non-docker users.\n */\nexport async function packTarFile(name: string, data: Buffer): Promise<Buffer> {\n let pack: typeof import('tar-stream')['pack']\n try {\n pack = (await import('tar-stream')).pack\n }\n catch {\n throw new Error('tar-stream is required for Docker writeFile. Install it with: bun add tar-stream')\n }\n\n const tar = pack()\n tar.entry({ name }, data)\n tar.finalize()\n\n const chunks: Buffer[] = []\n for await (const chunk of tar)\n chunks.push(chunk as Buffer)\n return Buffer.concat(chunks)\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { Yu as Logger } from "../index-ZWibV230.js";
2
- import { t as SandboxProvider } from "../sandbox-DLY6uah7.js";
1
+ import { Yu as Logger } from "../index-A1qz0l55.js";
2
+ import { t as SandboxProvider } from "../sandbox-CDl9tfTo.js";
3
3
 
4
4
  //#region src/contexts/e2b.d.ts
5
5
  /**
@@ -53,7 +53,14 @@ interface E2BSandbox {
53
53
  };
54
54
  files: {
55
55
  read: (path: string) => Promise<string>;
56
- write: (path: string, data: string) => Promise<unknown>;
56
+ /**
57
+ * Deliberately WIDER than the SDK's declared input (string | ArrayBuffer |
58
+ * Blob | ReadableStream): every upload path there ends at `toBlob`, whose
59
+ * fallback is `new Blob([data])` — a BlobPart, so a Uint8Array view
60
+ * (byteOffset-aware) uploads verbatim. Strings still travel as strings, so
61
+ * only a byte write depends on this widening.
62
+ */
63
+ write: (path: string, data: string | Uint8Array) => Promise<unknown>;
57
64
  list: (path: string) => Promise<E2BEntry[]>;
58
65
  };
59
66
  kill: () => Promise<unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"e2b.d.ts","names":[],"sources":["../../src/contexts/e2b.ts"],"mappings":";;;;;;;;;;AAuGsC;AAAA;;;;UArC5B,oBAAA;EACR,UAAA;EACA,KAAA;EACA,SAAA;EACA,GAAA;EACA,IAAA,GAAO,MAAM;EACb,QAAA,IAAY,IAAA;EACZ,QAAA,IAAY,IAAA;AAAA;AAAA,UAYJ,gBAAA;EACR,MAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGQ,QAAA;EACR,IAAA;EAkCyB;EAhCzB,IAAI;AAAA;;;;;UAOI,gBAAA;EACR,GAAA;EACA,IAAA,QAAY,OAAO,CAAC,gBAAA;AAAA;AAAA,UAGZ,UAAA;EACR,SAAA;EACA,QAAA;IACE,GAAA,IAAO,GAAA,UAAa,IAAA;MAAS,GAAA;MAAc,IAAA,GAAO,MAAA;MAAwB,SAAA;IAAA,MAAyB,OAAA,CAAQ,gBAAA,OACrG,GAAA,UAAa,IAAA,EAAM,oBAAA,KAAyB,OAAA,CAAQ,gBAAA,IAEjB;IAAzC,SAAA,IAAa,GAAA,UAAa,IAAA,WAAe,UAAA,KAAe,OAAA;IACxD,IAAA,IAAQ,GAAA,aAAgB,OAAA;EAAA;EAE1B,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;EAKZ,KAAA,SAAc,OAAA;EACd,SAAA,SAAkB,OAAA;AAAA;AAAA,UA0BH,kBAAA;EAlCb;EAoCF,MAAA;EApC0B;;;;;EA0C1B,MAAA;EAlCA;;;AAAyB;AA0B3B;EAcE,QAAA;;;;;;;;;EASA,SAAA;EAgBM;;;;EAXN,GAAA;EAkCY;;;;EA7BZ,cAAA;EA0EoB;;;;;EApEpB,GAAA,GAAM,MAAA;EAwEL;;;;;EAlED,MAAA,GAAS,MAAM;EAgEf;;;;;;;EAxDA,uBAAA;EA0DQ;AAAA;AA8BV;;;;;;EA/EE,OAAA;IAAY,IAAA;IAAc,OAAA;EAAA;EA4FK;AAAA;AAgBjC;;;;;;;;;;;;;;EA3FE,SAAA;AAAA;;AA+FQ;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"}
1
+ {"version":3,"file":"e2b.d.ts","names":[],"sources":["../../src/contexts/e2b.ts"],"mappings":";;;;;;;;;;AAwGsC;AAAA;;;;UArC5B,oBAAA;EACR,UAAA;EACA,KAAA;EACA,SAAA;EACA,GAAA;EACA,IAAA,GAAO,MAAM;EACb,QAAA,IAAY,IAAA;EACZ,QAAA,IAAY,IAAA;AAAA;AAAA,UAYJ,gBAAA;EACR,MAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGQ,QAAA;EACR,IAAA;EAyCyB;EAvCzB,IAAI;AAAA;;;;;UAOI,gBAAA;EACR,GAAA;EACA,IAAA,QAAY,OAAO,CAAC,gBAAA;AAAA;AAAA,UAGZ,UAAA;EACR,SAAA;EACA,QAAA;IACE,GAAA,IAAO,GAAA,UAAa,IAAA;MAAS,GAAA;MAAc,IAAA,GAAO,MAAA;MAAwB,SAAA;IAAA,MAAyB,OAAA,CAAQ,gBAAA,OACrG,GAAA,UAAa,IAAA,EAAM,oBAAA,KAAyB,OAAA,CAAQ,gBAAA,IAE7C;IAAb,SAAA,IAAa,GAAA,UAAa,IAAA,WAAe,UAAA,KAAe,OAAA;IACxD,IAAA,IAAQ,GAAA,aAAgB,OAAA;EAAA;EAE1B,KAAA;IACE,IAAA,GAAO,IAAA,aAAiB,OAAA;IAHA;;;;;;;IAWxB,KAAA,GAAQ,IAAA,UAAc,IAAA,WAAe,UAAA,KAAe,OAAA;IACpD,IAAA,GAAO,IAAA,aAAiB,OAAA,CAAQ,QAAA;EAAA;EAElC,IAAA,QAAY,OAAA;EAKZ,KAAA,SAAc,OAAA;EACd,SAAA,SAAkB,OAAA;AAAA;AAAA,UA0BH,kBAAA;EAhCH;EAkCZ,MAAA;EA7Bc;;;;AACW;EAkCzB,MAAA;EARiC;;;;;EAcjC,QAAA;EASA;;;;;;;;EAAA,SAAA;EAuCY;;;;EAlCZ,GAAA;EA+EoB;;;;EA1EpB,cAAA;EA6E4D;;;;;EAvE5D,GAAA,GAAM,MAAA;EAsEE;;;;;EAhER,MAAA,GAAS,MAAM;EAiE6C;;;;AACpD;AA8BV;;EAxFE,uBAAA;EAqG+B;;;;;;;;EA5F/B,OAAA;IAAY,IAAA;IAAc,OAAA;EAAA;EA6GjB;;;;;;;;;;;;;;AAGD;AAmCV;EAlIE,SAAA;AAAA;;;;;;;AAqIqD;AAWvD;;;;;iBApHsB,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;EAqFd;EAnFD,cAAA;EAmFO;AAgBT;;;;;EA5FE,aAAA;EA4FmE;EA1FnE,GAAA;EA0FkF;EAxFlF,KAAA,IAAS,EAAA,aAAe,OAAO;AAAA;;;;;;;;;;;;;;iBAgBX,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"}
@@ -2,6 +2,7 @@ import { r as capBufferedExecOutput } from "../output-cap-C9zukcCj.js";
2
2
  import { t as alwaysQuote } from "../shell-quote-BmnhZmdM.js";
3
3
  import { r as createLogger, t as consoleSink } from "../logger-DBX9uYOw.js";
4
4
  import { t as StreamQueue } from "../stream-queue-Cd9OJxl7.js";
5
+ import { Buffer } from "node:buffer";
5
6
  //#region src/contexts/e2b.ts
6
7
  /**
7
8
  * "Effectively unlimited" command timeout for `execStream`. 2^31-1 ms
@@ -356,8 +357,9 @@ function createE2BProvider(options = {}) {
356
357
  async readFile(sandboxId, path) {
357
358
  return get(sandboxId).sandbox.files.read(path);
358
359
  },
359
- async writeFile(sandboxId, path, content) {
360
- await get(sandboxId).sandbox.files.write(path, content);
360
+ async writeFile(sandboxId, path, content, encoding = "utf-8") {
361
+ const data = typeof content === "string" ? encoding === "utf-8" || encoding === "utf8" ? content : Buffer.from(content, encoding) : content;
362
+ await get(sandboxId).sandbox.files.write(path, data);
361
363
  },
362
364
  async listFiles(sandboxId, path) {
363
365
  return (await get(sandboxId).sandbox.files.list(path)).map((e) => e.type === "dir" ? `${e.name}/` : e.name);
@@ -1 +1 @@
1
- {"version":3,"file":"e2b.js","names":[],"sources":["../../src/contexts/e2b.ts"],"sourcesContent":["/**\n * E2B sandbox provider.\n *\n * Implements {@link SandboxProvider} on top of E2B's firecracker-backed\n * micro-VMs. Pair it with {@link createSandboxContext} to run an agent's\n * shell + filesystem tools inside an ephemeral E2B sandbox:\n *\n * ```ts\n * import { createSandboxContext } from 'zidane/contexts'\n * import { createE2BProvider } from 'zidane/contexts/e2b'\n *\n * const execution = createSandboxContext(createE2BProvider())\n * ```\n *\n * ## Pointing at your own E2B\n *\n * The two knobs that select *which* E2B you talk to are `apiKey` and\n * `domain`. Self-hosted / on-prem E2B clusters expose a custom domain; pass\n * it here (or via the `E2B_DOMAIN` env var) and the SDK routes every call to\n * your cluster instead of E2B's hosted API. Precedence, highest first:\n *\n * 1. Per-spawn `SpawnConfig.sandbox` fields (`apiKey`, `domain`, `template`)\n * 2. {@link E2BProviderOptions} passed to this factory\n * 3. `E2B_API_KEY` / `E2B_DOMAIN` env vars (read by the SDK itself), and\n * `E2B_TEMPLATE` (read by this provider — the SDK does not read it)\n *\n * The default `base` template does not exist on self-hosted clusters, so\n * picking the right template matters: an invalid template still returns a\n * sandbox id from the control plane, but its envd never boots and every\n * `exec`/file op then hangs until it times out. Set `E2B_TEMPLATE` (or pass\n * `template`) to a template that actually exists on your cluster.\n *\n * Requires `e2b@^2` as an optional peer dependency:\n * `bun add e2b`.\n */\n\nimport type { Logger } from '../logger'\nimport type { SandboxProvider } from './sandbox'\nimport type { ExecResult, SpawnConfig, StreamHandle } from './types'\nimport { consoleSink, createLogger } from '../logger'\nimport { alwaysQuote } from '../tools/shell-quote'\nimport { capBufferedExecOutput } from './output-cap'\nimport { StreamQueue } from './stream-queue'\n\n// ---------------------------------------------------------------------------\n// Minimal structural views of the `e2b` SDK surface we touch. Declared locally\n// (rather than imported) so the package typechecks without the optional peer\n// dependency installed — mirroring how `docker.ts` keeps `dockerode` at arm's\n// length. The real SDK satisfies these shapes. The peer dependency floor is\n// `e2b@^2`: every member used here exists across 2.x, but the pause surface\n// shifted within it — `betaPause()` from 2.0.0, the stable `pause()` from\n// 2.27.1 — so `onDestroy: 'pause'` prefers `pause()` and falls back to\n// `betaPause()` (both optional below, resolved at runtime in `destroy`).\n// ---------------------------------------------------------------------------\n\n/**\n * Options that flip `commands.run` into background mode. Split out so the\n * overload above can discriminate on it: `background: true` changes the\n * return type from a finished result to a live handle.\n *\n * `stdin: true` and a huge `timeoutMs` are not incidental — the SDK\n * defaults are `false` and 60s, which would respectively hand the child an\n * immediate EOF and kill a session-lifetime process after a minute. See\n * {@link E2B_STREAM_TIMEOUT_MS} for why the timeout is a large number and\n * not `0`.\n */\ninterface E2BBackgroundOptions {\n background: true\n stdin: true\n timeoutMs: number\n cwd?: string\n envs?: Record<string, string>\n onStdout?: (data: string) => void\n onStderr?: (data: string) => void\n}\n\n/**\n * \"Effectively unlimited\" command timeout for `execStream`. 2^31-1 ms\n * (~24.8 days — far beyond any sandbox lifetime) because both edges are\n * cliffs: `0` is treated by `@connectrpc/connect`'s deadline signal as\n * already expired (instant DeadlineExceeded), and anything above 2^31-1\n * overflows `setTimeout` and fires immediately.\n */\nconst E2B_STREAM_TIMEOUT_MS = 2 ** 31 - 1\n\ninterface E2BCommandResult {\n stdout: string\n stderr: string\n exitCode: number\n}\n\ninterface E2BEntry {\n name: string\n /** Present in the E2B SDK's EntryInfo; used for the `/` dir suffix. */\n type?: string\n}\n\n/**\n * Handle returned by a `background: true` command. Structural subset of the\n * SDK's `CommandHandle` — only what `execStream` needs.\n */\ninterface E2BCommandHandle {\n pid: number\n wait: () => Promise<E2BCommandResult>\n}\n\ninterface E2BSandbox {\n sandboxId: string\n commands: {\n run: ((cmd: string, opts?: { cwd?: string, envs?: Record<string, string>, timeoutMs?: number }) => Promise<E2BCommandResult>)\n & ((cmd: string, opts: E2BBackgroundOptions) => Promise<E2BCommandHandle>)\n /** Present from the SDK's Commands module; used only by `execStream`. */\n sendStdin?: (pid: number, data: string | Uint8Array) => Promise<void>\n kill?: (pid: number) => Promise<boolean>\n }\n files: {\n read: (path: string) => Promise<string>\n write: (path: string, data: string) => Promise<unknown>\n list: (path: string) => Promise<E2BEntry[]>\n }\n kill: () => Promise<unknown>\n // The pause surface shifted within 2.x: `betaPause` from 2.0.0, the stable\n // `pause` from 2.27.1. Both are optional so `destroy` can prefer `pause`,\n // fall back to `betaPause`, and stay defensive if a mismatched install\n // exposes neither. Both resolve to whether the sandbox was newly paused.\n pause?: () => Promise<boolean>\n betaPause?: () => Promise<boolean>\n}\n\ninterface E2BCreateOptions {\n apiKey?: string\n domain?: string\n timeoutMs?: number\n envs?: Record<string, string>\n metadata?: Record<string, string>\n}\n\ninterface E2BConnectOptions {\n apiKey?: string\n domain?: string\n timeoutMs?: number\n}\n\ninterface E2BSandboxStatic {\n create: (templateOrOpts?: string | E2BCreateOptions, opts?: E2BCreateOptions) => Promise<E2BSandbox>\n connect: (sandboxId: string, opts?: E2BConnectOptions) => Promise<E2BSandbox>\n}\n\n// ---------------------------------------------------------------------------\n// Provider options\n// ---------------------------------------------------------------------------\n\nexport interface E2BProviderOptions {\n /** E2B API key. Falls back to the `E2B_API_KEY` env var when omitted. */\n apiKey?: string\n /**\n * E2B API domain. Set this to point at a self-hosted / on-prem cluster\n * (e.g. `e2b.my-company.internal`). Falls back to the `E2B_DOMAIN` env var,\n * then to E2B's hosted default.\n */\n domain?: string\n /**\n * Sandbox template id / name to launch. Falls back to the `E2B_TEMPLATE`\n * env var, then to E2B's hosted `base` template. Note that `base` does not\n * exist on self-hosted clusters — supply a template that does.\n */\n template?: string\n /**\n * Connect to a pre-existing sandbox by id instead of creating a fresh one\n * (E2B's `Sandbox.connect`). When set, `template` is ignored — you're\n * attaching to a sandbox that already exists. The provider treats such a\n * sandbox as externally owned: it is NOT killed on `destroy`, nor torn down\n * if the readiness probe / pregame fails, since the caller manages its\n * lifecycle. Per-spawn `SpawnConfig.sandbox.sandboxId` overrides this.\n */\n sandboxId?: string\n /**\n * Default working directory, applied when a spawn doesn't set its own\n * `SpawnConfig.cwd`. Created on spawn. Defaults to E2B's `/home/user`.\n */\n cwd?: string\n /**\n * Sandbox lifetime in seconds before E2B auto-kills it. Defaults to the\n * SDK default (300s). Per-spawn `SpawnConfig.limits.timeout` overrides this.\n */\n timeoutSeconds?: number\n /**\n * Environment variables baked into every sandbox at create time (passed to\n * E2B's `Sandbox.create({ envs })`), so every command run in the sandbox\n * sees them. Per-spawn `SpawnConfig.env` is merged over these.\n */\n env?: Record<string, string>\n /**\n * Logger for provider-level lifecycle lines (notably the readiness wait).\n * Defaults to a {@link consoleSink}-backed logger so the messages reach\n * stderr without the caller wiring one up.\n */\n logger?: Logger\n /**\n * Total deadline, in seconds, for the post-create readiness probe (see\n * {@link waitForE2BReady}). A freshly-created sandbox can return its id\n * before `envd` accepts commands; `spawn` blocks until a trivial probe\n * succeeds so the first real command never absorbs the cold start. Defaults\n * to `60`. Set to `0` to disable the gate.\n */\n readinessTimeoutSeconds?: number\n /**\n * A setup script to upload and run once the sandbox is ready, before any\n * prompting begins (see {@link runE2BPregame}). `name` is the basename used\n * for the uploaded file; `content` is the script body. The script is run\n * directly so its shebang selects the interpreter. A non-zero exit aborts\n * `spawn` (and tears the sandbox down) — a broken setup must not silently\n * hand back a half-provisioned sandbox.\n */\n pregame?: { name: string, content: string }\n /**\n * What to do with an owned sandbox when `destroy` is called:\n *\n * - `'kill'` (default) — call `sandbox.kill()`. The sandbox is deleted and\n * its state is gone.\n * - `'pause'` — call `sandbox.pause()` (falling back to the older\n * `betaPause()` on `e2b` 2.0–2.26). The sandbox's filesystem and process\n * state are preserved; reconnect later with the `sandboxId` option (E2B's\n * `Sandbox.connect` auto-resumes a paused sandbox).\n * - `'leave'` — drop the local handle without making any API call. The\n * sandbox keeps running until E2B's own timeout fires.\n *\n * Has no effect on sandboxes the provider only connected to (the `sandboxId`\n * option) — those are always left untouched on destroy since the caller owns\n * their lifecycle.\n */\n onDestroy?: 'kill' | 'pause' | 'leave'\n}\n\n/** Per-probe timeout for the readiness `true` command. */\nconst READINESS_PROBE_TIMEOUT_MS = 5000\n/** Pause between readiness probes while the sandbox is still warming up. */\nconst READINESS_BACKOFF_MS = 250\n/**\n * Command timeout for the pregame setup script. E2B's `commands.run` defaults\n * to 60s and offers no true \"unlimited\" — a `timeoutMs` of 0 is a 0ms deadline\n * (instant timeout), not \"off\". Setup scripts (`apt`/`pip` installs, builds)\n * routinely exceed 60s, so we pass a deliberately generous 30-minute ceiling.\n * The sandbox's own lifetime independently bounds anything longer.\n */\nconst PREGAME_TIMEOUT_MS = 30 * 60 * 1000\n\n/**\n * Upload a setup (\"pregame\") script into a ready sandbox and execute it.\n *\n * Writes the script to `/tmp/<name>`, then `chmod +x`'s and runs it directly\n * (so a `#!/bin/bash` / `#!/usr/bin/env python3` shebang chooses the\n * interpreter) in `cwd` with `envs` applied — the same env the sandbox was\n * created with, so the script sees the run's `--env` / `--pass-env`. A\n * generous {@link PREGAME_TIMEOUT_MS} ceiling is used so long `apt`/`pip`\n * installs aren't killed by E2B's 60s command default. A non-zero exit\n * (whether E2B returns it or raises on it) throws, so the caller can tear the\n * sandbox down rather than prompt against a broken environment.\n */\nexport async function runE2BPregame(\n sandbox: E2BSandbox,\n logger: Logger,\n opts: { name: string, content: string, cwd?: string, envs?: Record<string, string> },\n): Promise<void> {\n const path = `/tmp/${opts.name}`\n logger.info('running E2B pregame script', { sandboxId: sandbox.sandboxId, path })\n await sandbox.files.write(path, opts.content)\n\n const quoted = JSON.stringify(path)\n let result: E2BCommandResult\n try {\n result = await sandbox.commands.run(`chmod +x ${quoted} && ${quoted}`, {\n cwd: opts.cwd,\n envs: opts.envs,\n timeoutMs: PREGAME_TIMEOUT_MS,\n })\n }\n catch (err: any) {\n // E2B raises CommandExitError (carrying the captured streams) on a\n // non-zero exit. Surface its output, then fail the spawn.\n const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 124\n logger.error('E2B pregame script failed', { sandboxId: sandbox.sandboxId, exitCode, stdout: err?.stdout ?? '', stderr: err?.stderr ?? err?.message ?? '' })\n throw new Error(`E2B pregame script ${opts.name} failed with exit code ${exitCode}`)\n }\n\n if (result.exitCode !== 0) {\n logger.error('E2B pregame script failed', { sandboxId: sandbox.sandboxId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr })\n throw new Error(`E2B pregame script ${opts.name} failed with exit code ${result.exitCode}`)\n }\n\n logger.info('E2B pregame ready', { sandboxId: sandbox.sandboxId, stdout: result.stdout, stderr: result.stderr })\n}\n\nexport interface E2BReadinessOptions {\n /** Total deadline in seconds. `<= 0` disables the probe entirely. */\n timeoutSeconds: number\n /**\n * Whether to kill the sandbox if it never becomes ready. Defaults to `true`\n * (we created it, so a dead sandbox is ours to clean up). Pass `false` when\n * attaching to an externally-owned sandbox — failing the probe shouldn't tear\n * down a sandbox the caller is managing.\n */\n killOnTimeout?: boolean\n /** Clock source — injectable so tests can drive the deadline deterministically. */\n now?: () => number\n /** Sleep between probes — injectable so tests can advance the clock without waiting. */\n sleep?: (ms: number) => Promise<void>\n}\n\n/**\n * Block until an E2B sandbox is ready to run commands, or throw if it never\n * becomes ready within the deadline.\n *\n * The E2B control plane can hand back a sandbox id before the sandbox's `envd`\n * is accepting commands; the first real command then absorbs that cold start\n * and may time out. This polls a trivial `true` command on a short backoff,\n * logging a `waiting…` line up front and a `ready` line (with `elapsedMs`) on\n * success. On deadline exhaustion it best-effort kills the sandbox and throws.\n *\n * `now`/`sleep` are injectable purely for deterministic tests; in production\n * they default to `Date.now` and a real timer.\n */\nexport async function waitForE2BReady(\n sandbox: E2BSandbox,\n logger: Logger,\n opts: E2BReadinessOptions,\n): Promise<void> {\n if (opts.timeoutSeconds <= 0)\n return\n\n const now = opts.now ?? Date.now\n const sleep = opts.sleep ?? (ms => new Promise<void>(resolve => setTimeout(resolve, ms)))\n const start = now()\n const deadline = start + opts.timeoutSeconds * 1000\n\n logger.info('waiting for E2B sandbox to initialize', { sandboxId: sandbox.sandboxId })\n\n while (true) {\n try {\n await sandbox.commands.run('true', { timeoutMs: READINESS_PROBE_TIMEOUT_MS })\n logger.info('E2B sandbox ready', { sandboxId: sandbox.sandboxId, elapsedMs: now() - start })\n return\n }\n catch {\n if (now() >= deadline) {\n if (opts.killOnTimeout !== false)\n await sandbox.kill().catch(() => {})\n throw new Error(`E2B sandbox ${sandbox.sandboxId} did not become ready within ${opts.timeoutSeconds}s`)\n }\n await sleep(READINESS_BACKOFF_MS)\n }\n }\n}\n\n/**\n * Resolve the effective E2B template, highest precedence first: the per-spawn\n * `SpawnConfig.sandbox.template`, then the factory `template` option, then the\n * `E2B_TEMPLATE` env var. Returns `undefined` (→ SDK's `base` default) when\n * none is set. The E2B SDK reads `E2B_API_KEY`/`E2B_DOMAIN` from the env but\n * NOT `E2B_TEMPLATE`, so this provider has to honor it itself.\n */\nexport function resolveE2BTemplate(\n perSpawnTemplate: string | undefined,\n optionTemplate: string | undefined,\n env: Record<string, string | undefined> = process.env,\n): string | undefined {\n return perSpawnTemplate || optionTemplate || env.E2B_TEMPLATE || undefined\n}\n\n/**\n * Merge the factory-level {@link E2BProviderOptions.env} with the per-spawn\n * `SpawnConfig.env`, per-spawn winning on key conflicts. Returns `undefined`\n * when the merge is empty so callers can skip setting `envs` on the create\n * options entirely (rather than handing the SDK an empty object).\n */\nexport function resolveE2BEnv(\n optionEnv: Record<string, string> | undefined,\n perSpawnEnv: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n const merged = { ...optionEnv, ...perSpawnEnv }\n return Object.keys(merged).length > 0 ? merged : undefined\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\n/**\n * Build a {@link SandboxProvider} backed by E2B.\n *\n * The provider owns a registry of live {@link E2BSandbox} instances keyed by\n * sandbox id, so the per-call `exec`/`readFile`/… hooks reuse the same warm\n * connection rather than reconnecting each time.\n */\nexport function createE2BProvider(options: E2BProviderOptions = {}): SandboxProvider {\n // Track the live sandbox, the env it was created with, and whether WE own it.\n // We re-send that env on every `commands.run` rather than relying on\n // `Sandbox.create`'s `envs` alone — a warm / pre-provisioned sandbox handed\n // back by the control plane may never have run our create-time env injection.\n // `owned` is false for sandboxes we attached to via `Sandbox.connect`: those\n // are never killed on teardown/failure because the caller manages them.\n const live = new Map<string, { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean }>()\n\n // Resolve the readiness logger once: caller-supplied, else a console-backed\n // default so the \"waiting for sandbox\" line still reaches stderr.\n const logger = options.logger ?? createLogger(consoleSink())\n const readinessTimeoutSeconds = options.readinessTimeoutSeconds ?? 60\n\n async function loadSdk(): Promise<E2BSandboxStatic> {\n try {\n const mod = await import('e2b') as { Sandbox: E2BSandboxStatic }\n return mod.Sandbox\n }\n catch {\n throw new Error('e2b is required for the E2B sandbox provider. Install it with: bun add e2b')\n }\n }\n\n function get(sandboxId: string): { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean } {\n const entry = live.get(sandboxId)\n if (!entry)\n throw new Error(`E2B sandbox ${sandboxId} is not tracked by this provider`)\n return entry\n }\n\n // Best-effort kill of an owned sandbox. Failures are swallowed: the sandbox\n // may already be gone (timed out, or killed out-of-band).\n async function killOwnedSandbox(sandbox: E2BSandbox): Promise<void> {\n try {\n await sandbox.kill()\n }\n catch {\n // Already gone — nothing to clean up.\n }\n }\n\n // Pause an owned sandbox so its filesystem/process state survives for a later\n // reconnect. Prefers the stable `pause()` (e2b@2.27.1+), falling back to the\n // deprecated `betaPause()` (e2b@2.0.0+) across the rest of the supported 2.x\n // range. If a mismatched install exposes neither, surface it loudly rather\n // than let a bare `catch` swallow the \"not a function\" error — which would\n // leave the caller believing state was preserved while the sandbox is really\n // about to time out. A genuine pause failure is logged the same way; we\n // deliberately do NOT fall back to kill, since that would destroy the very\n // state the caller asked to keep.\n async function pauseOwnedSandbox(sandbox: E2BSandbox): Promise<void> {\n try {\n if (sandbox.pause) {\n await sandbox.pause()\n }\n else if (sandbox.betaPause) {\n await sandbox.betaPause()\n }\n else {\n logger.error('cannot pause E2B sandbox: this e2b SDK exposes neither pause() nor betaPause() (expected e2b@^2); leaving it to time out', { sandboxId: sandbox.sandboxId })\n return\n }\n logger.info('paused E2B sandbox; reconnect later via the sandboxId option (CLI: --sandbox)', { sandboxId: sandbox.sandboxId })\n }\n catch (err) {\n logger.error('failed to pause E2B sandbox; it will run until its timeout', { sandboxId: sandbox.sandboxId, error: err instanceof Error ? err.message : String(err) })\n }\n }\n\n return {\n name: 'e2b',\n\n async spawn(config: SpawnConfig): Promise<{ id: string, cwd: string }> {\n const Sandbox = await loadSdk()\n const sb: Record<string, unknown> = config.sandbox ?? {}\n\n // Per-spawn overrides win over factory options win over SDK env defaults.\n const apiKey = (sb.apiKey as string | undefined) ?? options.apiKey\n const domain = (sb.domain as string | undefined) ?? options.domain\n const template = resolveE2BTemplate(sb.template as string | undefined, options.template)\n const timeoutSeconds = config.limits?.timeout ?? options.timeoutSeconds\n const connectId = (sb.sandboxId as string | undefined) ?? options.sandboxId\n\n const envs = resolveE2BEnv(options.env, config.env)\n\n // Attach to an existing sandbox when a sandbox id was supplied; otherwise\n // create a fresh one. A connected sandbox is externally owned (`owned`\n // false) so we never kill it on failure or teardown. The create-time\n // `envs`/`template` don't apply to an attach — env is still re-sent on\n // every command, so a connected sandbox isn't left without it.\n let sandbox: E2BSandbox\n const owned = connectId === undefined\n if (connectId !== undefined) {\n const connectOpts: E2BConnectOptions = {}\n if (apiKey !== undefined)\n connectOpts.apiKey = apiKey\n if (domain !== undefined)\n connectOpts.domain = domain\n if (timeoutSeconds !== undefined)\n connectOpts.timeoutMs = timeoutSeconds * 1000\n sandbox = await Sandbox.connect(connectId, connectOpts)\n }\n else {\n const createOpts: E2BCreateOptions = {}\n if (apiKey !== undefined)\n createOpts.apiKey = apiKey\n if (domain !== undefined)\n createOpts.domain = domain\n if (envs)\n createOpts.envs = envs\n if (timeoutSeconds !== undefined)\n createOpts.timeoutMs = timeoutSeconds * 1000\n sandbox = template !== undefined\n ? await Sandbox.create(template, createOpts)\n : await Sandbox.create(createOpts)\n }\n\n live.set(sandbox.sandboxId, { sandbox, env: envs, owned })\n\n // Wait for envd to actually accept commands before handing back the\n // handle — otherwise the cwd mkdir below (and the agent's first turn)\n // races the sandbox's cold start. On failure a sandbox we created is\n // already killed; a connected one is left alone (`killOnTimeout`). Either\n // way, stop tracking it and propagate.\n try {\n await waitForE2BReady(sandbox, logger, { timeoutSeconds: readinessTimeoutSeconds, killOnTimeout: owned })\n }\n catch (err) {\n live.delete(sandbox.sandboxId)\n throw err\n }\n\n // Resolve the working directory. An explicit cwd (per-spawn, else the\n // provider default) is the caller's responsibility — create it up front\n // so relative ops have somewhere to land. With no explicit cwd, DON'T\n // assume E2B's hosted `/home/user`: self-hosted templates may run as a\n // different user whose home lives elsewhere and which we lack permission\n // to create under `/home`. Handing envd a non-existent cwd makes it fail\n // process spawn with ENOENT (\"No such file or directory\"), breaking the\n // pregame script and the agent's first shell turn alike. Instead, ask the\n // sandbox where a login shell actually lands (a cwd-less `pwd`, which\n // can't itself hit the ENOENT) and use that.\n const requestedCwd = config.cwd ?? options.cwd\n let cwd: string\n if (requestedCwd) {\n cwd = requestedCwd\n // Best-effort: the cwd usually already exists (or is creatable), and a\n // genuine failure surfaces as ENOENT on the very next command anyway.\n await sandbox.commands.run(`mkdir -p ${JSON.stringify(cwd)}`).catch(() => {})\n }\n else {\n const discovered = await sandbox.commands.run('pwd')\n .then(r => r.stdout.trim())\n .catch(() => '')\n cwd = discovered || '/home/user'\n }\n\n // Pre-provision the environment before any prompting: upload + run the\n // setup script in the now-ready sandbox. A failure tears the sandbox\n // down (mirroring the readiness gate) rather than handing back a\n // half-provisioned environment.\n if (options.pregame) {\n try {\n await runE2BPregame(sandbox, logger, { name: options.pregame.name, content: options.pregame.content, cwd, envs })\n }\n catch (err) {\n // Only tear down a sandbox we created; a connected one is the\n // caller's to manage even when our setup script fails.\n if (owned)\n await sandbox.kill().catch(() => {})\n live.delete(sandbox.sandboxId)\n throw err\n }\n }\n\n return { id: sandbox.sandboxId, cwd }\n },\n\n /**\n * Live bidirectional stream — see `ExecutionContext.execStream`.\n *\n * Backed by E2B's pipe-based command API rather than `sandbox.pty`.\n * Both can carry stdin, but a PTY would force us to defeat terminal\n * semantics we never wanted (input echo, `\\n`→`\\r\\n` rewriting, the\n * ~4096-byte canonical line cap, and stdout/stderr merged into one\n * stream). Pipes give split streams and byte-faithful framing for free.\n *\n * KNOWN RESIDUAL: `onStdout` / `onStderr` deliver `string`, already\n * decoded by the SDK. A chunk boundary that splits a multibyte\n * codepoint is therefore corrupted before it reaches us — we re-encode\n * to bytes to satisfy the seam, but cannot recover what the decoder\n * dropped. Newline framing and ASCII structure are unaffected, so\n * JSON-RPC still parses; the damage would be confined to non-ASCII\n * inside string values. If that ever bites, `sandbox.pty.create`\n * hands back raw `Uint8Array` and the Daytona PTY path in\n * `./daytona.ts` is the template for adopting it here.\n */\n async execStream(sandboxId, command, opts): Promise<StreamHandle> {\n const { sandbox, env } = get(sandboxId)\n\n opts?.signal?.throwIfAborted()\n\n // A stalled consumer cannot be signalled back to the E2B SDK (its\n // output callbacks are sync `void`), so surface the backlog instead of\n // letting it read as an unexplained host OOM.\n const warnBacklog = (which: string) => (bytes: number) =>\n logger.warn('execStream consumer is not draining output', { sandboxId, stream: which, bufferedBytes: bytes })\n const stdout = new StreamQueue(warnBacklog('stdout'))\n const stderr = new StreamQueue(warnBacklog('stderr'))\n\n // `background: true` returns a handle instead of awaiting completion;\n // `stdin: true` is required (it defaults to FALSE) or the child gets\n // EOF immediately and a JSON-RPC server exits before its first read.\n // `timeoutMs` overrides E2B's 60s default command timeout, which\n // would otherwise kill a session-lifetime server mid-run. NOT `0`:\n // the SDK forwards the value into `@connectrpc/connect`, whose\n // deadline signal treats `timeoutMs <= 0` as ALREADY EXPIRED and\n // aborts the call instantly (the PTY docs' \"0 = indefinite\" does not\n // hold for `commands.run` on this SDK). So: the largest value a JS\n // timer accepts (~24.8 days) — anything above 2^31-1 overflows\n // setTimeout and fires immediately, which is just `0` with extra\n // steps.\n // Kill semantics need a process GROUP, and E2B does not give us one:\n // the reported pid runs in envd's own group (observed live: pid 2003,\n // pgid 1235 = envd). Signalling that group would kill the sandbox\n // agent itself, and signalling the pid alone leaves grandchildren\n // running — a wrapper launcher (npx, uv, a shell one-liner) keeps the\n // real server alive to accumulate across reconnects. So put the child\n // in its own session with `setsid -w` and record ITS pid, which is\n // then the group id. `-w` makes setsid wait and propagate the exit\n // code; bare setsid's parent exits immediately and the stream tears\n // down. The probe falls back to a plain exec on images without `-w`,\n // where the pid-only kill still reaches the direct child.\n const pidFile = `/tmp/.zidane-stream-${Math.random().toString(36).slice(2, 10)}.pid`\n // `sh -c <quoted>` rather than a bare `exec ${command}`: with a\n // compound command (`a; b >&2; c`) a bare exec binds to the FIRST\n // command only and silently drops the rest.\n const leader = `echo ${'$'}${'$'} > ${pidFile}; exec sh -c ${alwaysQuote(command)}`\n const wrapped = `if setsid -w true 2>/dev/null; then exec setsid -w sh -c ${alwaysQuote(leader)}; else exec sh -c ${alwaysQuote(leader)}; fi`\n\n // Typed via the `background: true` overload on `E2BSandbox['commands']`\n // — that is what the overload exists for, so no cast is needed here.\n const handle: E2BCommandHandle = await sandbox.commands.run(wrapped, {\n background: true,\n stdin: true,\n timeoutMs: E2B_STREAM_TIMEOUT_MS,\n ...(opts?.cwd ? { cwd: opts.cwd } : {}),\n envs: resolveE2BEnv(env, opts?.env),\n onStdout: (data: string) => stdout.push(new TextEncoder().encode(data)),\n onStderr: (data: string) => stderr.push(new TextEncoder().encode(data)),\n })\n\n const pid: number = handle.pid\n\n const exited = handle.wait()\n .then(result => result?.exitCode ?? 0)\n // E2B throws CommandExitError on a non-zero exit; the seam promises\n // a never-rejecting `exited`, so translate rather than propagate.\n .catch((err: any) => typeof err?.exitCode === 'number' ? err.exitCode : 137)\n .finally(() => { stdout.close(); stderr.close() })\n\n // Read back the session leader's pid, which `setsid -w` made the\n // group id. Separate one-shot command: the API exposes no group verb.\n let commandExited = false\n void exited.then(() => { commandExited = true })\n const readLeaderPid = async (): Promise<number> => {\n for (let attempt = 0; attempt < 20; attempt++) {\n const probe = await sandbox.commands\n .run(`cat ${pidFile} 2>/dev/null || true`)\n .catch(() => null)\n const parsed = Number.parseInt(probe?.stdout?.trim() ?? '', 10)\n if (Number.isFinite(parsed) && parsed > 0)\n return parsed\n // A dead command will never write the pidfile — a missing binary\n // in the image (the most likely user-facing failure) exits\n // immediately, and burning the remaining attempts would cost up\n // to 20 sequential sandbox round-trips before bootstrap can\n // surface the real error. One more probe already ran above, so\n // nothing written before death is missed.\n if (commandExited)\n break\n await new Promise(r => setTimeout(r, 50))\n }\n return -1\n }\n const leaderPid = await readLeaderPid()\n\n const kill = async (): Promise<void> => {\n if (leaderPid > 0) {\n // Group first (the leader IS the pgid), pid-only as the fallback\n // for images that took the no-setsid branch. NEVER a bare group\n // kill on the reported pid: that group belongs to envd.\n await sandbox.commands\n .run(`kill -TERM -${leaderPid} 2>/dev/null || kill -TERM ${leaderPid} 2>/dev/null || true`)\n .catch(() => {})\n }\n await sandbox.commands.run(`rm -f ${pidFile}`).catch(() => {})\n await sandbox.commands.kill?.(pid).catch(() => {})\n }\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n void kill()\n }\n else {\n // Detach on exit — the (typically run-long) signal otherwise\n // retains the `kill` closure and the sandbox handle for the rest\n // of the run, one dead closure per exec.\n const onAbort = () => { void kill() }\n const detach = () => opts.signal!.removeEventListener('abort', onAbort)\n opts.signal.addEventListener('abort', onAbort, { once: true })\n exited.then(detach, detach)\n }\n }\n\n return {\n pid,\n stdout,\n stderr,\n write: async (data: Uint8Array): Promise<void> => {\n if (!sandbox.commands.sendStdin)\n throw new Error('E2B SDK is too old: `commands.sendStdin` is required for execStream')\n await sandbox.commands.sendStdin(pid, data)\n },\n kill,\n exited,\n }\n },\n\n async exec(sandboxId, command, opts): Promise<ExecResult> {\n const { sandbox, env } = get(sandboxId)\n // Match the process/docker contexts: `timeout` is in seconds, default 30.\n const timeoutMs = (opts?.timeout ?? 30) * 1000\n try {\n const result = await sandbox.commands.run(command, {\n cwd: opts?.cwd,\n // Re-send the sandbox's configured env on every command (warm\n // sandboxes may lack the create-time injection), with the per-call\n // env layered on top.\n envs: resolveE2BEnv(env, opts?.env),\n timeoutMs: timeoutMs > 0 ? timeoutMs : undefined,\n })\n // The SDK buffers the streams uncapped (see the KNOWN RESIDUAL note\n // on `ExecutionContext.fileSize`) — cap host-side before the strings\n // propagate into tool results and session history.\n return { ...capBufferedExecOutput(result.stdout, result.stderr), exitCode: result.exitCode }\n }\n catch (err: any) {\n // E2B throws on non-zero exit (CommandExitError carries the captured\n // streams) and on timeout. Normalize both into an ExecResult so the\n // shell tool sees a clean exit code rather than a thrown error.\n if (typeof err?.exitCode === 'number') {\n return {\n ...capBufferedExecOutput(err.stdout ?? '', err.stderr ?? err.message ?? ''),\n exitCode: err.exitCode,\n }\n }\n return { stdout: '', stderr: err?.message ?? String(err), exitCode: 124 }\n }\n },\n\n async readFile(sandboxId, path): Promise<string> {\n return get(sandboxId).sandbox.files.read(path)\n },\n\n async writeFile(sandboxId, path, content): Promise<void> {\n await get(sandboxId).sandbox.files.write(path, content)\n },\n\n async listFiles(sandboxId, path): Promise<string[]> {\n const entries = await get(sandboxId).sandbox.files.list(path)\n // Trailing `/` on directories — parity with process/docker/daytona.\n return entries.map(e => (e.type === 'dir' ? `${e.name}/` : e.name))\n },\n\n async destroy(sandboxId): Promise<void> {\n const entry = live.get(sandboxId)\n if (!entry)\n return\n // Never kill/pause a sandbox we only connected to — the caller owns its\n // lifecycle. Just drop our handle to it.\n if (entry.owned) {\n const action = options.onDestroy ?? 'kill'\n if (action === 'pause')\n await pauseOwnedSandbox(entry.sandbox)\n else if (action === 'kill')\n await killOwnedSandbox(entry.sandbox)\n // 'leave' makes no API call — the sandbox runs until E2B's own timeout.\n }\n live.delete(sandboxId)\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"e2b.js","names":[],"sources":["../../src/contexts/e2b.ts"],"sourcesContent":["/**\n * E2B sandbox provider.\n *\n * Implements {@link SandboxProvider} on top of E2B's firecracker-backed\n * micro-VMs. Pair it with {@link createSandboxContext} to run an agent's\n * shell + filesystem tools inside an ephemeral E2B sandbox:\n *\n * ```ts\n * import { createSandboxContext } from 'zidane/contexts'\n * import { createE2BProvider } from 'zidane/contexts/e2b'\n *\n * const execution = createSandboxContext(createE2BProvider())\n * ```\n *\n * ## Pointing at your own E2B\n *\n * The two knobs that select *which* E2B you talk to are `apiKey` and\n * `domain`. Self-hosted / on-prem E2B clusters expose a custom domain; pass\n * it here (or via the `E2B_DOMAIN` env var) and the SDK routes every call to\n * your cluster instead of E2B's hosted API. Precedence, highest first:\n *\n * 1. Per-spawn `SpawnConfig.sandbox` fields (`apiKey`, `domain`, `template`)\n * 2. {@link E2BProviderOptions} passed to this factory\n * 3. `E2B_API_KEY` / `E2B_DOMAIN` env vars (read by the SDK itself), and\n * `E2B_TEMPLATE` (read by this provider — the SDK does not read it)\n *\n * The default `base` template does not exist on self-hosted clusters, so\n * picking the right template matters: an invalid template still returns a\n * sandbox id from the control plane, but its envd never boots and every\n * `exec`/file op then hangs until it times out. Set `E2B_TEMPLATE` (or pass\n * `template`) to a template that actually exists on your cluster.\n *\n * Requires `e2b@^2` as an optional peer dependency:\n * `bun add e2b`.\n */\n\nimport type { Logger } from '../logger'\nimport type { SandboxProvider } from './sandbox'\nimport type { ExecResult, SpawnConfig, StreamHandle } from './types'\nimport { Buffer } from 'node:buffer'\nimport { consoleSink, createLogger } from '../logger'\nimport { alwaysQuote } from '../tools/shell-quote'\nimport { capBufferedExecOutput } from './output-cap'\nimport { StreamQueue } from './stream-queue'\n\n// ---------------------------------------------------------------------------\n// Minimal structural views of the `e2b` SDK surface we touch. Declared locally\n// (rather than imported) so the package typechecks without the optional peer\n// dependency installed — mirroring how `docker.ts` keeps `dockerode` at arm's\n// length. The real SDK satisfies these shapes. The peer dependency floor is\n// `e2b@^2`: every member used here exists across 2.x, but the pause surface\n// shifted within it — `betaPause()` from 2.0.0, the stable `pause()` from\n// 2.27.1 — so `onDestroy: 'pause'` prefers `pause()` and falls back to\n// `betaPause()` (both optional below, resolved at runtime in `destroy`).\n// ---------------------------------------------------------------------------\n\n/**\n * Options that flip `commands.run` into background mode. Split out so the\n * overload above can discriminate on it: `background: true` changes the\n * return type from a finished result to a live handle.\n *\n * `stdin: true` and a huge `timeoutMs` are not incidental — the SDK\n * defaults are `false` and 60s, which would respectively hand the child an\n * immediate EOF and kill a session-lifetime process after a minute. See\n * {@link E2B_STREAM_TIMEOUT_MS} for why the timeout is a large number and\n * not `0`.\n */\ninterface E2BBackgroundOptions {\n background: true\n stdin: true\n timeoutMs: number\n cwd?: string\n envs?: Record<string, string>\n onStdout?: (data: string) => void\n onStderr?: (data: string) => void\n}\n\n/**\n * \"Effectively unlimited\" command timeout for `execStream`. 2^31-1 ms\n * (~24.8 days — far beyond any sandbox lifetime) because both edges are\n * cliffs: `0` is treated by `@connectrpc/connect`'s deadline signal as\n * already expired (instant DeadlineExceeded), and anything above 2^31-1\n * overflows `setTimeout` and fires immediately.\n */\nconst E2B_STREAM_TIMEOUT_MS = 2 ** 31 - 1\n\ninterface E2BCommandResult {\n stdout: string\n stderr: string\n exitCode: number\n}\n\ninterface E2BEntry {\n name: string\n /** Present in the E2B SDK's EntryInfo; used for the `/` dir suffix. */\n type?: string\n}\n\n/**\n * Handle returned by a `background: true` command. Structural subset of the\n * SDK's `CommandHandle` — only what `execStream` needs.\n */\ninterface E2BCommandHandle {\n pid: number\n wait: () => Promise<E2BCommandResult>\n}\n\ninterface E2BSandbox {\n sandboxId: string\n commands: {\n run: ((cmd: string, opts?: { cwd?: string, envs?: Record<string, string>, timeoutMs?: number }) => Promise<E2BCommandResult>)\n & ((cmd: string, opts: E2BBackgroundOptions) => Promise<E2BCommandHandle>)\n /** Present from the SDK's Commands module; used only by `execStream`. */\n sendStdin?: (pid: number, data: string | Uint8Array) => Promise<void>\n kill?: (pid: number) => Promise<boolean>\n }\n files: {\n read: (path: string) => Promise<string>\n /**\n * Deliberately WIDER than the SDK's declared input (string | ArrayBuffer |\n * Blob | ReadableStream): every upload path there ends at `toBlob`, whose\n * fallback is `new Blob([data])` — a BlobPart, so a Uint8Array view\n * (byteOffset-aware) uploads verbatim. Strings still travel as strings, so\n * only a byte write depends on this widening.\n */\n write: (path: string, data: string | Uint8Array) => Promise<unknown>\n list: (path: string) => Promise<E2BEntry[]>\n }\n kill: () => Promise<unknown>\n // The pause surface shifted within 2.x: `betaPause` from 2.0.0, the stable\n // `pause` from 2.27.1. Both are optional so `destroy` can prefer `pause`,\n // fall back to `betaPause`, and stay defensive if a mismatched install\n // exposes neither. Both resolve to whether the sandbox was newly paused.\n pause?: () => Promise<boolean>\n betaPause?: () => Promise<boolean>\n}\n\ninterface E2BCreateOptions {\n apiKey?: string\n domain?: string\n timeoutMs?: number\n envs?: Record<string, string>\n metadata?: Record<string, string>\n}\n\ninterface E2BConnectOptions {\n apiKey?: string\n domain?: string\n timeoutMs?: number\n}\n\ninterface E2BSandboxStatic {\n create: (templateOrOpts?: string | E2BCreateOptions, opts?: E2BCreateOptions) => Promise<E2BSandbox>\n connect: (sandboxId: string, opts?: E2BConnectOptions) => Promise<E2BSandbox>\n}\n\n// ---------------------------------------------------------------------------\n// Provider options\n// ---------------------------------------------------------------------------\n\nexport interface E2BProviderOptions {\n /** E2B API key. Falls back to the `E2B_API_KEY` env var when omitted. */\n apiKey?: string\n /**\n * E2B API domain. Set this to point at a self-hosted / on-prem cluster\n * (e.g. `e2b.my-company.internal`). Falls back to the `E2B_DOMAIN` env var,\n * then to E2B's hosted default.\n */\n domain?: string\n /**\n * Sandbox template id / name to launch. Falls back to the `E2B_TEMPLATE`\n * env var, then to E2B's hosted `base` template. Note that `base` does not\n * exist on self-hosted clusters — supply a template that does.\n */\n template?: string\n /**\n * Connect to a pre-existing sandbox by id instead of creating a fresh one\n * (E2B's `Sandbox.connect`). When set, `template` is ignored — you're\n * attaching to a sandbox that already exists. The provider treats such a\n * sandbox as externally owned: it is NOT killed on `destroy`, nor torn down\n * if the readiness probe / pregame fails, since the caller manages its\n * lifecycle. Per-spawn `SpawnConfig.sandbox.sandboxId` overrides this.\n */\n sandboxId?: string\n /**\n * Default working directory, applied when a spawn doesn't set its own\n * `SpawnConfig.cwd`. Created on spawn. Defaults to E2B's `/home/user`.\n */\n cwd?: string\n /**\n * Sandbox lifetime in seconds before E2B auto-kills it. Defaults to the\n * SDK default (300s). Per-spawn `SpawnConfig.limits.timeout` overrides this.\n */\n timeoutSeconds?: number\n /**\n * Environment variables baked into every sandbox at create time (passed to\n * E2B's `Sandbox.create({ envs })`), so every command run in the sandbox\n * sees them. Per-spawn `SpawnConfig.env` is merged over these.\n */\n env?: Record<string, string>\n /**\n * Logger for provider-level lifecycle lines (notably the readiness wait).\n * Defaults to a {@link consoleSink}-backed logger so the messages reach\n * stderr without the caller wiring one up.\n */\n logger?: Logger\n /**\n * Total deadline, in seconds, for the post-create readiness probe (see\n * {@link waitForE2BReady}). A freshly-created sandbox can return its id\n * before `envd` accepts commands; `spawn` blocks until a trivial probe\n * succeeds so the first real command never absorbs the cold start. Defaults\n * to `60`. Set to `0` to disable the gate.\n */\n readinessTimeoutSeconds?: number\n /**\n * A setup script to upload and run once the sandbox is ready, before any\n * prompting begins (see {@link runE2BPregame}). `name` is the basename used\n * for the uploaded file; `content` is the script body. The script is run\n * directly so its shebang selects the interpreter. A non-zero exit aborts\n * `spawn` (and tears the sandbox down) — a broken setup must not silently\n * hand back a half-provisioned sandbox.\n */\n pregame?: { name: string, content: string }\n /**\n * What to do with an owned sandbox when `destroy` is called:\n *\n * - `'kill'` (default) — call `sandbox.kill()`. The sandbox is deleted and\n * its state is gone.\n * - `'pause'` — call `sandbox.pause()` (falling back to the older\n * `betaPause()` on `e2b` 2.0–2.26). The sandbox's filesystem and process\n * state are preserved; reconnect later with the `sandboxId` option (E2B's\n * `Sandbox.connect` auto-resumes a paused sandbox).\n * - `'leave'` — drop the local handle without making any API call. The\n * sandbox keeps running until E2B's own timeout fires.\n *\n * Has no effect on sandboxes the provider only connected to (the `sandboxId`\n * option) — those are always left untouched on destroy since the caller owns\n * their lifecycle.\n */\n onDestroy?: 'kill' | 'pause' | 'leave'\n}\n\n/** Per-probe timeout for the readiness `true` command. */\nconst READINESS_PROBE_TIMEOUT_MS = 5000\n/** Pause between readiness probes while the sandbox is still warming up. */\nconst READINESS_BACKOFF_MS = 250\n/**\n * Command timeout for the pregame setup script. E2B's `commands.run` defaults\n * to 60s and offers no true \"unlimited\" — a `timeoutMs` of 0 is a 0ms deadline\n * (instant timeout), not \"off\". Setup scripts (`apt`/`pip` installs, builds)\n * routinely exceed 60s, so we pass a deliberately generous 30-minute ceiling.\n * The sandbox's own lifetime independently bounds anything longer.\n */\nconst PREGAME_TIMEOUT_MS = 30 * 60 * 1000\n\n/**\n * Upload a setup (\"pregame\") script into a ready sandbox and execute it.\n *\n * Writes the script to `/tmp/<name>`, then `chmod +x`'s and runs it directly\n * (so a `#!/bin/bash` / `#!/usr/bin/env python3` shebang chooses the\n * interpreter) in `cwd` with `envs` applied — the same env the sandbox was\n * created with, so the script sees the run's `--env` / `--pass-env`. A\n * generous {@link PREGAME_TIMEOUT_MS} ceiling is used so long `apt`/`pip`\n * installs aren't killed by E2B's 60s command default. A non-zero exit\n * (whether E2B returns it or raises on it) throws, so the caller can tear the\n * sandbox down rather than prompt against a broken environment.\n */\nexport async function runE2BPregame(\n sandbox: E2BSandbox,\n logger: Logger,\n opts: { name: string, content: string, cwd?: string, envs?: Record<string, string> },\n): Promise<void> {\n const path = `/tmp/${opts.name}`\n logger.info('running E2B pregame script', { sandboxId: sandbox.sandboxId, path })\n await sandbox.files.write(path, opts.content)\n\n const quoted = JSON.stringify(path)\n let result: E2BCommandResult\n try {\n result = await sandbox.commands.run(`chmod +x ${quoted} && ${quoted}`, {\n cwd: opts.cwd,\n envs: opts.envs,\n timeoutMs: PREGAME_TIMEOUT_MS,\n })\n }\n catch (err: any) {\n // E2B raises CommandExitError (carrying the captured streams) on a\n // non-zero exit. Surface its output, then fail the spawn.\n const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 124\n logger.error('E2B pregame script failed', { sandboxId: sandbox.sandboxId, exitCode, stdout: err?.stdout ?? '', stderr: err?.stderr ?? err?.message ?? '' })\n throw new Error(`E2B pregame script ${opts.name} failed with exit code ${exitCode}`)\n }\n\n if (result.exitCode !== 0) {\n logger.error('E2B pregame script failed', { sandboxId: sandbox.sandboxId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr })\n throw new Error(`E2B pregame script ${opts.name} failed with exit code ${result.exitCode}`)\n }\n\n logger.info('E2B pregame ready', { sandboxId: sandbox.sandboxId, stdout: result.stdout, stderr: result.stderr })\n}\n\nexport interface E2BReadinessOptions {\n /** Total deadline in seconds. `<= 0` disables the probe entirely. */\n timeoutSeconds: number\n /**\n * Whether to kill the sandbox if it never becomes ready. Defaults to `true`\n * (we created it, so a dead sandbox is ours to clean up). Pass `false` when\n * attaching to an externally-owned sandbox — failing the probe shouldn't tear\n * down a sandbox the caller is managing.\n */\n killOnTimeout?: boolean\n /** Clock source — injectable so tests can drive the deadline deterministically. */\n now?: () => number\n /** Sleep between probes — injectable so tests can advance the clock without waiting. */\n sleep?: (ms: number) => Promise<void>\n}\n\n/**\n * Block until an E2B sandbox is ready to run commands, or throw if it never\n * becomes ready within the deadline.\n *\n * The E2B control plane can hand back a sandbox id before the sandbox's `envd`\n * is accepting commands; the first real command then absorbs that cold start\n * and may time out. This polls a trivial `true` command on a short backoff,\n * logging a `waiting…` line up front and a `ready` line (with `elapsedMs`) on\n * success. On deadline exhaustion it best-effort kills the sandbox and throws.\n *\n * `now`/`sleep` are injectable purely for deterministic tests; in production\n * they default to `Date.now` and a real timer.\n */\nexport async function waitForE2BReady(\n sandbox: E2BSandbox,\n logger: Logger,\n opts: E2BReadinessOptions,\n): Promise<void> {\n if (opts.timeoutSeconds <= 0)\n return\n\n const now = opts.now ?? Date.now\n const sleep = opts.sleep ?? (ms => new Promise<void>(resolve => setTimeout(resolve, ms)))\n const start = now()\n const deadline = start + opts.timeoutSeconds * 1000\n\n logger.info('waiting for E2B sandbox to initialize', { sandboxId: sandbox.sandboxId })\n\n while (true) {\n try {\n await sandbox.commands.run('true', { timeoutMs: READINESS_PROBE_TIMEOUT_MS })\n logger.info('E2B sandbox ready', { sandboxId: sandbox.sandboxId, elapsedMs: now() - start })\n return\n }\n catch {\n if (now() >= deadline) {\n if (opts.killOnTimeout !== false)\n await sandbox.kill().catch(() => {})\n throw new Error(`E2B sandbox ${sandbox.sandboxId} did not become ready within ${opts.timeoutSeconds}s`)\n }\n await sleep(READINESS_BACKOFF_MS)\n }\n }\n}\n\n/**\n * Resolve the effective E2B template, highest precedence first: the per-spawn\n * `SpawnConfig.sandbox.template`, then the factory `template` option, then the\n * `E2B_TEMPLATE` env var. Returns `undefined` (→ SDK's `base` default) when\n * none is set. The E2B SDK reads `E2B_API_KEY`/`E2B_DOMAIN` from the env but\n * NOT `E2B_TEMPLATE`, so this provider has to honor it itself.\n */\nexport function resolveE2BTemplate(\n perSpawnTemplate: string | undefined,\n optionTemplate: string | undefined,\n env: Record<string, string | undefined> = process.env,\n): string | undefined {\n return perSpawnTemplate || optionTemplate || env.E2B_TEMPLATE || undefined\n}\n\n/**\n * Merge the factory-level {@link E2BProviderOptions.env} with the per-spawn\n * `SpawnConfig.env`, per-spawn winning on key conflicts. Returns `undefined`\n * when the merge is empty so callers can skip setting `envs` on the create\n * options entirely (rather than handing the SDK an empty object).\n */\nexport function resolveE2BEnv(\n optionEnv: Record<string, string> | undefined,\n perSpawnEnv: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n const merged = { ...optionEnv, ...perSpawnEnv }\n return Object.keys(merged).length > 0 ? merged : undefined\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\n/**\n * Build a {@link SandboxProvider} backed by E2B.\n *\n * The provider owns a registry of live {@link E2BSandbox} instances keyed by\n * sandbox id, so the per-call `exec`/`readFile`/… hooks reuse the same warm\n * connection rather than reconnecting each time.\n */\nexport function createE2BProvider(options: E2BProviderOptions = {}): SandboxProvider {\n // Track the live sandbox, the env it was created with, and whether WE own it.\n // We re-send that env on every `commands.run` rather than relying on\n // `Sandbox.create`'s `envs` alone — a warm / pre-provisioned sandbox handed\n // back by the control plane may never have run our create-time env injection.\n // `owned` is false for sandboxes we attached to via `Sandbox.connect`: those\n // are never killed on teardown/failure because the caller manages them.\n const live = new Map<string, { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean }>()\n\n // Resolve the readiness logger once: caller-supplied, else a console-backed\n // default so the \"waiting for sandbox\" line still reaches stderr.\n const logger = options.logger ?? createLogger(consoleSink())\n const readinessTimeoutSeconds = options.readinessTimeoutSeconds ?? 60\n\n async function loadSdk(): Promise<E2BSandboxStatic> {\n try {\n const mod = await import('e2b') as { Sandbox: E2BSandboxStatic }\n return mod.Sandbox\n }\n catch {\n throw new Error('e2b is required for the E2B sandbox provider. Install it with: bun add e2b')\n }\n }\n\n function get(sandboxId: string): { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean } {\n const entry = live.get(sandboxId)\n if (!entry)\n throw new Error(`E2B sandbox ${sandboxId} is not tracked by this provider`)\n return entry\n }\n\n // Best-effort kill of an owned sandbox. Failures are swallowed: the sandbox\n // may already be gone (timed out, or killed out-of-band).\n async function killOwnedSandbox(sandbox: E2BSandbox): Promise<void> {\n try {\n await sandbox.kill()\n }\n catch {\n // Already gone — nothing to clean up.\n }\n }\n\n // Pause an owned sandbox so its filesystem/process state survives for a later\n // reconnect. Prefers the stable `pause()` (e2b@2.27.1+), falling back to the\n // deprecated `betaPause()` (e2b@2.0.0+) across the rest of the supported 2.x\n // range. If a mismatched install exposes neither, surface it loudly rather\n // than let a bare `catch` swallow the \"not a function\" error — which would\n // leave the caller believing state was preserved while the sandbox is really\n // about to time out. A genuine pause failure is logged the same way; we\n // deliberately do NOT fall back to kill, since that would destroy the very\n // state the caller asked to keep.\n async function pauseOwnedSandbox(sandbox: E2BSandbox): Promise<void> {\n try {\n if (sandbox.pause) {\n await sandbox.pause()\n }\n else if (sandbox.betaPause) {\n await sandbox.betaPause()\n }\n else {\n logger.error('cannot pause E2B sandbox: this e2b SDK exposes neither pause() nor betaPause() (expected e2b@^2); leaving it to time out', { sandboxId: sandbox.sandboxId })\n return\n }\n logger.info('paused E2B sandbox; reconnect later via the sandboxId option (CLI: --sandbox)', { sandboxId: sandbox.sandboxId })\n }\n catch (err) {\n logger.error('failed to pause E2B sandbox; it will run until its timeout', { sandboxId: sandbox.sandboxId, error: err instanceof Error ? err.message : String(err) })\n }\n }\n\n return {\n name: 'e2b',\n\n async spawn(config: SpawnConfig): Promise<{ id: string, cwd: string }> {\n const Sandbox = await loadSdk()\n const sb: Record<string, unknown> = config.sandbox ?? {}\n\n // Per-spawn overrides win over factory options win over SDK env defaults.\n const apiKey = (sb.apiKey as string | undefined) ?? options.apiKey\n const domain = (sb.domain as string | undefined) ?? options.domain\n const template = resolveE2BTemplate(sb.template as string | undefined, options.template)\n const timeoutSeconds = config.limits?.timeout ?? options.timeoutSeconds\n const connectId = (sb.sandboxId as string | undefined) ?? options.sandboxId\n\n const envs = resolveE2BEnv(options.env, config.env)\n\n // Attach to an existing sandbox when a sandbox id was supplied; otherwise\n // create a fresh one. A connected sandbox is externally owned (`owned`\n // false) so we never kill it on failure or teardown. The create-time\n // `envs`/`template` don't apply to an attach — env is still re-sent on\n // every command, so a connected sandbox isn't left without it.\n let sandbox: E2BSandbox\n const owned = connectId === undefined\n if (connectId !== undefined) {\n const connectOpts: E2BConnectOptions = {}\n if (apiKey !== undefined)\n connectOpts.apiKey = apiKey\n if (domain !== undefined)\n connectOpts.domain = domain\n if (timeoutSeconds !== undefined)\n connectOpts.timeoutMs = timeoutSeconds * 1000\n sandbox = await Sandbox.connect(connectId, connectOpts)\n }\n else {\n const createOpts: E2BCreateOptions = {}\n if (apiKey !== undefined)\n createOpts.apiKey = apiKey\n if (domain !== undefined)\n createOpts.domain = domain\n if (envs)\n createOpts.envs = envs\n if (timeoutSeconds !== undefined)\n createOpts.timeoutMs = timeoutSeconds * 1000\n sandbox = template !== undefined\n ? await Sandbox.create(template, createOpts)\n : await Sandbox.create(createOpts)\n }\n\n live.set(sandbox.sandboxId, { sandbox, env: envs, owned })\n\n // Wait for envd to actually accept commands before handing back the\n // handle — otherwise the cwd mkdir below (and the agent's first turn)\n // races the sandbox's cold start. On failure a sandbox we created is\n // already killed; a connected one is left alone (`killOnTimeout`). Either\n // way, stop tracking it and propagate.\n try {\n await waitForE2BReady(sandbox, logger, { timeoutSeconds: readinessTimeoutSeconds, killOnTimeout: owned })\n }\n catch (err) {\n live.delete(sandbox.sandboxId)\n throw err\n }\n\n // Resolve the working directory. An explicit cwd (per-spawn, else the\n // provider default) is the caller's responsibility — create it up front\n // so relative ops have somewhere to land. With no explicit cwd, DON'T\n // assume E2B's hosted `/home/user`: self-hosted templates may run as a\n // different user whose home lives elsewhere and which we lack permission\n // to create under `/home`. Handing envd a non-existent cwd makes it fail\n // process spawn with ENOENT (\"No such file or directory\"), breaking the\n // pregame script and the agent's first shell turn alike. Instead, ask the\n // sandbox where a login shell actually lands (a cwd-less `pwd`, which\n // can't itself hit the ENOENT) and use that.\n const requestedCwd = config.cwd ?? options.cwd\n let cwd: string\n if (requestedCwd) {\n cwd = requestedCwd\n // Best-effort: the cwd usually already exists (or is creatable), and a\n // genuine failure surfaces as ENOENT on the very next command anyway.\n await sandbox.commands.run(`mkdir -p ${JSON.stringify(cwd)}`).catch(() => {})\n }\n else {\n const discovered = await sandbox.commands.run('pwd')\n .then(r => r.stdout.trim())\n .catch(() => '')\n cwd = discovered || '/home/user'\n }\n\n // Pre-provision the environment before any prompting: upload + run the\n // setup script in the now-ready sandbox. A failure tears the sandbox\n // down (mirroring the readiness gate) rather than handing back a\n // half-provisioned environment.\n if (options.pregame) {\n try {\n await runE2BPregame(sandbox, logger, { name: options.pregame.name, content: options.pregame.content, cwd, envs })\n }\n catch (err) {\n // Only tear down a sandbox we created; a connected one is the\n // caller's to manage even when our setup script fails.\n if (owned)\n await sandbox.kill().catch(() => {})\n live.delete(sandbox.sandboxId)\n throw err\n }\n }\n\n return { id: sandbox.sandboxId, cwd }\n },\n\n /**\n * Live bidirectional stream — see `ExecutionContext.execStream`.\n *\n * Backed by E2B's pipe-based command API rather than `sandbox.pty`.\n * Both can carry stdin, but a PTY would force us to defeat terminal\n * semantics we never wanted (input echo, `\\n`→`\\r\\n` rewriting, the\n * ~4096-byte canonical line cap, and stdout/stderr merged into one\n * stream). Pipes give split streams and byte-faithful framing for free.\n *\n * KNOWN RESIDUAL: `onStdout` / `onStderr` deliver `string`, already\n * decoded by the SDK. A chunk boundary that splits a multibyte\n * codepoint is therefore corrupted before it reaches us — we re-encode\n * to bytes to satisfy the seam, but cannot recover what the decoder\n * dropped. Newline framing and ASCII structure are unaffected, so\n * JSON-RPC still parses; the damage would be confined to non-ASCII\n * inside string values. If that ever bites, `sandbox.pty.create`\n * hands back raw `Uint8Array` and the Daytona PTY path in\n * `./daytona.ts` is the template for adopting it here.\n */\n async execStream(sandboxId, command, opts): Promise<StreamHandle> {\n const { sandbox, env } = get(sandboxId)\n\n opts?.signal?.throwIfAborted()\n\n // A stalled consumer cannot be signalled back to the E2B SDK (its\n // output callbacks are sync `void`), so surface the backlog instead of\n // letting it read as an unexplained host OOM.\n const warnBacklog = (which: string) => (bytes: number) =>\n logger.warn('execStream consumer is not draining output', { sandboxId, stream: which, bufferedBytes: bytes })\n const stdout = new StreamQueue(warnBacklog('stdout'))\n const stderr = new StreamQueue(warnBacklog('stderr'))\n\n // `background: true` returns a handle instead of awaiting completion;\n // `stdin: true` is required (it defaults to FALSE) or the child gets\n // EOF immediately and a JSON-RPC server exits before its first read.\n // `timeoutMs` overrides E2B's 60s default command timeout, which\n // would otherwise kill a session-lifetime server mid-run. NOT `0`:\n // the SDK forwards the value into `@connectrpc/connect`, whose\n // deadline signal treats `timeoutMs <= 0` as ALREADY EXPIRED and\n // aborts the call instantly (the PTY docs' \"0 = indefinite\" does not\n // hold for `commands.run` on this SDK). So: the largest value a JS\n // timer accepts (~24.8 days) — anything above 2^31-1 overflows\n // setTimeout and fires immediately, which is just `0` with extra\n // steps.\n // Kill semantics need a process GROUP, and E2B does not give us one:\n // the reported pid runs in envd's own group (observed live: pid 2003,\n // pgid 1235 = envd). Signalling that group would kill the sandbox\n // agent itself, and signalling the pid alone leaves grandchildren\n // running — a wrapper launcher (npx, uv, a shell one-liner) keeps the\n // real server alive to accumulate across reconnects. So put the child\n // in its own session with `setsid -w` and record ITS pid, which is\n // then the group id. `-w` makes setsid wait and propagate the exit\n // code; bare setsid's parent exits immediately and the stream tears\n // down. The probe falls back to a plain exec on images without `-w`,\n // where the pid-only kill still reaches the direct child.\n const pidFile = `/tmp/.zidane-stream-${Math.random().toString(36).slice(2, 10)}.pid`\n // `sh -c <quoted>` rather than a bare `exec ${command}`: with a\n // compound command (`a; b >&2; c`) a bare exec binds to the FIRST\n // command only and silently drops the rest.\n const leader = `echo ${'$'}${'$'} > ${pidFile}; exec sh -c ${alwaysQuote(command)}`\n const wrapped = `if setsid -w true 2>/dev/null; then exec setsid -w sh -c ${alwaysQuote(leader)}; else exec sh -c ${alwaysQuote(leader)}; fi`\n\n // Typed via the `background: true` overload on `E2BSandbox['commands']`\n // — that is what the overload exists for, so no cast is needed here.\n const handle: E2BCommandHandle = await sandbox.commands.run(wrapped, {\n background: true,\n stdin: true,\n timeoutMs: E2B_STREAM_TIMEOUT_MS,\n ...(opts?.cwd ? { cwd: opts.cwd } : {}),\n envs: resolveE2BEnv(env, opts?.env),\n onStdout: (data: string) => stdout.push(new TextEncoder().encode(data)),\n onStderr: (data: string) => stderr.push(new TextEncoder().encode(data)),\n })\n\n const pid: number = handle.pid\n\n const exited = handle.wait()\n .then(result => result?.exitCode ?? 0)\n // E2B throws CommandExitError on a non-zero exit; the seam promises\n // a never-rejecting `exited`, so translate rather than propagate.\n .catch((err: any) => typeof err?.exitCode === 'number' ? err.exitCode : 137)\n .finally(() => { stdout.close(); stderr.close() })\n\n // Read back the session leader's pid, which `setsid -w` made the\n // group id. Separate one-shot command: the API exposes no group verb.\n let commandExited = false\n void exited.then(() => { commandExited = true })\n const readLeaderPid = async (): Promise<number> => {\n for (let attempt = 0; attempt < 20; attempt++) {\n const probe = await sandbox.commands\n .run(`cat ${pidFile} 2>/dev/null || true`)\n .catch(() => null)\n const parsed = Number.parseInt(probe?.stdout?.trim() ?? '', 10)\n if (Number.isFinite(parsed) && parsed > 0)\n return parsed\n // A dead command will never write the pidfile — a missing binary\n // in the image (the most likely user-facing failure) exits\n // immediately, and burning the remaining attempts would cost up\n // to 20 sequential sandbox round-trips before bootstrap can\n // surface the real error. One more probe already ran above, so\n // nothing written before death is missed.\n if (commandExited)\n break\n await new Promise(r => setTimeout(r, 50))\n }\n return -1\n }\n const leaderPid = await readLeaderPid()\n\n const kill = async (): Promise<void> => {\n if (leaderPid > 0) {\n // Group first (the leader IS the pgid), pid-only as the fallback\n // for images that took the no-setsid branch. NEVER a bare group\n // kill on the reported pid: that group belongs to envd.\n await sandbox.commands\n .run(`kill -TERM -${leaderPid} 2>/dev/null || kill -TERM ${leaderPid} 2>/dev/null || true`)\n .catch(() => {})\n }\n await sandbox.commands.run(`rm -f ${pidFile}`).catch(() => {})\n await sandbox.commands.kill?.(pid).catch(() => {})\n }\n\n if (opts?.signal) {\n if (opts.signal.aborted) {\n void kill()\n }\n else {\n // Detach on exit — the (typically run-long) signal otherwise\n // retains the `kill` closure and the sandbox handle for the rest\n // of the run, one dead closure per exec.\n const onAbort = () => { void kill() }\n const detach = () => opts.signal!.removeEventListener('abort', onAbort)\n opts.signal.addEventListener('abort', onAbort, { once: true })\n exited.then(detach, detach)\n }\n }\n\n return {\n pid,\n stdout,\n stderr,\n write: async (data: Uint8Array): Promise<void> => {\n if (!sandbox.commands.sendStdin)\n throw new Error('E2B SDK is too old: `commands.sendStdin` is required for execStream')\n await sandbox.commands.sendStdin(pid, data)\n },\n kill,\n exited,\n }\n },\n\n async exec(sandboxId, command, opts): Promise<ExecResult> {\n const { sandbox, env } = get(sandboxId)\n // Match the process/docker contexts: `timeout` is in seconds, default 30.\n const timeoutMs = (opts?.timeout ?? 30) * 1000\n try {\n const result = await sandbox.commands.run(command, {\n cwd: opts?.cwd,\n // Re-send the sandbox's configured env on every command (warm\n // sandboxes may lack the create-time injection), with the per-call\n // env layered on top.\n envs: resolveE2BEnv(env, opts?.env),\n timeoutMs: timeoutMs > 0 ? timeoutMs : undefined,\n })\n // The SDK buffers the streams uncapped (see the KNOWN RESIDUAL note\n // on `ExecutionContext.fileSize`) — cap host-side before the strings\n // propagate into tool results and session history.\n return { ...capBufferedExecOutput(result.stdout, result.stderr), exitCode: result.exitCode }\n }\n catch (err: any) {\n // E2B throws on non-zero exit (CommandExitError carries the captured\n // streams) and on timeout. Normalize both into an ExecResult so the\n // shell tool sees a clean exit code rather than a thrown error.\n if (typeof err?.exitCode === 'number') {\n return {\n ...capBufferedExecOutput(err.stdout ?? '', err.stderr ?? err.message ?? ''),\n exitCode: err.exitCode,\n }\n }\n return { stdout: '', stderr: err?.message ?? String(err), exitCode: 124 }\n }\n },\n\n async readFile(sandboxId, path): Promise<string> {\n return get(sandboxId).sandbox.files.read(path)\n },\n\n async writeFile(sandboxId, path, content, encoding: BufferEncoding = 'utf-8'): Promise<void> {\n // A plain utf-8 string rides the SDK's own string path, byte-identical to\n // what it did before byte writes existed: `new Blob(['…'])` encodes utf-8.\n // Only a Buffer or an explicit non-utf8 encoding needs host-side bytes,\n // so the common write keeps hitting the code the SDK actually documents.\n const data = typeof content === 'string'\n ? (encoding === 'utf-8' || encoding === 'utf8' ? content : Buffer.from(content, encoding))\n : content\n await get(sandboxId).sandbox.files.write(path, data)\n },\n\n async listFiles(sandboxId, path): Promise<string[]> {\n const entries = await get(sandboxId).sandbox.files.list(path)\n // Trailing `/` on directories — parity with process/docker/daytona.\n return entries.map(e => (e.type === 'dir' ? `${e.name}/` : e.name))\n },\n\n async destroy(sandboxId): Promise<void> {\n const entry = live.get(sandboxId)\n if (!entry)\n return\n // Never kill/pause a sandbox we only connected to — the caller owns its\n // lifecycle. Just drop our handle to it.\n if (entry.owned) {\n const action = options.onDestroy ?? 'kill'\n if (action === 'pause')\n await pauseOwnedSandbox(entry.sandbox)\n else if (action === 'kill')\n await killOwnedSandbox(entry.sandbox)\n // 'leave' makes no API call — the sandbox runs until E2B's own timeout.\n }\n live.delete(sandboxId)\n },\n }\n}\n"],"mappings":"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"}
@@ -1,2 +1,2 @@
1
- import { n as createSandboxContext, t as SandboxProvider } from "../sandbox-DLY6uah7.js";
1
+ import { n as createSandboxContext, t as SandboxProvider } from "../sandbox-CDl9tfTo.js";
2
2
  export { SandboxProvider, createSandboxContext };
@@ -37,8 +37,8 @@ function createSandboxContext(provider) {
37
37
  async fileSize(handle, path) {
38
38
  return fileSizeViaExec((command) => provider.exec(getSandboxId(handle), command), path);
39
39
  },
40
- async writeFile(handle, path, content) {
41
- return provider.writeFile(getSandboxId(handle), path, content);
40
+ async writeFile(handle, path, content, encoding) {
41
+ return provider.writeFile(getSandboxId(handle), path, content, encoding);
42
42
  },
43
43
  async listFiles(handle, path, options) {
44
44
  const sandboxId = getSandboxId(handle);
@@ -1 +1 @@
1
- {"version":3,"file":"sandbox.js","names":[],"sources":["../../src/contexts/sandbox.ts"],"sourcesContent":["/**\n * Remote sandbox execution context.\n *\n * Offloads execution to a remote sandbox API (e.g. Rivet, E2B).\n * Specific providers implement the SandboxProvider interface.\n */\n\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, ListFilesOptions, SpawnConfig, StreamHandle } from './types'\nimport { fileSizeViaExec } from './exec-file-size'\nimport { globFilesViaShell } from './glob-shell'\n\n// ---------------------------------------------------------------------------\n// Sandbox provider interface\n// ---------------------------------------------------------------------------\n\nexport interface SandboxProvider {\n name: string\n spawn: (config: SpawnConfig) => Promise<{ id: string, cwd: string }>\n exec: (sandboxId: string, command: string, options?: { cwd?: string, env?: Record<string, string>, timeout?: number }) => Promise<ExecResult>\n readFile: (sandboxId: string, path: string) => Promise<string>\n writeFile: (sandboxId: string, path: string, content: string) => Promise<void>\n listFiles: (sandboxId: string, path: string, options?: ListFilesOptions) => Promise<string[]>\n destroy: (sandboxId: string) => Promise<void>\n /**\n * Optional live bidirectional stream — see {@link ExecutionContext.execStream}\n * for the implementor contract. Providers whose backing API has no stdin\n * channel omit it; `createSandboxContext` then leaves `execStream` off the\n * context entirely so `capabilities.stream` stays false and callers get a\n * clear \"unsupported\" error instead of a silent host spawn.\n */\n execStream?: (\n sandboxId: string,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, signal?: AbortSignal },\n ) => Promise<StreamHandle>\n}\n\n// ---------------------------------------------------------------------------\n// Sandbox execution context\n// ---------------------------------------------------------------------------\n\nexport function createSandboxContext(provider: SandboxProvider): ExecutionContext {\n const sandboxes = new Map<string, string>()\n const destroyed = new Set<string>()\n\n // The handle id IS the sandbox id (set in `spawn` below), so the map is an\n // identity record of sandboxes this context instance spawned. A handle this\n // instance never spawned falls back to `handle.id` — the re-attach path for\n // durable hosts that journal the handle and hand it back via\n // `AgentOptions.handle` on a replay where `spawn` never re-ran. An\n // EXPLICITLY-destroyed handle still throws (a use-after-free guard, distinct\n // from never-spawned).\n function getSandboxId(handle: ExecutionHandle): string {\n if (destroyed.has(handle.id))\n throw new Error(`Sandbox ${handle.id} not found`)\n return sandboxes.get(handle.id) ?? handle.id\n }\n\n return {\n type: 'sandbox',\n\n capabilities: {\n shell: true,\n filesystem: true,\n glob: true,\n network: true,\n gpu: false,\n stream: provider.execStream !== undefined,\n } satisfies ContextCapabilities,\n\n async spawn(config?: SpawnConfig): Promise<ExecutionHandle> {\n const result = await provider.spawn(config ?? {})\n const handle: ExecutionHandle = { id: result.id, type: 'sandbox', cwd: result.cwd }\n sandboxes.set(handle.id, result.id)\n return handle\n },\n\n async exec(handle: ExecutionHandle, command: string, options?): Promise<ExecResult> {\n return provider.exec(getSandboxId(handle), command, options)\n },\n\n // Conditionally present: `capabilities.stream` is derived from the\n // provider, so the method must disappear too when the provider has no\n // stdin channel. A stub that always throws would make the two disagree.\n ...(provider.execStream\n ? {\n execStream: async (\n handle: ExecutionHandle,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, signal?: AbortSignal },\n ): Promise<StreamHandle> => provider.execStream!(getSandboxId(handle), command, options),\n }\n : {}),\n\n async readFile(handle: ExecutionHandle, path: string): Promise<string> {\n return provider.readFile(getSandboxId(handle), path)\n },\n\n async fileSize(handle: ExecutionHandle, path: string): Promise<number | null> {\n // Shared exec-probed capability (see exec-file-size.ts). Probe\n // through the sandbox's shell rather than the provider's file API:\n // `readFile` buffers the WHOLE file into host memory, so the\n // whole-read guard needs the size BEFORE any transfer happens. One\n // exec round-trip per whole-read tool call (read_file / edit) — the\n // price of not ballooning the host on a multi-GB sandbox file.\n return fileSizeViaExec(command => provider.exec(getSandboxId(handle), command), path)\n },\n\n async writeFile(handle: ExecutionHandle, path: string, content: string): Promise<void> {\n return provider.writeFile(getSandboxId(handle), path, content)\n },\n\n async listFiles(handle: ExecutionHandle, path: string, options?: ListFilesOptions): Promise<string[]> {\n const sandboxId = getSandboxId(handle)\n if (options?.glob) {\n return globFilesViaShell(\n path,\n command => provider.exec(sandboxId, command, { cwd: handle.cwd }),\n options?.limit ?? 1000,\n )\n }\n return provider.listFiles(sandboxId, path, options)\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n const id = sandboxes.get(handle.id)\n if (!id)\n return\n await provider.destroy(id)\n sandboxes.delete(handle.id)\n // Mark as torn down so a later op on this exact handle fails loudly\n // (use-after-free) instead of silently re-attaching by id.\n destroyed.add(handle.id)\n },\n }\n}\n"],"mappings":";;AAyCA,SAAgB,qBAAqB,UAA6C;CAChF,MAAM,4BAAY,IAAI,IAAoB;CAC1C,MAAM,4BAAY,IAAI,IAAY;CASlC,SAAS,aAAa,QAAiC;EACrD,IAAI,UAAU,IAAI,OAAO,EAAE,GACzB,MAAM,IAAI,MAAM,WAAW,OAAO,GAAG,WAAW;EAClD,OAAO,UAAU,IAAI,OAAO,EAAE,KAAK,OAAO;CAC5C;CAEA,OAAO;EACL,MAAM;EAEN,cAAc;GACZ,OAAO;GACP,YAAY;GACZ,MAAM;GACN,SAAS;GACT,KAAK;GACL,QAAQ,SAAS,eAAe,KAAA;EAClC;EAEA,MAAM,MAAM,QAAgD;GAC1D,MAAM,SAAS,MAAM,SAAS,MAAM,UAAU,CAAC,CAAC;GAChD,MAAM,SAA0B;IAAE,IAAI,OAAO;IAAI,MAAM;IAAW,KAAK,OAAO;GAAI;GAClF,UAAU,IAAI,OAAO,IAAI,OAAO,EAAE;GAClC,OAAO;EACT;EAEA,MAAM,KAAK,QAAyB,SAAiB,SAA+B;GAClF,OAAO,SAAS,KAAK,aAAa,MAAM,GAAG,SAAS,OAAO;EAC7D;EAKA,GAAI,SAAS,aACT,EACE,YAAY,OACV,QACA,SACA,YAC0B,SAAS,WAAY,aAAa,MAAM,GAAG,SAAS,OAAO,EACzF,IACA,CAAC;EAEL,MAAM,SAAS,QAAyB,MAA+B;GACrE,OAAO,SAAS,SAAS,aAAa,MAAM,GAAG,IAAI;EACrD;EAEA,MAAM,SAAS,QAAyB,MAAsC;GAO5E,OAAO,iBAAgB,YAAW,SAAS,KAAK,aAAa,MAAM,GAAG,OAAO,GAAG,IAAI;EACtF;EAEA,MAAM,UAAU,QAAyB,MAAc,SAAgC;GACrF,OAAO,SAAS,UAAU,aAAa,MAAM,GAAG,MAAM,OAAO;EAC/D;EAEA,MAAM,UAAU,QAAyB,MAAc,SAA+C;GACpG,MAAM,YAAY,aAAa,MAAM;GACrC,IAAI,SAAS,MACX,OAAO,kBACL,OACA,YAAW,SAAS,KAAK,WAAW,SAAS,EAAE,KAAK,OAAO,IAAI,CAAC,GAChE,SAAS,SAAS,GACpB;GAEF,OAAO,SAAS,UAAU,WAAW,MAAM,OAAO;EACpD;EAEA,MAAM,QAAQ,QAAwC;GACpD,MAAM,KAAK,UAAU,IAAI,OAAO,EAAE;GAClC,IAAI,CAAC,IACH;GACF,MAAM,SAAS,QAAQ,EAAE;GACzB,UAAU,OAAO,OAAO,EAAE;GAG1B,UAAU,IAAI,OAAO,EAAE;EACzB;CACF;AACF"}
1
+ {"version":3,"file":"sandbox.js","names":[],"sources":["../../src/contexts/sandbox.ts"],"sourcesContent":["/**\n * Remote sandbox execution context.\n *\n * Offloads execution to a remote sandbox API (e.g. Rivet, E2B).\n * Specific providers implement the SandboxProvider interface.\n */\n\nimport type { Buffer } from 'node:buffer'\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, ListFilesOptions, SpawnConfig, StreamHandle } from './types'\nimport { fileSizeViaExec } from './exec-file-size'\nimport { globFilesViaShell } from './glob-shell'\n\n// ---------------------------------------------------------------------------\n// Sandbox provider interface\n// ---------------------------------------------------------------------------\n\nexport interface SandboxProvider {\n name: string\n spawn: (config: SpawnConfig) => Promise<{ id: string, cwd: string }>\n exec: (sandboxId: string, command: string, options?: { cwd?: string, env?: Record<string, string>, timeout?: number }) => Promise<ExecResult>\n readFile: (sandboxId: string, path: string) => Promise<string>\n writeFile: (sandboxId: string, path: string, content: string | Buffer, encoding?: BufferEncoding) => Promise<void>\n listFiles: (sandboxId: string, path: string, options?: ListFilesOptions) => Promise<string[]>\n destroy: (sandboxId: string) => Promise<void>\n /**\n * Optional live bidirectional stream — see {@link ExecutionContext.execStream}\n * for the implementor contract. Providers whose backing API has no stdin\n * channel omit it; `createSandboxContext` then leaves `execStream` off the\n * context entirely so `capabilities.stream` stays false and callers get a\n * clear \"unsupported\" error instead of a silent host spawn.\n */\n execStream?: (\n sandboxId: string,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, signal?: AbortSignal },\n ) => Promise<StreamHandle>\n}\n\n// ---------------------------------------------------------------------------\n// Sandbox execution context\n// ---------------------------------------------------------------------------\n\nexport function createSandboxContext(provider: SandboxProvider): ExecutionContext {\n const sandboxes = new Map<string, string>()\n const destroyed = new Set<string>()\n\n // The handle id IS the sandbox id (set in `spawn` below), so the map is an\n // identity record of sandboxes this context instance spawned. A handle this\n // instance never spawned falls back to `handle.id` — the re-attach path for\n // durable hosts that journal the handle and hand it back via\n // `AgentOptions.handle` on a replay where `spawn` never re-ran. An\n // EXPLICITLY-destroyed handle still throws (a use-after-free guard, distinct\n // from never-spawned).\n function getSandboxId(handle: ExecutionHandle): string {\n if (destroyed.has(handle.id))\n throw new Error(`Sandbox ${handle.id} not found`)\n return sandboxes.get(handle.id) ?? handle.id\n }\n\n return {\n type: 'sandbox',\n\n capabilities: {\n shell: true,\n filesystem: true,\n glob: true,\n network: true,\n gpu: false,\n stream: provider.execStream !== undefined,\n } satisfies ContextCapabilities,\n\n async spawn(config?: SpawnConfig): Promise<ExecutionHandle> {\n const result = await provider.spawn(config ?? {})\n const handle: ExecutionHandle = { id: result.id, type: 'sandbox', cwd: result.cwd }\n sandboxes.set(handle.id, result.id)\n return handle\n },\n\n async exec(handle: ExecutionHandle, command: string, options?): Promise<ExecResult> {\n return provider.exec(getSandboxId(handle), command, options)\n },\n\n // Conditionally present: `capabilities.stream` is derived from the\n // provider, so the method must disappear too when the provider has no\n // stdin channel. A stub that always throws would make the two disagree.\n ...(provider.execStream\n ? {\n execStream: async (\n handle: ExecutionHandle,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, signal?: AbortSignal },\n ): Promise<StreamHandle> => provider.execStream!(getSandboxId(handle), command, options),\n }\n : {}),\n\n async readFile(handle: ExecutionHandle, path: string): Promise<string> {\n return provider.readFile(getSandboxId(handle), path)\n },\n\n async fileSize(handle: ExecutionHandle, path: string): Promise<number | null> {\n // Shared exec-probed capability (see exec-file-size.ts). Probe\n // through the sandbox's shell rather than the provider's file API:\n // `readFile` buffers the WHOLE file into host memory, so the\n // whole-read guard needs the size BEFORE any transfer happens. One\n // exec round-trip per whole-read tool call (read_file / edit) — the\n // price of not ballooning the host on a multi-GB sandbox file.\n return fileSizeViaExec(command => provider.exec(getSandboxId(handle), command), path)\n },\n\n async writeFile(handle: ExecutionHandle, path: string, content: string | Buffer, encoding?: BufferEncoding): Promise<void> {\n return provider.writeFile(getSandboxId(handle), path, content, encoding)\n },\n\n async listFiles(handle: ExecutionHandle, path: string, options?: ListFilesOptions): Promise<string[]> {\n const sandboxId = getSandboxId(handle)\n if (options?.glob) {\n return globFilesViaShell(\n path,\n command => provider.exec(sandboxId, command, { cwd: handle.cwd }),\n options?.limit ?? 1000,\n )\n }\n return provider.listFiles(sandboxId, path, options)\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n const id = sandboxes.get(handle.id)\n if (!id)\n return\n await provider.destroy(id)\n sandboxes.delete(handle.id)\n // Mark as torn down so a later op on this exact handle fails loudly\n // (use-after-free) instead of silently re-attaching by id.\n destroyed.add(handle.id)\n },\n }\n}\n"],"mappings":";;AA0CA,SAAgB,qBAAqB,UAA6C;CAChF,MAAM,4BAAY,IAAI,IAAoB;CAC1C,MAAM,4BAAY,IAAI,IAAY;CASlC,SAAS,aAAa,QAAiC;EACrD,IAAI,UAAU,IAAI,OAAO,EAAE,GACzB,MAAM,IAAI,MAAM,WAAW,OAAO,GAAG,WAAW;EAClD,OAAO,UAAU,IAAI,OAAO,EAAE,KAAK,OAAO;CAC5C;CAEA,OAAO;EACL,MAAM;EAEN,cAAc;GACZ,OAAO;GACP,YAAY;GACZ,MAAM;GACN,SAAS;GACT,KAAK;GACL,QAAQ,SAAS,eAAe,KAAA;EAClC;EAEA,MAAM,MAAM,QAAgD;GAC1D,MAAM,SAAS,MAAM,SAAS,MAAM,UAAU,CAAC,CAAC;GAChD,MAAM,SAA0B;IAAE,IAAI,OAAO;IAAI,MAAM;IAAW,KAAK,OAAO;GAAI;GAClF,UAAU,IAAI,OAAO,IAAI,OAAO,EAAE;GAClC,OAAO;EACT;EAEA,MAAM,KAAK,QAAyB,SAAiB,SAA+B;GAClF,OAAO,SAAS,KAAK,aAAa,MAAM,GAAG,SAAS,OAAO;EAC7D;EAKA,GAAI,SAAS,aACT,EACE,YAAY,OACV,QACA,SACA,YAC0B,SAAS,WAAY,aAAa,MAAM,GAAG,SAAS,OAAO,EACzF,IACA,CAAC;EAEL,MAAM,SAAS,QAAyB,MAA+B;GACrE,OAAO,SAAS,SAAS,aAAa,MAAM,GAAG,IAAI;EACrD;EAEA,MAAM,SAAS,QAAyB,MAAsC;GAO5E,OAAO,iBAAgB,YAAW,SAAS,KAAK,aAAa,MAAM,GAAG,OAAO,GAAG,IAAI;EACtF;EAEA,MAAM,UAAU,QAAyB,MAAc,SAA0B,UAA0C;GACzH,OAAO,SAAS,UAAU,aAAa,MAAM,GAAG,MAAM,SAAS,QAAQ;EACzE;EAEA,MAAM,UAAU,QAAyB,MAAc,SAA+C;GACpG,MAAM,YAAY,aAAa,MAAM;GACrC,IAAI,SAAS,MACX,OAAO,kBACL,OACA,YAAW,SAAS,KAAK,WAAW,SAAS,EAAE,KAAK,OAAO,IAAI,CAAC,GAChE,SAAS,SAAS,GACpB;GAEF,OAAO,SAAS,UAAU,WAAW,MAAM,OAAO;EACpD;EAEA,MAAM,QAAQ,QAAwC;GACpD,MAAM,KAAK,UAAU,IAAI,OAAO,EAAE;GAClC,IAAI,CAAC,IACH;GACF,MAAM,SAAS,QAAQ,EAAE;GACzB,UAAU,OAAO,OAAO,EAAE;GAG1B,UAAU,IAAI,OAAO,EAAE;EACzB;CACF;AACF"}
@@ -368,10 +368,11 @@ function createProcessContext(config) {
368
368
  const buf = await readFile(await resolveContained(handle, path));
369
369
  return new Uint8Array(buf);
370
370
  },
371
- async writeFile(handle, path, content) {
371
+ async writeFile(handle, path, content, encoding = "utf-8") {
372
372
  const fullPath = await resolveContained(handle, path);
373
373
  await mkdir(dirname(fullPath), { recursive: true });
374
- await writeFile(fullPath, content, "utf-8");
374
+ if (typeof content === "string") await writeFile(fullPath, content, encoding);
375
+ else await writeFile(fullPath, content);
375
376
  },
376
377
  async listFiles(handle, path, options) {
377
378
  if (options?.glob) {
@@ -786,4 +787,4 @@ function resolveDetachedTasksCapability(context) {
786
787
  //#endregion
787
788
  export { killProcessGroup as i, SUPPORTS_PROCESS_GROUPS as n, createProcessContext as r, resolveDetachedTasksCapability as t };
788
789
 
789
- //# sourceMappingURL=contexts-DWVt7MvF.js.map
790
+ //# sourceMappingURL=contexts-BSFQVclW.js.map