zidane 6.0.0 → 6.1.0

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 (165) hide show
  1. package/README.md +27 -0
  2. package/dist/{acp-Irby04Us.js → acp-cfR8T19u.js} +8 -8
  3. package/dist/{acp-Irby04Us.js.map → acp-cfR8T19u.js.map} +1 -1
  4. package/dist/acp-cli.js +7 -7
  5. package/dist/acp-cli.js.map +1 -1
  6. package/dist/acp.d.ts +1 -1
  7. package/dist/acp.js +1 -1
  8. package/dist/{agent-DkSmGkDM.js → agent-DZRA7wip.js} +377 -61
  9. package/dist/agent-DZRA7wip.js.map +1 -0
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +1 -1
  12. package/dist/{anthropic-CTHsdFG9.js → anthropic-DNJbRMUK.js} +7 -7
  13. package/dist/anthropic-DNJbRMUK.js.map +1 -0
  14. package/dist/atomic-write-B7zr1ddF.js.map +1 -1
  15. package/dist/{auth-BDSu_0t3.js → auth-B3cB5K6d.js} +7 -7
  16. package/dist/auth-B3cB5K6d.js.map +1 -0
  17. package/dist/cache-telemetry-BKAwsNxm.js.map +1 -1
  18. package/dist/chat/pure.d.ts +4 -4
  19. package/dist/chat/pure.js +3 -3
  20. package/dist/chat.d.ts +44 -21
  21. package/dist/chat.d.ts.map +1 -1
  22. package/dist/chat.js +7 -7
  23. package/dist/chat.js.map +1 -1
  24. package/dist/completion-core-CXYSVhZo.js.map +1 -1
  25. package/dist/context-breakdown-Dzo25N45.js.map +1 -1
  26. package/dist/contexts/daytona.d.ts +3 -1
  27. package/dist/contexts/daytona.d.ts.map +1 -1
  28. package/dist/contexts/daytona.js +2 -2
  29. package/dist/contexts/daytona.js.map +1 -1
  30. package/dist/contexts/docker.d.ts.map +1 -1
  31. package/dist/contexts/docker.js +2 -2
  32. package/dist/contexts/docker.js.map +1 -1
  33. package/dist/contexts/e2b.d.ts +3 -1
  34. package/dist/contexts/e2b.d.ts.map +1 -1
  35. package/dist/contexts/e2b.js +1 -1
  36. package/dist/contexts/e2b.js.map +1 -1
  37. package/dist/{contexts-Vone3qQQ.js → contexts-BIbcfBFH.js} +2 -2
  38. package/dist/contexts-BIbcfBFH.js.map +1 -0
  39. package/dist/contexts.js +1 -1
  40. package/dist/edit-utils-EGosADZq.js.map +1 -1
  41. package/dist/eval.d.ts +1 -1
  42. package/dist/eval.js +4 -3
  43. package/dist/eval.js.map +1 -1
  44. package/dist/exec-file-size-PXkr-MGA.js.map +1 -1
  45. package/dist/extensions.d.ts +1 -1
  46. package/dist/extensions.js +1 -1
  47. package/dist/{fetch-url-CWE8X5OD.js → fetch-url-_5WKWpbQ.js} +2 -2
  48. package/dist/{fetch-url-CWE8X5OD.js.map → fetch-url-_5WKWpbQ.js.map} +1 -1
  49. package/dist/{format-BNOXpl-1.js → format-BF745pLL.js} +9 -2
  50. package/dist/format-BF745pLL.js.map +1 -0
  51. package/dist/generate-data-WL_yGVL6.js.map +1 -1
  52. package/dist/hash-DJRzmcPI.js.map +1 -1
  53. package/dist/headless.d.ts +1 -1
  54. package/dist/headless.js +4 -4
  55. package/dist/headless.js.map +1 -1
  56. package/dist/{index-B8UtiZy7.d.ts → index-BGpM2YaU.d.ts} +378 -5
  57. package/dist/index-BGpM2YaU.d.ts.map +1 -0
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.js +11 -11
  60. package/dist/index.js.map +1 -1
  61. package/dist/logger-DBX9uYOw.js.map +1 -1
  62. package/dist/{login-DOF8pMVM.js → login-DZLG__2f.js} +4 -3
  63. package/dist/{login-DOF8pMVM.js.map → login-DZLG__2f.js.map} +1 -1
  64. package/dist/mcp-CcvuVXB9.js.map +1 -1
  65. package/dist/mcp.d.ts +1 -1
  66. package/dist/media-sniff-Bn76JxAu.js.map +1 -1
  67. package/dist/{messages-CwujiS74.js → messages-C5BDOjUG.js} +2 -2
  68. package/dist/{messages-CwujiS74.js.map → messages-C5BDOjUG.js.map} +1 -1
  69. package/dist/oauth-8LqbeI_Q.js.map +1 -1
  70. package/dist/{openai-compat-BvgYGSR1.js → openai-compat-B0kqvZqN.js} +2 -2
  71. package/dist/openai-compat-B0kqvZqN.js.map +1 -0
  72. package/dist/output/stream-json.d.ts +1 -1
  73. package/dist/output/terminal.d.ts +1 -1
  74. package/dist/output/terminal.js +1 -1
  75. package/dist/output/terminal.js.map +1 -1
  76. package/dist/output-cap-kBUHBlnq.js.map +1 -1
  77. package/dist/presets.d.ts +1 -1
  78. package/dist/presets.js +1 -1
  79. package/dist/providers/anthropic.d.ts +1 -1
  80. package/dist/providers/anthropic.js +1 -1
  81. package/dist/providers/arcee.d.ts +1 -1
  82. package/dist/providers/arcee.js +1 -1
  83. package/dist/providers/baseten.d.ts +1 -1
  84. package/dist/providers/baseten.js +2 -2
  85. package/dist/providers/baseten.js.map +1 -1
  86. package/dist/providers/cerebras.d.ts +1 -1
  87. package/dist/providers/cerebras.js +1 -1
  88. package/dist/providers/local.d.ts +1 -1
  89. package/dist/providers/local.js +1 -1
  90. package/dist/providers/openai-compat.d.ts +1 -1
  91. package/dist/providers/openai-compat.js +1 -1
  92. package/dist/providers/openai.d.ts +1 -1
  93. package/dist/providers/openai.js +8 -8
  94. package/dist/providers/openai.js.map +1 -1
  95. package/dist/providers/openrouter.d.ts +1 -1
  96. package/dist/providers/openrouter.js +1 -1
  97. package/dist/providers/xai.d.ts +1 -1
  98. package/dist/providers/xai.js +1 -1
  99. package/dist/{providers-z6LDMAr3.js → providers-BMlrTr6a.js} +3 -3
  100. package/dist/{providers-z6LDMAr3.js.map → providers-BMlrTr6a.js.map} +1 -1
  101. package/dist/providers.d.ts +1 -1
  102. package/dist/providers.js +4 -4
  103. package/dist/read-state-Dq_TXUX1.js.map +1 -1
  104. package/dist/{resolve-Cr8JSL_O.js → resolve-CC8Lmwcj.js} +3 -3
  105. package/dist/{resolve-Cr8JSL_O.js.map → resolve-CC8Lmwcj.js.map} +1 -1
  106. package/dist/restate.d.ts +1 -1
  107. package/dist/restate.d.ts.map +1 -1
  108. package/dist/restate.js.map +1 -1
  109. package/dist/session/sqlite.d.ts +1 -1
  110. package/dist/session/sqlite.d.ts.map +1 -1
  111. package/dist/session/sqlite.js +75 -20
  112. package/dist/session/sqlite.js.map +1 -1
  113. package/dist/{session-CsdDIPY8.js → session-CsKzK8-P.js} +3 -3
  114. package/dist/session-CsKzK8-P.js.map +1 -0
  115. package/dist/session.d.ts +2 -2
  116. package/dist/session.js +2 -2
  117. package/dist/{skills-Bsb0rvWI.js → skills-BjeXSRlQ.js} +2 -2
  118. package/dist/{skills-Bsb0rvWI.js.map → skills-BjeXSRlQ.js.map} +1 -1
  119. package/dist/skills.d.ts +1 -1
  120. package/dist/skills.js +2 -2
  121. package/dist/stdio-loader-Ce68wUmM.js.map +1 -1
  122. package/dist/{tool-formatters-DY0TDb1r.d.ts → tool-formatters-CMwjDvkp.d.ts} +2 -2
  123. package/dist/tool-formatters-CMwjDvkp.d.ts.map +1 -0
  124. package/dist/tools/fetch-url.d.ts +1 -1
  125. package/dist/tools/fetch-url.d.ts.map +1 -1
  126. package/dist/tools/fetch-url.js +1 -1
  127. package/dist/tools/web-search.d.ts +1 -1
  128. package/dist/tools/web-search.js +1 -1
  129. package/dist/tools/web-search.js.map +1 -1
  130. package/dist/tools.d.ts +2 -2
  131. package/dist/tools.js +2 -2
  132. package/dist/{transcript-anchors-DSRPWjiH.js → transcript-anchors-BTNe1OJi.js} +346 -89
  133. package/dist/transcript-anchors-BTNe1OJi.js.map +1 -0
  134. package/dist/{transcript-anchors-Cb-bDbNH.d.ts → transcript-anchors-D5wvdKQQ.d.ts} +37 -3
  135. package/dist/transcript-anchors-D5wvdKQQ.d.ts.map +1 -0
  136. package/dist/tui.d.ts +21 -6
  137. package/dist/tui.d.ts.map +1 -1
  138. package/dist/tui.js +497 -83
  139. package/dist/tui.js.map +1 -1
  140. package/dist/{turn-operations-Dq9Kx7m2.js → turn-operations-CEt_l3fy.js} +23 -8
  141. package/dist/turn-operations-CEt_l3fy.js.map +1 -0
  142. package/dist/{turn-operations-CgMROLsr.d.ts → turn-operations-JO5MYM9_.d.ts} +27 -9
  143. package/dist/turn-operations-JO5MYM9_.d.ts.map +1 -0
  144. package/dist/types-BIarq1qE.js.map +1 -1
  145. package/dist/types.d.ts +1 -1
  146. package/dist/{xai-B_e6TOA8.js → xai-C_aq671K.js} +3 -3
  147. package/dist/{xai-B_e6TOA8.js.map → xai-C_aq671K.js.map} +1 -1
  148. package/docs/ARCHITECTURE.md +4 -0
  149. package/docs/CHAT.md +12 -10
  150. package/docs/SKILL.md +1 -1
  151. package/docs/TUI.md +3 -3
  152. package/package.json +2 -2
  153. package/dist/agent-DkSmGkDM.js.map +0 -1
  154. package/dist/anthropic-CTHsdFG9.js.map +0 -1
  155. package/dist/auth-BDSu_0t3.js.map +0 -1
  156. package/dist/contexts-Vone3qQQ.js.map +0 -1
  157. package/dist/format-BNOXpl-1.js.map +0 -1
  158. package/dist/index-B8UtiZy7.d.ts.map +0 -1
  159. package/dist/openai-compat-BvgYGSR1.js.map +0 -1
  160. package/dist/session-CsdDIPY8.js.map +0 -1
  161. package/dist/tool-formatters-DY0TDb1r.d.ts.map +0 -1
  162. package/dist/transcript-anchors-Cb-bDbNH.d.ts.map +0 -1
  163. package/dist/transcript-anchors-DSRPWjiH.js.map +0 -1
  164. package/dist/turn-operations-CgMROLsr.d.ts.map +0 -1
  165. package/dist/turn-operations-Dq9Kx7m2.js.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 } 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 } 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 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 const result = await ctx.exec(handle, `ls -1 ${alwaysQuote(path)}`)\n if (result.exitCode !== 0)\n return []\n return result.stdout.trim().split('\\n').filter(Boolean)\n },\n\n async getMappedPort(handle: ExecutionHandle, containerPort: number): Promise<number | null> {\n const ref = containers.get(handle.id)\n if (!ref)\n return null\n\n const info = await ref.container.inspect()\n const bindings = info?.NetworkSettings?.Ports ?? {}\n\n for (const proto of ['tcp', 'udp']) {\n const list = bindings[`${containerPort}/${proto}`]\n if (Array.isArray(list) && list.length > 0 && list[0]?.HostPort) {\n return Number.parseInt(list[0].HostPort, 10)\n }\n }\n\n return null\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n const ref = containers.get(handle.id)\n if (!ref)\n return\n\n try {\n try {\n await ref.container.stop({ t: 5 })\n }\n catch {\n // 304 (already stopped) / 404 (already gone) are expected here.\n // Any other stop failure must not prevent the remove({force})\n // below — force removal also works on running containers.\n }\n try {\n await ref.container.remove({ force: true })\n }\n catch (err) {\n if (!isGoneOrAlreadyStopped(err))\n throw err\n }\n }\n finally {\n containers.delete(handle.id)\n }\n },\n }\n\n return ctx\n}\n\n/**\n * Format one {@link ContextMount} as a dockerode `HostConfig.Binds` entry.\n * Supports read-only (`:ro`) and SELinux shared-label (`:z`) options; the two\n * are mutually exclusive (a read-only mount can't also be a shared rw mount).\n */\nfunction formatBindMount(mount: NonNullable<SpawnConfig['mounts']>[number]): string {\n if (!mount.source)\n throw new Error('Docker mount source is required')\n if (!mount.target || !mount.target.startsWith('/'))\n throw new Error(`Docker mount target must be an absolute path: ${mount.target}`)\n if (mount.readonly && mount.shared)\n throw new Error(`Docker mount cannot be both readonly and shared: ${mount.target}`)\n const opts = mount.readonly ? ':ro' : mount.shared ? ':z' : ''\n return `${mount.source}:${mount.target}${opts}`\n}\n\n/** Assemble dockerode `HostConfig.Binds` from the context's `mounts`. */\nexport function buildBindSpecs(mounts: NonNullable<SpawnConfig['mounts']>): string[] {\n return mounts.map(formatBindMount)\n}\n"],"mappings":"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"}
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 } 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 } 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 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,4 +1,4 @@
1
- import { Ll as Logger } from "../index-B8UtiZy7.js";
1
+ import { Ul as Logger } from "../index-BGpM2YaU.js";
2
2
  import { t as SandboxProvider } from "../sandbox-CgjG2VvQ.js";
3
3
 
4
4
  //#region src/contexts/e2b.d.ts
@@ -9,6 +9,8 @@ interface E2BCommandResult {
9
9
  }
10
10
  interface E2BEntry {
11
11
  name: string;
12
+ /** Present in the E2B SDK's EntryInfo; used for the `/` dir suffix. */
13
+ type?: string;
12
14
  }
13
15
  interface E2BSandbox {
14
16
  sandboxId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"e2b.d.ts","names":[],"sources":["../../src/contexts/e2b.ts"],"mappings":";;;;UAoDU,gBAAA;EACR,MAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGQ,QAAA;EACR,IAAI;AAAA;AAAA,UAGI,UAAA;EACR,SAAA;EACA,QAAA;IACE,GAAA,GAAM,GAAA,UAAa,IAAA;MAAS,GAAA;MAAc,IAAA,GAAO,MAAA;MAAwB,SAAA;IAAA,MAAyB,OAAA,CAAQ,gBAAA;EAAA;EAE5G,KAAA;IACE,IAAA,GAAO,IAAA,aAAiB,OAAA;IACxB,KAAA,GAAQ,IAAA,UAAc,IAAA,aAAiB,OAAA;IACvC,IAAA,GAAO,IAAA,aAAiB,OAAA,CAAQ,QAAA;EAAA;EAElC,IAAA,QAAY,OAAA;EAKZ,KAAA,SAAc,OAAA;EACd,SAAA,SAAkB,OAAA;AAAA;AAAA,UA0BH,kBAAA;EAAA;EAEf,MAAA;;;;;;EAMA,MAAA;EAoBA;;;;;EAdA,QAAA;EAuCA;;;;;;AA0BS;AA4BX;EApFE,SAAA;;;;;EAKA,GAAA;EAmFQ;;;;EA9ER,cAAA;EA4EA;;;;;EAtEA,GAAA,GAAM,MAAA;EAuEN;;;AACQ;AA8BV;EAhGE,MAAA,GAAS,MAAM;;;;;;;;EAQf,uBAAA;EAqG+B;AAAA;AAgBjC;;;;;;EA5GE,OAAA;IAAY,IAAA;IAAc,OAAA;EAAA;EA6G1B;;;;;;;AAGQ;AAmCV;;;;;;;;EAlIE,SAAA;AAAA;AAgJF;;;;;;;;;;;;AAAA,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;EAqG0B;EAnGzC,cAAA;EAmGmE;;AAAe;;;;EA5FlF,aAAA;;EAEA,GAAA;;EAEA,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"}
1
+ {"version":3,"file":"e2b.d.ts","names":[],"sources":["../../src/contexts/e2b.ts"],"mappings":";;;;UAoDU,gBAAA;EACR,MAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGQ,QAAA;EACR,IAAA;EAUA;EARA,IAAI;AAAA;AAAA,UAGI,UAAA;EACR,SAAA;EACA,QAAA;IACE,GAAA,GAAM,GAAA,UAAa,IAAA;MAAS,GAAA;MAAc,IAAA,GAAO,MAAA;MAAwB,SAAA;IAAA,MAAyB,OAAA,CAAQ,gBAAA;EAAA;EAE5G,KAAA;IACE,IAAA,GAAO,IAAA,aAAiB,OAAA;IACxB,KAAA,GAAQ,IAAA,UAAc,IAAA,aAAiB,OAAA;IACvC,IAAA,GAAO,IAAA,aAAiB,OAAA,CAAQ,QAAA;EAAA;EAElC,IAAA,QAAY,OAAA;EAKZ,KAAA,SAAc,OAAA;EACd,SAAA,SAAkB,OAAA;AAAA;AAAA,UA0BH,kBAAA;;EAEf,MAAA;EAAA;;;;;EAMA,MAAA;EA+BA;;;;;EAzBA,QAAA;EAgDY;;;;AAiBH;AA4BX;;;EApFE,SAAA;EAsFQ;;;;EAjFR,GAAA;EAgFS;;;;EA3ET,cAAA;EA6EsB;;;;;EAvEtB,GAAA,GAAM,MAAA;EAwEE;AAAA;AA8BV;;;EAhGE,MAAA,GAAS,MAAM;EAkGf;;;;;;;EA1FA,uBAAA;EAqHoB;;;;;;;;EA5GpB,OAAA;IAAY,IAAA;IAAc,OAAA;EAAA;EA8G1B;;;;;AAEQ;AAmCV;;;;;;;;;AAGuD;EArIrD,SAAA;AAAA;;;;;;;;;;;;;iBA4BoB,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;EAqGoD;EAnGnE,cAAA;EAmGkF;;;;;;EA5FlF,aAAA;;EAEA,GAAA;;EAEA,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"}
@@ -264,7 +264,7 @@ function createE2BProvider(options = {}) {
264
264
  await get(sandboxId).sandbox.files.write(path, content);
265
265
  },
266
266
  async listFiles(sandboxId, path) {
267
- return (await get(sandboxId).sandbox.files.list(path)).map((e) => e.name);
267
+ return (await get(sandboxId).sandbox.files.list(path)).map((e) => e.type === "dir" ? `${e.name}/` : e.name);
268
268
  },
269
269
  async destroy(sandboxId) {
270
270
  const entry = live.get(sandboxId);
@@ -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 } from './types'\nimport { consoleSink, createLogger } from '../logger'\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\ninterface E2BCommandResult {\n stdout: string\n stderr: string\n exitCode: number\n}\n\ninterface E2BEntry {\n name: string\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 }\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 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 return { stdout: result.stdout, stderr: 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 stdout: err.stdout ?? '',\n stderr: 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 return entries.map(e => 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 } from './types'\nimport { consoleSink, createLogger } from '../logger'\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\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\ninterface E2BSandbox {\n sandboxId: string\n commands: {\n run: (cmd: string, opts?: { cwd?: string, envs?: Record<string, string>, timeoutMs?: number }) => Promise<E2BCommandResult>\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 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 return { stdout: result.stdout, stderr: 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 stdout: err.stdout ?? '',\n stderr: 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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"}
@@ -364,7 +364,7 @@ function createProcessContext(config) {
364
364
  } catch {}
365
365
  return contained;
366
366
  }
367
- return readdir(await resolveContained(handle, path));
367
+ return (await readdir(await resolveContained(handle, path), { withFileTypes: true })).map((d) => d.isDirectory() ? `${d.name}/` : d.name);
368
368
  },
369
369
  async execBackground(handle, command, options) {
370
370
  const cwd = options.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
@@ -676,4 +676,4 @@ function resolveDetachedTasksCapability(context) {
676
676
  //#endregion
677
677
  export { createProcessContext as n, resolveDetachedTasksCapability as t };
678
678
 
679
- //# sourceMappingURL=contexts-Vone3qQQ.js.map
679
+ //# sourceMappingURL=contexts-BIbcfBFH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contexts-BIbcfBFH.js","names":[],"sources":["../src/contexts/process.ts","../src/contexts/types.ts"],"sourcesContent":["/**\n * In-process execution context.\n *\n * Runs everything in the current Node/Bun process.\n * No isolation — fastest, used as the default.\n */\n\nimport type { Buffer } from 'node:buffer'\nimport type { ChildProcess } from 'node:child_process'\nimport type { WriteStream } from 'node:fs'\nimport type { CappedSlot } from './output-cap'\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, ListFilesEntry, SpawnConfig, TaskEntry, TaskExitInfo, TaskHandle, TaskStallInfo } from './types'\nimport { createWriteStream } from 'node:fs'\nimport { mkdir, readdir, readFile, realpath, stat, writeFile } from 'node:fs/promises'\nimport { dirname, isAbsolute, relative, resolve } from 'node:path'\nimport { lazyAsync } from '../lazy'\nimport { globFilesLocal, globLiteralPrefix } from './glob'\nimport { appendCapped, DEFAULT_MAX_OUTPUT_BUFFER, outputExceededMessage } from './output-cap'\n\n/**\n * Lazily load `node:child_process`'s `spawn` (memoized after first use).\n *\n * Deliberately NOT a static top-level import. `createAgent` pulls this module\n * into the graph as the default execution context, so a static\n * `import … from 'node:child_process'` would make merely importing zidane\n * depend on subprocess support. Runtimes that gate that builtin at import time\n * — Supabase Edge Functions, Cloudflare `nodejs_compat`, other shell-less V8\n * isolates — would then fail to load the package at all. Deferring the import\n * to the first `exec` keeps `import 'zidane'` + the provider loop runnable\n * there; only actually invoking a shell command touches subprocess, which is\n * exactly where an unsupported host should fail (the agent loop turns that\n * throw into a clean tool-result error).\n */\nconst loadSpawn = lazyAsync(async () => (await import('node:child_process')).spawn)\n\n/**\n * Whether the host supports POSIX process groups (the `detached: true` +\n * `process.kill(-pid)` combination). Windows doesn't — its job-object\n * model is shaped differently — so on win32 we fall back to killing the\n * shell wrapper alone (matches pre-fix behavior; better than nothing).\n */\nconst SUPPORTS_PROCESS_GROUPS = process.platform !== 'win32'\n\n/**\n * Default cap on captured stdout / stderr per child. Matches the\n * pre-fix `child_process.exec` setting so existing callers see the\n * same buffer envelope. Output beyond this is truncated and a\n * marker is appended to stderr.\n */\n\n/**\n * How long `destroy()` waits for a SIGTERM'd background task to settle\n * before escalating to SIGKILL and abandoning the wait. Matches the MCP\n * connection's default `closeTimeout` so `agent.destroy()` stays bounded\n * by a single grace period regardless of which leg is slowest.\n */\nconst DESTROY_SIGTERM_GRACE_MS = 5_000\n\n/**\n * How long after the child's `exit` event we keep waiting for `close`\n * before settling with the output collected so far.\n *\n * `close` fires only once every stdio pipe has closed — and a\n * daemonized grandchild that inherited stdout/stderr (a dev server\n * started with `&`, `nohup` without redirection, simulator helpers, …)\n * keeps those pipes open indefinitely. Pre-fix, a foreground `exec`\n * whose command finished in milliseconds would hang until its timeout,\n * then `killProcessGroup` took the intentionally-backgrounded daemon\n * down with it. The process itself is done at `exit`; this grace only\n * exists to let normally-closing pipes flush their tail bytes.\n */\nconst EXIT_PIPE_DRAIN_GRACE_MS = 1_500\n\n/**\n * Sanitize a task id before it's joined into a filesystem path.\n *\n * We mint `bash_<n>` ids ourselves (no user input flows into the path\n * for `ProcessContext`), so this is defensive — but third-party contexts\n * MAY accept caller-provided ids, so the helper exists for them too.\n * Anything that doesn't match the expected shape is rejected — never\n * coerced — so the call site sees a clear error rather than a\n * traversal-shaped path.\n */\nconst TASK_ID_RE = /^[a-z][\\w-]*$/i\n\nfunction assertSafeTaskId(taskId: string): void {\n if (!TASK_ID_RE.test(taskId))\n throw new Error(`Invalid task id \"${taskId}\" — must match ${TASK_ID_RE}.`)\n}\n\n/**\n * Format `date` as `YYYYMMDD-HHMMSS-mmm` in UTC.\n *\n * Pinned to UTC so the lexical sort of two timestamps always matches\n * their chronological order (local time + DST does not). Used as the\n * per-context suffix on background-task log filenames; see the field\n * doc on `contextTimestamp` for the why.\n */\nexport function formatContextTimestamp(date: Date): string {\n const pad2 = (n: number): string => n.toString().padStart(2, '0')\n const pad3 = (n: number): string => n.toString().padStart(3, '0')\n const y = date.getUTCFullYear()\n const M = pad2(date.getUTCMonth() + 1)\n const d = pad2(date.getUTCDate())\n const h = pad2(date.getUTCHours())\n const m = pad2(date.getUTCMinutes())\n const s = pad2(date.getUTCSeconds())\n const ms = pad3(date.getUTCMilliseconds())\n return `${y}${M}${d}-${h}${m}${s}-${ms}`\n}\n\n/** Pattern of a background-task log filename. Used by tests + tooling. */\nexport const TASK_LOG_FILENAME_RE = /^(bash_\\d+)\\.(\\d{8}-\\d{6}-\\d{3})\\.log$/\n\n/** Copy only the named keys (when present) from an env object. */\nfunction pickEnv(env: NodeJS.ProcessEnv, keys: string[]): NodeJS.ProcessEnv {\n const out: NodeJS.ProcessEnv = {}\n for (const key of keys) {\n if (env[key] !== undefined)\n out[key] = env[key]\n }\n return out\n}\n\nexport function createProcessContext(config?: SpawnConfig): ExecutionContext {\n let counter = 0\n const handles = new Map<string, ExecutionHandle>()\n const defaultCwd = config?.cwd ?? process.cwd()\n const defaultEnv = config?.env\n const destroyGraceMs = config?.destroyGraceMs ?? DESTROY_SIGTERM_GRACE_MS\n\n // Base environment children inherit. Defaults to the full parent env\n // (back-compat); when `inheritEnv: false`, only a minimal allow-list is\n // forwarded so parent secrets (e.g. API keys Bun loaded from `.env`) aren't\n // exposed to tool commands. Explicit `env` / per-call env always apply on top.\n const baseEnv: NodeJS.ProcessEnv = config?.inheritEnv === false\n ? pickEnv(process.env, ['PATH', 'HOME', 'SHELL', 'LANG', 'LC_ALL', 'USER', 'TERM', 'TMPDIR'])\n : process.env\n\n // Optional workspace containment for the file methods. Unset = no checks.\n const workspaceRoot = config?.workspaceRoot !== undefined\n ? resolve(defaultCwd, config.workspaceRoot)\n : undefined\n\n /** True when `target` is the root itself or lives under it. */\n function isWithinRoot(root: string, target: string): boolean {\n const rel = relative(root, target)\n return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel))\n }\n\n /**\n * Resolve `path` against the handle cwd and, when a `workspaceRoot` is\n * configured, reject anything that escapes it. Uses `realpath` on the\n * nearest existing ancestor so symlink escapes (and `..`) are caught for\n * both existing and not-yet-created targets.\n */\n async function resolveContained(handle: ExecutionHandle, path: string): Promise<string> {\n const full = resolve(handle.cwd, path)\n if (workspaceRoot === undefined)\n return full\n\n // Canonicalize the deepest existing ancestor to defeat symlink escapes;\n // the non-existent tail (for writes) is appended back verbatim.\n let existing = full\n const tail: string[] = []\n while (true) {\n try {\n existing = await realpath(existing)\n break\n }\n catch {\n const parent = dirname(existing)\n if (parent === existing) {\n // Reached the filesystem root without an existing ancestor.\n existing = parent\n break\n }\n // `relative` (not slice) so the segment is correct even on Windows\n // drive roots where `dirname('C:\\\\foo')` is `C:\\\\` (no 1-char sep).\n tail.unshift(relative(parent, existing))\n existing = parent\n }\n }\n const canonical = tail.length ? resolve(existing, ...tail) : existing\n const canonicalRoot = await realpath(workspaceRoot).catch(() => workspaceRoot)\n if (!isWithinRoot(canonicalRoot, canonical)) {\n throw new Error(\n `Path escapes the workspace root: ${path} (resolved outside ${canonicalRoot})`,\n )\n }\n return full\n }\n\n /**\n * Per-context background-task registry. Entries live for the context's\n * lifetime — even after the child exits — so the model can read output\n * of completed tasks until `destroy()` tears everything down. Kept as\n * a plain Map (not a class) per code-quality checklist #7: no premature\n * abstraction. The state and the operations on it live inline.\n */\n const tasks = new Map<string, TaskState>()\n let taskCounter = 0\n\n /**\n * Per-context UTC timestamp segment baked into every background task's\n * log filename. Same `taskCounter` value across two contexts (e.g. a\n * TUI restart) would otherwise re-open the SAME `bash_<n>.log` file —\n * we open with `flags: 'a'` so the new task would APPEND into the old\n * log, producing scrambled output. The timestamp guarantees each\n * context owns a distinct log filename without forcing the\n * model-facing task id (`bash_<n>`) to grow longer.\n *\n * Format: `YYYYMMDD-HHMMSS-mmm` in UTC.\n * - Sortable (lexical sort = chronological sort).\n * - Unambiguous (UTC sidesteps DST / locale shifts).\n * - Filesystem-safe (digits + hyphens only).\n * - Millisecond precision avoids same-second-restart collisions.\n *\n * The timestamp is computed once per context, NOT per task — all of a\n * context's tasks share the same suffix so a directory listing groups\n * cleanly by \"which run produced these\".\n */\n const contextTimestamp = formatContextTimestamp(new Date())\n\n /**\n * Last-resort orphan reaper. With `detached: true`, background tasks\n * are in their OWN process group — when the parent (zidane TUI) dies,\n * the OS does NOT send them SIGHUP and they keep running indefinitely.\n * The user's \"Ctrl+C the TUI\" intent is \"stop everything I started\",\n * not \"leak a `sleep 60` into the background\".\n *\n * `process.on('exit')` fires SYNCHRONOUSLY on `process.exit()` AND on\n * natural shutdown — exactly the seam we need. The handler can only\n * do synchronous work (Node ignores async), but `process.kill` IS\n * synchronous, so a SIGTERM-the-group sweep lands cleanly. The kill\n * is best-effort: already-dead children throw ESRCH (swallowed).\n *\n * Registered lazily on first `execBackground` so contexts that never\n * background a task don't pay the listener cost. Deregistered in\n * `destroy()` so reconstructed contexts don't accumulate listeners\n * (Node warns past 10 — a long-running session that switches sessions\n * frequently would otherwise hit that).\n */\n let exitHandlerRegistered = false\n const exitHandler = (): void => {\n for (const task of tasks.values()) {\n if (task.status !== 'running')\n continue\n const pid = task.child.pid\n if (pid === undefined)\n continue\n try {\n if (SUPPORTS_PROCESS_GROUPS)\n process.kill(-pid, 'SIGTERM')\n else\n process.kill(pid, 'SIGTERM')\n }\n catch {\n // ESRCH (already dead) / EPERM (lost ownership). Swallow —\n // the process either won't kill cleanly OR is already gone,\n // both acceptable at shutdown.\n }\n }\n }\n\n return {\n type: 'process',\n\n capabilities: {\n shell: true,\n filesystem: true,\n glob: true,\n network: true,\n gpu: false,\n // Background tasks are OS children of this process's machine —\n // they survive `agent.destroy()` reassignment games but NOT a\n // host machine reboot, and the orphan reaper kills them on\n // process exit. That's the 'process-lifetime' tier.\n detachedTasks: 'process-lifetime',\n } satisfies ContextCapabilities,\n\n async spawn(overrides?: SpawnConfig): Promise<ExecutionHandle> {\n const id = `process-${++counter}`\n const cwd = overrides?.cwd ?? defaultCwd\n\n // Only create `cwd` when it is genuinely missing. An unconditional\n // `mkdir(cwd, { recursive: true })` on an already-existing directory is a\n // wasted write that needlessly demands FS-write permission — which breaks\n // read-only / no-write runtimes (Supabase Edge Functions write only to\n // `/tmp`; Deno edge isolates) where the cwd already exists. `stat` is a\n // read (permitted there); the write is attempted only on a confirmed\n // ENOENT. Any other stat failure (e.g. EACCES on a sandbox-restricted\n // path) must NOT escalate to a mkdir we'd also be denied — assume present\n // and let a later file op surface a precise error if it truly isn't.\n let needsCreate = false\n try {\n await stat(cwd)\n }\n catch (err) {\n needsCreate = (err as NodeJS.ErrnoException)?.code === 'ENOENT'\n }\n if (needsCreate)\n await mkdir(cwd, { recursive: true })\n\n const handle: ExecutionHandle = { id, type: 'process', cwd }\n handles.set(id, handle)\n return handle\n },\n\n async exec(\n handle: ExecutionHandle,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, timeout?: number, signal?: AbortSignal },\n ): Promise<ExecResult> {\n const cwd = options?.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd\n\n // Pre-aborted fast path: skip the spawn entirely and synthesize a\n // killed-by-signal result. Saves a spawn round-trip + dodges Node\n // emitting an immediate `AbortError`.\n if (options?.signal?.aborted) {\n return { stdout: '', stderr: 'aborted by signal before spawn', exitCode: 143 }\n }\n\n const timeoutMs = (options?.timeout ?? config?.limits?.timeout ?? 30) * 1000\n const maxBuffer = DEFAULT_MAX_OUTPUT_BUFFER\n\n // Lazy subprocess load (see `loadSpawn`) — awaited before the Promise\n // executor so the spawn primitive is ready synchronously inside it.\n const spawnChild = await loadSpawn()\n\n return new Promise<ExecResult>((resolveP) => {\n // Spawn as a NEW process group leader so we can kill the whole\n // subtree on abort. Without `detached: true`, sending SIGTERM to\n // the shell's pid only kills the shell wrapper — its child\n // processes (the actual `sleep`, `npm`, `python`, …) get\n // reparented to init and keep running. `process.kill(-pid, …)`\n // with a NEGATIVE pid targets the whole process group; that's\n // the POSIX idiom for \"shut down everything I started\".\n //\n // On Windows there are no process groups in the POSIX sense, so\n // we leave `detached` off and accept the shell-only kill — the\n // platform's job-object machinery is the path forward there if\n // we ever need it, but it's not the bug zidane's users are\n // hitting today.\n const child = spawnChild('/bin/sh', ['-c', command], {\n cwd,\n env: { ...baseEnv, ...defaultEnv, ...options?.env },\n stdio: ['ignore', 'pipe', 'pipe'],\n detached: SUPPORTS_PROCESS_GROUPS,\n })\n\n // Byte-capped accumulation via the shared exec-transport primitive\n // (see contexts/output-cap.ts) — byte accounting + codepoint-safe\n // cuts. Overflow orchestration (kill the process group, matching\n // `execAsync`'s maxBuffer behavior) stays here.\n const stdoutSlot: CappedSlot = { text: '', bytes: 0 }\n const stderrSlot: CappedSlot = { text: '', bytes: 0 }\n let bufferTruncated = false\n let timedOut = false\n let killedByAbort = false\n let settled = false\n\n const onChunk = (slot: CappedSlot, chunk: Buffer): void => {\n const outcome = appendCapped(slot, chunk, maxBuffer)\n if (outcome !== 'ok' && !bufferTruncated) {\n bufferTruncated = true\n killProcessGroup(child, 'SIGTERM')\n }\n }\n\n child.stdout?.on('data', chunk => onChunk(stdoutSlot, chunk as Buffer))\n child.stderr?.on('data', chunk => onChunk(stderrSlot, chunk as Buffer))\n\n const timeoutTimer = timeoutMs > 0\n ? setTimeout(() => {\n timedOut = true\n killProcessGroup(child, 'SIGTERM')\n }, timeoutMs)\n : undefined\n\n const onAbort = (): void => {\n killedByAbort = true\n killProcessGroup(child, 'SIGTERM')\n }\n const userSignal = options?.signal\n if (userSignal)\n userSignal.addEventListener('abort', onAbort, { once: true })\n\n let exitGraceTimer: NodeJS.Timeout | undefined\n\n const settle = (exitCode: number, extraStderr?: string): void => {\n if (settled)\n return\n settled = true\n if (timeoutTimer)\n clearTimeout(timeoutTimer)\n if (exitGraceTimer)\n clearTimeout(exitGraceTimer)\n if (userSignal)\n userSignal.removeEventListener('abort', onAbort)\n const finalStderr = extraStderr\n ? (stderrSlot.text ? `${stderrSlot.text}\\n${extraStderr}` : extraStderr)\n : stderrSlot.text\n resolveP({ stdout: stdoutSlot.text, stderr: finalStderr, exitCode })\n }\n\n // Shared classification ladder for `close` (fast path: pipes\n // drained normally) and the post-`exit` grace (pipes held open\n // by a daemonized grandchild). Order matters: killed-by-our-\n // abort wins over timeout wins over natural exit, because the\n // abort listener fires first when the user cancels mid-\n // timeout-window.\n const settleFromStatus = (code: number | null, signal: NodeJS.Signals | null): void => {\n if (killedByAbort) {\n settle(143, 'aborted by signal')\n return\n }\n if (timedOut) {\n settle(124, `command timed out after ${timeoutMs}ms`)\n return\n }\n if (bufferTruncated) {\n settle(143, outputExceededMessage(maxBuffer, 'process killed'))\n return\n }\n if (signal) {\n // Killed by some other signal we didn't issue. Treat as\n // signal-killed for consumer compat.\n settle(128 + 15, `terminated by signal ${signal}`)\n return\n }\n settle(typeof code === 'number' ? code : 1)\n }\n\n child.on('error', (err) => {\n // Spawn failure (ENOENT on `/bin/sh`, EACCES, …). Mirror\n // `execAsync`'s \"rejects with an Error\" shape by surfacing\n // the message on stderr and a non-zero exit.\n settle(1, err.message)\n })\n\n child.on('close', (code, signal) => settleFromStatus(code, signal))\n\n child.on('exit', (code, signal) => {\n // The command is DONE here — `close` only adds \"every stdio\n // pipe closed\", which a daemonized grandchild can block\n // forever (see EXIT_PIPE_DRAIN_GRACE_MS). Give the pipes a\n // short drain grace, then settle with what we have and\n // release them. The daemon keeps running — that was the\n // command's intent; killing the group here would be wrong.\n exitGraceTimer = setTimeout(() => {\n if (settled)\n return\n child.stdout?.destroy()\n child.stderr?.destroy()\n settleFromStatus(code, signal)\n }, EXIT_PIPE_DRAIN_GRACE_MS)\n exitGraceTimer.unref?.()\n })\n })\n },\n\n async readFile(handle: ExecutionHandle, path: string): Promise<string> {\n return readFile(await resolveContained(handle, path), 'utf-8')\n },\n\n async fileSize(handle: ExecutionHandle, path: string): Promise<number | null> {\n try {\n // Same containment as every other file method. `stat` (NOT lstat):\n // the caller wants the TARGET's size — an lstat of a symlink\n // reports the link itself and a link to a 10GB file would sail\n // past the whole-read guard this probe exists to feed.\n const info = await stat(await resolveContained(handle, path))\n return info.isFile() ? info.size : null\n }\n catch {\n return null\n }\n },\n\n async readFileBinary(handle: ExecutionHandle, path: string): Promise<Uint8Array> {\n // No encoding → returns a Buffer (which is a Uint8Array). Used by\n // read_file to ferry image / binary content into the multimodal route.\n const buf = await readFile(await resolveContained(handle, path))\n return new Uint8Array(buf)\n },\n\n async writeFile(handle: ExecutionHandle, path: string, content: string): Promise<void> {\n const fullPath = await resolveContained(handle, path)\n await mkdir(dirname(fullPath), { recursive: true })\n await writeFile(fullPath, content, 'utf-8')\n },\n\n async listFiles(handle: ExecutionHandle, path: string, options): Promise<ListFilesEntry[]> {\n if (options?.glob) {\n await resolveContained(handle, globLiteralPrefix(path) || '.')\n const entries = await globFilesLocal(path, handle.cwd, {\n limit: options?.limit ?? 1000,\n metadata: options?.metadata,\n gitignore: options?.gitignore,\n signal: options?.signal,\n })\n if (workspaceRoot === undefined)\n return entries\n const contained: ListFilesEntry[] = []\n for (const entry of entries) {\n try {\n await resolveContained(handle, entry.path)\n contained.push(entry)\n }\n catch {\n // Symlink escapes can be discovered by broad globs like `**/*`.\n // Keep the process context's workspaceRoot invariant by filtering\n // them exactly as read/write paths are filtered.\n }\n }\n return contained\n }\n // Literal listing — suffix directories with `/` so consumers (the\n // `list_files` tool, path suggestions) can tell files from dirs\n // without a second probe. Mirrors `ls -p` in the docker context.\n const dirents = await readdir(await resolveContained(handle, path), { withFileTypes: true })\n return dirents.map(d => (d.isDirectory() ? `${d.name}/` : d.name))\n },\n\n async execBackground(\n handle: ExecutionHandle,\n command: string,\n options: {\n cwd?: string\n env?: Record<string, string>\n outputDir: string\n onExit?: (info: TaskExitInfo) => void\n maxOutputBytes?: number\n stallTimeoutMs?: number\n onStall?: (info: TaskStallInfo) => void\n },\n ): Promise<TaskHandle> {\n const cwd = options.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd\n\n // Resolve the subprocess primitive up front (see `loadSpawn`) so a runtime\n // without `node:child_process` fails BEFORE we allocate the log dir +\n // output stream — no orphaned fd / log file on an unsupported host.\n const spawnChild = await loadSpawn()\n\n await mkdir(options.outputDir, { recursive: true })\n\n // Mint id + path. The id is sequential per context (model-facing,\n // short, ergonomic for `shell_kill`). The log FILENAME embeds the\n // context's start timestamp so two contexts sharing an `outputDir`\n // (TUI restart on the same session, concurrent zidane instances,\n // …) never resolve to the same file — we open with `flags: 'a'`\n // and a name collision would interleave their output. Path\n // validation is defensive — we mint our own ids so it never trips\n // today, but it pins the invariant for forks / third parties.\n const taskId = `bash_${++taskCounter}`\n assertSafeTaskId(taskId)\n const outputPath = resolve(options.outputDir, `${taskId}.${contextTimestamp}.log`)\n\n // Install the orphan reaper on first task. See `exitHandler`'s\n // JSDoc for the kill-on-shutdown rationale.\n if (!exitHandlerRegistered) {\n process.on('exit', exitHandler)\n exitHandlerRegistered = true\n }\n\n // Open the output file. The timestamped path is unique per context\n // so a brand-new file is the expected outcome; `flags: 'a'` is kept\n // as the safe default (preserves bytes if the path collides for any\n // reason — same-millisecond context creation, manual pre-population,\n // etc.) rather than blindly truncating. Streams are opened BEFORE\n // the spawn to avoid a race where the child writes before the stream\n // is ready — `child_process` buffers stdio until the consumer\n // attaches, but the FS handle has to exist either way for our pipe.\n const outputStream: WriteStream = createWriteStream(outputPath, { flags: 'a' })\n // Surface FS errors (ENOSPC, EACCES on a remounted FS, etc.)\n // under ZIDANE_DEBUG instead of crashing the host via an\n // unhandled 'error' event. Without a listener Node escalates\n // any stream-level error to an uncaughtException and the whole\n // process exits — the model and the user would lose every\n // unrelated in-flight piece of work to one bad task's disk\n // hiccup. Swallow + log is the safer default for a fire-and-\n // forget log writer; the task's exit code still reports.\n outputStream.on('error', (err) => {\n if (process.env.ZIDANE_DEBUG)\n process.stderr.write(`[zidane/contexts] task ${taskId} log stream error: ${err.message}\\n`)\n })\n\n // Spawn as a NEW process group leader so we can kill the whole\n // subtree on demand. Same primitive `exec` uses for foreground\n // shells — see the long comment in that method for the\n // process-group rationale.\n const child = spawnChild('/bin/sh', ['-c', command], {\n cwd,\n env: { ...baseEnv, ...defaultEnv, ...options.env },\n stdio: ['ignore', 'pipe', 'pipe'],\n detached: SUPPORTS_PROCESS_GROUPS,\n })\n\n const state: TaskState = {\n taskId,\n handleId: handle.id,\n pid: child.pid ?? -1,\n command,\n cwd,\n startedAt: Date.now(),\n outputPath,\n outputStream,\n child,\n status: 'running',\n bytesWritten: 0,\n settled: false,\n onExit: options.onExit,\n }\n tasks.set(taskId, state)\n\n // Output cap (optional). Beyond the cap the process keeps\n // running — head-priority retention: bytes already on disk stay,\n // subsequent bytes are counted but dropped, and a structured\n // truncation marker is appended at settle so the model can\n // pattern-match the loss. Killing on overflow (what the\n // foreground `exec` does) would be wrong here: long-running\n // servers legitimately log forever.\n const maxOutputBytes = typeof options.maxOutputBytes === 'number' && options.maxOutputBytes > 0\n ? options.maxOutputBytes\n : undefined\n let droppedBytes = 0\n // Latched on the first overflowing chunk. Without it, a codepoint-\n // boundary backoff (below) leaves a few bytes of \"room\" that later\n // chunks would dribble into — appending disjoint fragments from\n // much-later output right after the cut point.\n let capExhausted = false\n\n // Stall watchdog (optional). One-shot per quiet period: fires\n // `onStall` after `stallTimeoutMs` of no output, then stays quiet\n // until fresh output re-arms it. `unref()` so a pending timer\n // never holds the host process open.\n const stallTimeoutMs = typeof options.stallTimeoutMs === 'number' && options.stallTimeoutMs > 0\n ? options.stallTimeoutMs\n : undefined\n const onStall = options.onStall\n let stallTimer: NodeJS.Timeout | undefined\n let lastOutputAt = Date.now()\n const armStallTimer = (): void => {\n if (!stallTimeoutMs || !onStall)\n return\n if (stallTimer)\n clearTimeout(stallTimer)\n stallTimer = setTimeout(() => {\n stallTimer = undefined\n if (state.settled)\n return\n try {\n onStall({\n taskId,\n command,\n outputPath,\n stalledForMs: Date.now() - lastOutputAt,\n bytesWritten: state.bytesWritten,\n })\n }\n catch (err) {\n if (process.env.ZIDANE_DEBUG)\n process.stderr.write(`[zidane/contexts] task ${taskId} onStall threw: ${err instanceof Error ? err.message : String(err)}\\n`)\n }\n }, stallTimeoutMs)\n stallTimer.unref?.()\n }\n const clearStallTimer = (): void => {\n if (stallTimer) {\n clearTimeout(stallTimer)\n stallTimer = undefined\n }\n }\n armStallTimer()\n\n // Pipe both streams into the same file. Order between stdout and\n // stderr is preserved per-stream; cross-stream ordering depends on\n // Node's event loop — acceptable interleaving for log-shaped\n // output. Tracked-bytes is updated on every chunk for the\n // listBackground UX (counted even past the cap, so the UX shows\n // the task's TRUE output volume).\n const appendChunk = (chunk: Buffer): void => {\n state.bytesWritten += chunk.length\n lastOutputAt = Date.now()\n armStallTimer()\n if (maxOutputBytes !== undefined) {\n const onDisk = state.bytesWritten - droppedBytes - chunk.length\n if (capExhausted || onDisk >= maxOutputBytes) {\n capExhausted = true\n droppedBytes += chunk.length\n return\n }\n const room = maxOutputBytes - onDisk\n if (chunk.length > room) {\n capExhausted = true\n // Cut on a UTF-8 codepoint boundary, same as the foreground\n // exec cap: back off while the byte after the cut is a\n // continuation byte (0b10xxxxxx) so the log never ends in a\n // mangled half-codepoint right before the truncation marker.\n let end = room\n while (end > 0 && (chunk[end] & 0xC0) === 0x80)\n end--\n droppedBytes += chunk.length - end\n if (end > 0)\n outputStream.write(chunk.subarray(0, end))\n return\n }\n }\n outputStream.write(chunk)\n }\n child.stdout?.on('data', chunk => appendChunk(chunk as Buffer))\n child.stderr?.on('data', chunk => appendChunk(chunk as Buffer))\n\n // Settle path — at-most-once via `settled` flag (checklist #14).\n // Three trigger sources: `close` (natural OR signal-killed),\n // `error` (spawn failure), explicit `killBackground` (which\n // routes through `close` itself after the SIGTERM lands).\n const settle = (cause: 'close' | 'error', code: number | null, signal: NodeJS.Signals | null, errMessage?: string): void => {\n if (state.settled)\n return\n state.settled = true\n clearStallTimer()\n state.endedAt = Date.now()\n\n // Determine final status from cause + signal.\n const status: TaskExitInfo['status']\n = signal === 'SIGTERM' || state.killRequested\n ? 'killed'\n : 'exited'\n // Signal-killed children report null `code` from Node; map back\n // to the POSIX `128 + signum` convention so consumers can read\n // an integer either way.\n const exitCode = code !== null\n ? code\n : signal === 'SIGTERM'\n ? 143\n : signal\n ? 128\n : 1\n state.status = status\n state.exitCode = exitCode\n if (signal)\n state.signal = signal\n\n // Flush + close the WriteStream BEFORE firing onExit — model\n // may read the file in the same turn it receives the\n // notification, and a still-open stream can hold tail bytes\n // back from disk. `stream.end(callback)` is the documented\n // \"all queued writes are flushed when this fires\" idiom.\n //\n // ORDER MATTERS: any error preamble we want in the log file\n // (spawn failures with no stdout, buffer overflows) MUST be\n // written BEFORE `end()` — once `end()` is called the stream\n // is closed for writing and subsequent `.write()` calls are\n // dropped. Earlier revisions had this reversed and silently\n // lost ENOENT-on-`/bin/sh` messages.\n if (errMessage) {\n try {\n outputStream.write(`\\n${errMessage}\\n`)\n }\n catch {\n // Stream may have errored before this — best-effort only.\n }\n }\n if (droppedBytes > 0) {\n try {\n // Structured + loud — the model can pattern-match the tag\n // rather than misread a truncated log as the full output.\n outputStream.write(`\\n<output-truncated bytes-dropped=\"${droppedBytes}\"/>\\n`)\n }\n catch {\n // Best-effort only, same as the error preamble above.\n }\n }\n outputStream.end(() => {\n // `stateToTaskExitInfo` reads the same fields we just set\n // on `state`, so the snapshot the consumer gets matches the\n // post-settle state exactly. `onExit` is optional — pull-based\n // consumers reconcile via `listBackground` instead.\n try {\n state.onExit?.(stateToTaskExitInfo(state))\n }\n catch (err) {\n // Defensive — a buggy onExit callback shouldn't crash the\n // host. Surface via stderr under ZIDANE_DEBUG; otherwise\n // swallow. Matches the spawn-tool's bubbleError pattern.\n if (process.env.ZIDANE_DEBUG)\n process.stderr.write(`[zidane/contexts] task ${taskId} onExit threw: ${err instanceof Error ? err.message : String(err)}\\n`)\n }\n })\n }\n\n child.on('close', (code, signal) => settle('close', code, signal))\n child.on('error', err => settle('error', null, null, `[spawn error] ${err.message}`))\n\n child.on('exit', (code, signal) => {\n // Same pipe-hostage hazard as foreground `exec`: a grandchild\n // that inherited the pipes blocks `close` forever, leaving the\n // task `running` (and any waiter parked) long after the\n // command exited. Settle after a short drain grace; `settle`'s\n // latch makes this a no-op when `close` already fired.\n const exitGraceTimer = setTimeout(() => {\n if (state.settled)\n return\n child.stdout?.destroy()\n child.stderr?.destroy()\n settle('close', code, signal)\n }, EXIT_PIPE_DRAIN_GRACE_MS)\n exitGraceTimer.unref?.()\n })\n\n return { taskId, pid: state.pid, outputPath }\n },\n\n async killBackground(handle: ExecutionHandle, taskId: string): Promise<TaskExitInfo | null> {\n const state = tasks.get(taskId)\n // Two miss cases collapse into one `null` return: unknown id, AND\n // known-id-but-not-owned-by-this-handle. The second case is the\n // subagent-can't-kill-parent-tasks defense; surfacing it as a\n // distinct error would leak the existence of the parent's task\n // to the subagent's model, which violates the per-handle\n // isolation contract.\n if (!state || state.handleId !== handle.id)\n return null\n // Already exited — return the cached info. We don't keep a\n // separate cached exit; the state itself carries every field\n // `TaskExitInfo` needs and `stateToTaskExitInfo` projects it.\n if (state.status !== 'running')\n return stateToTaskExitInfo(state)\n\n // Mark the intent BEFORE issuing the kill so the close handler\n // classifies the exit as `'killed'` even on platforms where the\n // SIGTERM-via-group lands faster than the close event drains.\n // (Checklist #14: at-most-once settle, plus correct status\n // classification regardless of event ordering.)\n state.killRequested = true\n\n // Wait for the existing close listener to fire — `settle()` does\n // all the flushing + onExit work. We just sit on a one-shot\n // promise tied to the `child.on('close')` we already registered\n // at spawn time.\n const closed = new Promise<void>((resolveP) => {\n if (state.settled) {\n resolveP()\n return\n }\n const originalOnExit = state.onExit\n state.onExit = (info) => {\n originalOnExit?.(info)\n resolveP()\n }\n })\n\n killProcessGroup(state.child, 'SIGTERM')\n await closed\n return stateToTaskExitInfo(state)\n },\n\n async waitBackground(\n handle: ExecutionHandle,\n taskId: string,\n options?: { timeoutMs?: number, signal?: AbortSignal },\n ): Promise<TaskExitInfo | null> {\n const state = tasks.get(taskId)\n // Same two-misses-collapse-to-null contract as `killBackground`:\n // unknown id AND known-but-other-handle both return `null` so a\n // subagent can't observe the parent's tasks through the wait seam.\n if (!state || state.handleId !== handle.id)\n return null\n if (state.settled)\n return stateToTaskExitInfo(state)\n\n return new Promise<TaskExitInfo | null>((resolveP) => {\n let done = false\n let timer: NodeJS.Timeout | undefined\n const signal = options?.signal\n const finish = (value: TaskExitInfo | null): void => {\n if (done)\n return\n done = true\n if (timer)\n clearTimeout(timer)\n signal?.removeEventListener('abort', onAbort)\n resolveP(value)\n }\n function onAbort(): void {\n finish(null)\n }\n\n // Chain onto the live exit callback rather than replacing it —\n // the original (notification enqueue, kill-waiter, a sibling\n // waiter) must keep firing. The `done` latch makes a late exit\n // after timeout a no-op for THIS waiter.\n const originalOnExit = state.onExit\n state.onExit = (info) => {\n originalOnExit?.(info)\n finish(stateToTaskExitInfo(state))\n }\n\n if (signal) {\n if (signal.aborted) {\n finish(null)\n return\n }\n signal.addEventListener('abort', onAbort, { once: true })\n }\n const timeoutMs = options?.timeoutMs\n if (typeof timeoutMs === 'number' && Number.isFinite(timeoutMs) && timeoutMs > 0) {\n timer = setTimeout(finish, timeoutMs, null)\n timer.unref?.()\n }\n })\n },\n\n async reassignBackgroundTasks(\n fromHandle: ExecutionHandle,\n toHandle: ExecutionHandle,\n newOnExit?: (info: TaskExitInfo) => void,\n ): Promise<readonly TaskEntry[]> {\n // No-op when source = destination — keeps the spawn.ts call site\n // unconditional without forcing it to dedupe.\n if (fromHandle.id === toHandle.id)\n return []\n const promoted: TaskEntry[] = []\n for (const state of tasks.values()) {\n if (state.handleId !== fromHandle.id || state.status !== 'running')\n continue\n state.handleId = toHandle.id\n // Replace the natural-exit callback. The original closed over\n // the spawning agent's hook bus, which is about to be destroyed\n // — without rewiring, the task's eventual `background:exit`\n // fires into a torn-down hookable and the parent never learns.\n if (newOnExit)\n state.onExit = newOnExit\n promoted.push(stateToTaskEntry(state))\n }\n return promoted\n },\n\n async listBackground(handle: ExecutionHandle): Promise<readonly TaskEntry[]> {\n // Snapshot — callers must not assume the returned array stays\n // in sync with the live registry. Sorted by startedAt so the\n // model / UI sees consistent ordering across calls. Scoped to\n // the calling handle so subagents don't see the parent's tasks\n // (and vice versa) in their listing.\n return [...tasks.values()]\n .filter(s => s.handleId === handle.id)\n .sort((a, b) => a.startedAt - b.startedAt)\n .map(stateToTaskEntry)\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n // Kill every still-running background task SPAWNED THROUGH THIS\n // HANDLE before tearing the handle down. SIGTERM the groups,\n // await the close + flush, THEN drop the registry entries.\n // Sequential — destroy is one-shot teardown, the few ms of extra\n // latency aren't worth the synchronization complexity.\n //\n // The handle scope matters when the same `ExecutionContext` is\n // shared across a parent agent and its `spawn`-ed subagents (the\n // default — `spawn.ts` passes `execution: ctx.execution`). Each\n // agent mints its own `ExecutionHandle` and registers its\n // background tasks under that handle's id. Without the filter,\n // a child agent's `destroy()` (fired by `spawn.ts`'s `finally`\n // when the subagent finishes / is cancelled) would walk the\n // shared registry and SIGTERM the parent's tasks too. So\n // cancelling a subagent that has its own background shells now\n // correctly kills JUST those subagent shells, leaving the\n // parent's intact.\n const survivors = [...tasks.values()].filter(s => s.handleId === handle.id && !s.settled)\n await Promise.all(survivors.map(async (state) => {\n state.killRequested = true\n await new Promise<void>((resolveP) => {\n let graceTimer: NodeJS.Timeout | undefined\n const originalOnExit = state.onExit\n state.onExit = (info) => {\n originalOnExit?.(info)\n if (graceTimer)\n clearTimeout(graceTimer)\n resolveP()\n }\n killProcessGroup(state.child, 'SIGTERM')\n // Bounded drain: a task that traps/ignores SIGTERM — or whose\n // `close` event is held back by an escaped grandchild (setsid'd\n // out of the process group) keeping the inherited stdio pipe\n // open — must not wedge `agent.destroy()` forever. After the\n // grace period, SIGKILL the group and stop waiting: `close`\n // may STILL never fire (the pipe holder isn't in the group),\n // so resolving here is the only way destroy() stays bounded.\n graceTimer = setTimeout(() => {\n killProcessGroup(state.child, 'SIGKILL')\n // settle() normally flushes + ends the log stream, but it\n // only runs off `close` — which we just gave up on. Tear the\n // stream down so the fd doesn't leak into a long-lived host.\n state.outputStream.destroy()\n resolveP()\n }, destroyGraceMs)\n graceTimer.unref?.()\n })\n }))\n // Drop only this handle's tasks from the registry. Other handles\n // (siblings, parent) keep their entries.\n for (const [taskId, state] of tasks) {\n if (state.handleId === handle.id)\n tasks.delete(taskId)\n }\n handles.delete(handle.id)\n // Drop the orphan reaper ONLY when no handles remain — otherwise\n // a child's `destroy()` would strip the parent's safety net. The\n // reaper protects every still-tracked task in the context, so it\n // sticks around until the LAST handle is gone.\n //\n // Without this guard, the spawn-tool sequence\n // parent.spawn(child) → child.run() → child.destroy() (auto)\n // would deregister the handler mid-parent-lifetime. The parent's\n // own subsequent Ctrl+C orphan-kill safety would silently degrade.\n if (exitHandlerRegistered && handles.size === 0) {\n process.off('exit', exitHandler)\n exitHandlerRegistered = false\n }\n },\n }\n}\n\n/**\n * Per-task state. Lives in the context's `tasks` registry. Fields are\n * mutated in place by the spawn / close / kill / destroy code paths —\n * the registry isn't immutable. Treat the type as a record-of-cells,\n * not a value.\n */\ninterface TaskState {\n taskId: string\n /**\n * `ExecutionHandle.id` of the spawning agent. The registry is\n * context-scoped (one Map shared across all handles a context minted),\n * so a per-task owner tag is what scopes `listBackground` /\n * `killBackground` / `destroy` to the calling handle's slice.\n *\n * Without this, a subagent spawned via `spawn` tool — which inherits\n * the parent's `ExecutionContext` but mints its OWN handle — would\n * see (and accidentally kill on `destroy()`) every task the parent\n * had running. Came up the first time the model spawned a subagent\n * that ran a background task: the subagent's run-end `destroy()`\n * SIGTERMed the parent's `npm run dev` mid-flight.\n */\n handleId: string\n pid: number\n command: string\n cwd: string\n startedAt: number\n outputPath: string\n outputStream: WriteStream\n child: ChildProcess\n status: 'running' | 'exited' | 'killed'\n exitCode?: number\n signal?: NodeJS.Signals\n bytesWritten: number\n /**\n * `at-most-once` settle latch. Multiple trigger sources (`close`,\n * `error`, `kill`) can race; the flag dedupes so `onExit` fires\n * exactly once per task (checklist #14).\n */\n settled: boolean\n /**\n * Set by `killBackground` / `destroy` before issuing SIGTERM so the\n * close handler classifies the exit as `'killed'` even when the\n * platform delivers the close event ahead of our intent record.\n */\n killRequested?: boolean\n /**\n * `Date.now()` at settle time. Lets `durationMs` (and pull-based\n * reconcile consumers reading `TaskEntry.endedAt`) report the task's\n * real lifetime instead of a projection-time delta.\n */\n endedAt?: number\n /**\n * Optional push-style exit callback. Undefined for pull-based\n * consumers (remote / durable hosts reconcile via `listBackground`).\n * Mutated in place by the kill / destroy / wait chains.\n */\n onExit?: (info: TaskExitInfo) => void\n}\n\n/**\n * Send `signal` to the child's whole process group. Falls back to a\n * single-process kill on Windows (no POSIX process groups). Shared\n * across the foreground `exec` path, the background spawn / kill\n * paths, and the shutdown-time orphan reaper — keeping one definition\n * so the kill semantics can't drift between them.\n */\nfunction killProcessGroup(child: ChildProcess, signal: NodeJS.Signals): void {\n const pid = child.pid\n if (pid === undefined)\n return\n try {\n if (SUPPORTS_PROCESS_GROUPS)\n process.kill(-pid, signal)\n else\n process.kill(pid, signal)\n }\n catch {\n // ESRCH / EPERM — process is already gone (race with natural exit)\n // or we lost the right to kill it. Both are safe to swallow.\n }\n}\n\n/**\n * Project a `TaskState` to the `TaskEntry` shape `listBackground` and\n * `reassignBackgroundTasks` return. Single helper keeps the shape\n * consistent across both call sites (and a future addition of fields\n * to `TaskEntry` only needs to land here).\n */\nfunction stateToTaskEntry(state: TaskState): TaskEntry {\n return {\n taskId: state.taskId,\n pid: state.pid,\n command: state.command,\n cwd: state.cwd,\n startedAt: state.startedAt,\n ...(state.endedAt !== undefined ? { endedAt: state.endedAt } : {}),\n outputPath: state.outputPath,\n status: state.status,\n ...(state.exitCode !== undefined ? { exitCode: state.exitCode } : {}),\n ...(state.signal ? { signal: state.signal } : {}),\n bytesWritten: state.bytesWritten,\n }\n}\n\n/**\n * Project a settled `TaskState` to the `TaskExitInfo` shape `settle()`\n * fires from `onExit` and `killBackground` returns on the\n * cached-exit path. Pre-condition: `state.status !== 'running'`\n * (callers gate on this).\n */\nfunction stateToTaskExitInfo(state: TaskState): TaskExitInfo {\n return {\n taskId: state.taskId,\n status: state.status as Exclude<TaskState['status'], 'running'>,\n exitCode: state.exitCode ?? 0,\n ...(state.signal ? { signal: state.signal } : {}),\n outputPath: state.outputPath,\n // Settle-time delta when available (always set since `endedAt` was\n // introduced); the `Date.now()` fallback covers defensive callers\n // projecting an unsettled state.\n durationMs: (state.endedAt ?? Date.now()) - state.startedAt,\n command: state.command,\n }\n}\n","/**\n * Execution context types.\n *\n * An execution context defines *where* and *how* an agent's tools run.\n * The agent loop and tools interact through this interface without knowing\n * whether they're running in-process, in a Docker container, or in a\n * remote sandbox.\n */\n\n// ---------------------------------------------------------------------------\n// Capabilities\n// ---------------------------------------------------------------------------\n\n/**\n * Lifetime guarantee of background tasks started through\n * {@link ExecutionContext.execBackground}.\n *\n * - `'none'` — the context cannot detach tasks at all.\n * - `'process-lifetime'` — tasks live as long as the HOST process; a\n * crash/restart of the host orphans or kills them (`ProcessContext`).\n * - `'durable'` — tasks live on a remote runner and survive host process\n * death (remote execution contexts driven by durable runtimes).\n *\n * Durable-execution adapters (e.g. `zidane/restate`) consult this to\n * decide whether backgrounding is safe to expose: a `'durable'` context\n * keeps its tasks across worker crashes, so there is no reason to strip\n * the capability from the model.\n */\nexport type DetachedTasksCapability = 'none' | 'process-lifetime' | 'durable'\n\nexport interface ContextCapabilities {\n /** Can execute shell commands */\n shell: boolean\n /** Can read/write files in a workspace */\n filesystem: boolean\n /** Can enumerate glob matches through `ExecutionContext.listFiles(..., { glob: true })`. */\n glob?: boolean\n /** Can make outbound network requests */\n network: boolean\n /** Has GPU access */\n gpu: boolean\n /**\n * Background-task lifetime guarantee. Optional for backward\n * compatibility — when absent, callers infer `'process-lifetime'`\n * if the context implements `execBackground`, `'none'` otherwise\n * (see {@link resolveDetachedTasksCapability}).\n */\n detachedTasks?: DetachedTasksCapability\n}\n\n/**\n * Effective {@link DetachedTasksCapability} of a context, with the\n * backward-compatible inference for contexts that predate the field.\n */\nexport function resolveDetachedTasksCapability(context: ExecutionContext): DetachedTasksCapability {\n return context.capabilities.detachedTasks\n ?? (context.execBackground ? 'process-lifetime' : 'none')\n}\n\n// ---------------------------------------------------------------------------\n// Execution handle\n// ---------------------------------------------------------------------------\n\n/** Opaque handle to a running execution context instance */\nexport interface ExecutionHandle {\n id: string\n type: ContextType\n /** Working directory within the context */\n cwd: string\n}\n\n// ---------------------------------------------------------------------------\n// Exec result\n// ---------------------------------------------------------------------------\n\nexport interface ExecResult {\n stdout: string\n stderr: string\n exitCode: number\n}\n\nexport interface ListFileMetadata {\n path: string\n size?: number\n mtimeMs?: number\n}\n\nexport type ListFilesEntry = string | ListFileMetadata\n\nexport interface ListFilesOptions {\n /** Treat `path` as a glob match pattern instead of a literal directory path. */\n glob?: boolean\n limit?: number\n metadata?: boolean\n /**\n * Skip git-ignored files when globbing. Default `true`. Contexts that can\n * resolve git's ignore rules (e.g. the local process context) honor it; the\n * rest treat it as a no-op. Set `false` to match every file, ignored or not.\n */\n gitignore?: boolean\n /** Abort the listing early — forwarded from the calling tool's `ctx.signal`. */\n signal?: AbortSignal\n}\n\n// ---------------------------------------------------------------------------\n// Spawn config\n// ---------------------------------------------------------------------------\n\nexport interface SpawnConfig {\n /** Working directory (created if it doesn't exist) */\n cwd?: string\n /**\n * Optional workspace-containment root for the file methods (`readFile`,\n * `writeFile`, `listFiles`) of the process context.\n *\n * When set, a resolved path that escapes this root (via absolute path,\n * `..`, or a symlink pointing outside) is rejected before any I/O. Default\n * unset = no containment (back-compat; the agent can read/write anywhere\n * the host process can). Set this for untrusted workloads to confine file\n * tools to a directory. Shell commands are not covered — use a sandbox /\n * docker context for full isolation.\n */\n workspaceRoot?: string\n /** Environment variables */\n env?: Record<string, string>\n /**\n * Whether spawned shells inherit the parent `process.env` (process context).\n *\n * Default `true` (preserves existing behavior — children see the parent\n * environment including anything Bun auto-loaded from `.env`). Set `false`\n * for untrusted workloads so secrets in the parent environment aren't\n * readable by tool commands; only `env` / per-call env plus a minimal base\n * (`PATH`, `HOME`, `SHELL`, `LANG`, `LC_ALL`, `USER`, `TERM`, `TMPDIR`) are\n * passed through.\n */\n inheritEnv?: boolean\n /** Docker image (only for 'docker' context) */\n image?: string\n /** Docker container name prefix (only for 'docker' context) */\n name?: string\n /** Host paths mounted into the context (only for 'docker' context today) */\n mounts?: ContextMount[]\n /** Resource limits */\n limits?: {\n /** Memory limit in MB */\n memory?: number\n /** CPU limit (e.g. '1.0' = 1 core) */\n cpu?: string\n /** Timeout in seconds for the entire context lifetime */\n timeout?: number\n }\n /** Sandbox provider config (only for 'sandbox' context) */\n sandbox?: {\n provider: string\n apiKey?: string\n /**\n * Attach to a pre-existing sandbox by id rather than creating one\n * (provider-specific; E2B maps it to `Sandbox.connect`). The provider\n * leaves a connected sandbox running on teardown.\n */\n sandboxId?: string\n [key: string]: unknown\n }\n\n /**\n * How long `destroy()` waits for a SIGTERM'd background task to settle\n * before escalating to SIGKILL and abandoning the wait (process context\n * only). Bounds `agent.destroy()` against tasks that trap SIGTERM or\n * whose stdio pipes are held open by escaped grandchildren.\n *\n * Default: `5000`.\n */\n destroyGraceMs?: number\n\n /**\n * Publish container ports on the host (docker context only).\n *\n * Each entry maps a container port to either an explicit host port or\n * (when `host` is omitted) a Docker-assigned random port. Retrieve the\n * actual host port at runtime via `getMappedPort(container)` on the\n * docker context.\n */\n ports?: Array<{ container: number, host?: number, proto?: 'tcp' | 'udp' }>\n\n /**\n * UID/GID the container should run as (docker context only).\n *\n * Accepts the same forms Docker's `--user` does: `uid`, `uid:gid`, or a\n * named user that exists in the image. Default is the image's default\n * user (typically root). Setting this to the host user's `uid:gid`\n * avoids the EACCES-on-cleanup problem when sharing a workspace via\n * a `shared` mount.\n */\n user?: string\n\n /**\n * User-defined Docker network to join (docker context only).\n *\n * Defaults to Docker's default bridge. Use a user network when you\n * need multiple sibling containers (e.g. agent + database + dev\n * server) to discover each other by name.\n */\n network?: string\n\n /**\n * Docker labels to attach to the container (docker context only).\n *\n * Useful for ownership tracking — callers can sweep abandoned\n * containers (e.g. from a crashed parent process) by filtering on\n * a label they own: `docker ps -aq --filter label=my-app=true`.\n */\n labels?: Record<string, string>\n\n /**\n * Container hardening options (docker context only).\n *\n * All fields are opt-in and OFF by default to preserve existing behavior\n * (containers run with the image's default user/capabilities). Enable them\n * for untrusted workloads. `dropAllCapabilities` and a non-root `user`\n * (see {@link SpawnConfig.user}) can break images that expect root or\n * specific capabilities, so they are not applied unless requested.\n */\n hardening?: ContextHardening\n}\n\n/**\n * Opt-in container hardening for the docker context. Every field defaults to\n * \"unset\" so an omitted `hardening` (or omitted field) reproduces the prior,\n * unrestricted behavior — this keeps the option purely additive.\n */\nexport interface ContextHardening {\n /**\n * Drop all Linux capabilities (`CapDrop: ['ALL']`). Strong isolation, but\n * breaks images needing capabilities (e.g. binding low ports, `ping`).\n * Default: `false`.\n */\n dropAllCapabilities?: boolean\n /**\n * Set `no-new-privileges` so processes can't gain privileges via setuid\n * binaries. Low blast radius; safe for most workloads. Default: `false`.\n */\n noNewPrivileges?: boolean\n /**\n * Mount the container root filesystem read-only. Pair with writable mounts\n * for scratch space. Default: `false`.\n */\n readonlyRootfs?: boolean\n /**\n * Max number of processes (`PidsLimit`) — caps fork-bombs. Default: unset\n * (no limit). A few hundred is usually plenty for an agent sandbox.\n */\n pidsLimit?: number\n}\n\nexport interface ContextMount {\n /** Absolute host path to mount. */\n source: string\n /** Absolute path inside the execution context. */\n target: string\n /** Mount read-only. Defaults to false for Docker's native bind behavior. */\n readonly?: boolean\n /**\n * Apply the SELinux shared label (`:z`) so the host user and the container\n * user can both read/write the mount (docker context only). No-op on\n * non-SELinux hosts. Combine with `SpawnConfig.user` to avoid root-owned\n * files leaking onto the host. Mutually exclusive with `readonly`.\n */\n shared?: boolean\n}\n\n// ---------------------------------------------------------------------------\n// Execution context interface\n// ---------------------------------------------------------------------------\n\nexport type ContextType = 'process' | 'docker' | 'sandbox'\n\nexport interface ExecutionContext {\n /** Context type identifier */\n readonly type: ContextType\n\n /** What this context supports */\n readonly capabilities: ContextCapabilities\n\n /** Spawn a new execution environment */\n spawn: (config?: SpawnConfig) => Promise<ExecutionHandle>\n\n /**\n * Execute a shell command in the context.\n *\n * `signal` propagates abort all the way down to the underlying child\n * process — the implementation is expected to wire it into whatever\n * spawn primitive it uses so the OS receives a SIGTERM (or equivalent)\n * when the caller aborts. Without it, a cancelled tool returns its\n * cancellation marker to the model but the underlying process keeps\n * running in the background, orphaning compute / locks / IO. Pass\n * `ctx.signal` from a tool body to inherit the per-call + run-level\n * abort union; pass a freshly-built one for a host-driven kill.\n *\n * Implementations are free to ignore `signal` (the contract degrades\n * gracefully — the process simply won't be killed), but the in-process\n * default DOES honor it via `child_process.exec`'s native `signal`\n * option.\n */\n /**\n * Run a shell command. `timeout` is in SECONDS (default 30) — every\n * implementation converts internally (process ×1000 to ms, docker wraps\n * with coreutils `timeout`, sandbox SDKs take seconds/ms per their API).\n * Documented here because the unit is invisible at call sites and a\n * caller assuming milliseconds would set an instant-kill timeout.\n */\n exec: (handle: ExecutionHandle, command: string, options?: { cwd?: string, env?: Record<string, string>, timeout?: number, signal?: AbortSignal }) => Promise<ExecResult>\n\n /**\n * Start a process in the background. Settles as soon as `spawn` returns\n * — does NOT wait for the child to exit. Stdout + stderr stream\n * interleaved to the file at the returned `outputPath`. The caller\n * (typically the agent) reads incremental output via the regular\n * {@link ExecutionContext.readFile} seam.\n *\n * Optional — contexts without background support (some remote sandboxes)\n * just don't implement it. The shell tool surfaces a clean\n * \"background mode is not supported in this execution context\" error\n * when this is undefined.\n *\n * `onExit` is called once when the child terminates (natural, killed,\n * or error). The same instance that called `execBackground` is the\n * exclusive owner of the callback — it's not a multi-cast bus. Hosts\n * wire this to the agent's pending-notification queue so the model\n * gets a `<task-notification>` on its next turn.\n *\n * `onExit` is OPTIONAL: remote / durable contexts may have no legal\n * way to push a callback from a timer back into the host (a Restate\n * journal, for instance, forbids out-of-band writes). Such contexts\n * simply record the exit in their registry; the agent loop reconciles\n * by polling {@link ExecutionContext.listBackground} at run\n * boundaries, where a host journal wrapper is legal (the\n * `background:reconcile` hook is the journalable seam).\n *\n * See `docs/RUN_IN_BACKGROUND.md` for the broader design contract\n * (file location, replay semantics, suppression rules).\n */\n execBackground?: (\n handle: ExecutionHandle,\n command: string,\n options: {\n cwd?: string\n env?: Record<string, string>\n /**\n * Absolute directory the context appends `<task-id>.log` to. The\n * agent owns this path because it carries session-shaped knowledge\n * (`<userDir>/<sessionId>/tasks/`) the context can't synthesize.\n * Must already exist OR be creatable by the context — the\n * implementation handles `mkdir -p` defensively.\n */\n outputDir: string\n /** Push-style exit callback. Optional — see the method doc. */\n onExit?: (info: TaskExitInfo) => void\n /**\n * Cap on bytes written to the output file. Beyond the cap the\n * process KEEPS RUNNING; further output is counted but dropped,\n * and a `<output-truncated bytes-dropped=\"N\"/>` marker is\n * appended when the task settles. Unset / non-positive = no cap.\n */\n maxOutputBytes?: number\n /**\n * Stall watchdog: when the task produces no output for this many\n * milliseconds, `onStall` fires ONCE (one-shot — re-arms only\n * after fresh output arrives). The process is NOT killed; the\n * consumer decides (typically by telling the model the task may\n * be stuck at an interactive prompt). Unset = no watchdog.\n */\n stallTimeoutMs?: number\n /** One-shot stall callback — see `stallTimeoutMs`. */\n onStall?: (info: TaskStallInfo) => void\n },\n ) => Promise<TaskHandle>\n\n /**\n * SIGTERM the whole process group of a running background task.\n * Idempotent — second call returns `null` (or the cached exit info).\n * Resolves once the process has exited AND its output stream has\n * been flushed + closed.\n *\n * `null` return on miss (unknown id, already cleaned up) so the\n * shell_kill tool can surface a clean \"no such task\" message\n * without throwing.\n */\n killBackground?: (\n handle: ExecutionHandle,\n taskId: string,\n ) => Promise<TaskExitInfo | null>\n\n /**\n * Snapshot of every task in the context's registry — running AND\n * terminated (entries remain until the next context destroy, so\n * the model can still read output of exited tasks).\n */\n listBackground?: (\n handle: ExecutionHandle,\n ) => Promise<readonly TaskEntry[]>\n\n /**\n * Block until a background task terminates, then resolve with its\n * exit info. Resolves immediately for already-terminated tasks.\n *\n * Returns `null` when:\n * - the task id is unknown (or owned by another handle — same\n * isolation contract as `killBackground`), OR\n * - `timeoutMs` elapsed / `signal` aborted before the task exited.\n *\n * Callers that need to distinguish \"unknown\" from \"still running\"\n * should consult `listBackground` first (the `wait_task` tool does).\n *\n * This is the injectable wait seam for the `wait_task` tool:\n * in-process contexts implement it on the existing `onExit`\n * machinery; durable hosts implement it as an awakeable park\n * (runner fires task-exit over their bridge → resolveAwakeable).\n */\n waitBackground?: (\n handle: ExecutionHandle,\n taskId: string,\n options?: { timeoutMs?: number, signal?: AbortSignal },\n ) => Promise<TaskExitInfo | null>\n\n /**\n * Transfer ownership of every still-running task from `fromHandle`\n * to `toHandle`. Used by the spawn tool to \"promote\" a subagent's\n * background tasks up to the parent's handle so they outlive the\n * subagent's destroy() — matching shell semantics, where a `&`-ed\n * command outlives the parent process.\n *\n * Side effects:\n * - The task's `handleId` is rewritten, so subsequent\n * `listBackground(toHandle)` / `killBackground(toHandle, …)` /\n * `destroy(toHandle)` see it (and `fromHandle`-scoped operations\n * don't).\n * - When `newOnExit` is provided, the original `onExit` is\n * REPLACED with it. Critical: the task's natural exit callback\n * was captured against the spawning agent's hook bus, which is\n * about to be destroyed; without rewiring, the parent never\n * learns when the task terminates. Pass a closure that fires\n * the parent agent's `background:exit` hook.\n *\n * Returns the entries that were actually reassigned (running tasks\n * only; terminated ones stay where they are). Implementations that\n * don't support reassignment can leave this undefined; the caller\n * (spawn.ts) falls back to the current behavior of killing child\n * tasks at subagent shutdown.\n */\n reassignBackgroundTasks?: (\n fromHandle: ExecutionHandle,\n toHandle: ExecutionHandle,\n newOnExit?: (info: TaskExitInfo) => void,\n ) => Promise<readonly TaskEntry[]>\n\n /** Read a file from the context's filesystem */\n readFile: (handle: ExecutionHandle, path: string) => Promise<string>\n\n /**\n * Read a file from the context's filesystem as raw bytes.\n *\n * Used by `read_file` to dispatch image / binary files into the multimodal\n * `ToolResultContent[]` route. Optional — when not implemented, the tool\n * falls back to `base64 < path` via the `exec` seam, which works in any\n * shell-capable context. Implementations that already have a native\n * binary read (in-process `fs.readFile` without encoding, container API,\n * sandbox SDK) should override for the latency win.\n */\n readFileBinary?: (handle: ExecutionHandle, path: string) => Promise<Uint8Array>\n\n /**\n * Cheap size probe for a file in the context's filesystem, in bytes.\n * `null` = unknowable (missing file, not a regular file, probe failed,\n * or containment rejected the path — a probe must not leak even the\n * size of files outside the workspace root).\n *\n * Exists because `readFile` has no partial-read form: tools that must\n * materialize a whole file (read_file's text path, edit/multi_edit,\n * grep's no-rg fallback) consult this first to refuse multi-GB targets\n * instead of OOMing the HOST — the read lands in host memory no matter\n * where the file lives. Per-context posture:\n *\n * - `process` — implements it (local stat, µs).\n * - `docker` — implements it via `wc -c` through its exec; its\n * `readFile` gets transport headroom above the tool ceiling so\n * the probe (tool policy), not the 10MB exec cap, decides read\n * limits — same experience as every other context.\n * - `sandbox` (and the e2b / daytona providers behind it) —\n * implements it via `wc -c` through the provider's exec: one extra\n * round-trip per whole-read tool call, paid so a multi-GB sandbox\n * file is refused BEFORE the SDK transfers it into host memory.\n *\n * KNOWN RESIDUAL (sandbox providers): this guards the `readFile` path\n * only. Their `exec` output is buffered inside the provider SDKs\n * (e2b `commands.run`, daytona `executeCommand`), which we cannot cap\n * from this side — a command that streams GBs to stdout within its\n * timeout window can still balloon host memory. Docker doesn't share\n * the residual (we own its transport; see `output-cap.ts`). A shell\n * wrapper (`| head -c`) was considered and rejected: preserving exit\n * codes, stderr separation, and timeout semantics through a pipe for\n * EVERY command is fragile, and the timeout already time-bounds the\n * exposure.\n */\n fileSize?: (handle: ExecutionHandle, path: string) => Promise<number | null>\n\n /** Write a file to the context's filesystem */\n writeFile: (handle: ExecutionHandle, path: string, content: string) => Promise<void>\n\n /**\n * List files in the context filesystem.\n *\n * Plain paths list immediate directory entries (legacy behavior). Paths that\n * contain glob metacharacters (`*`, `?`, `[]`, `{}`) request matched file\n * enumeration inside the target context. Contexts that cannot support\n * globbing should throw a clear error rather than silently returning no\n * matches. `metadata` is best-effort and only required when the context can\n * provide it without expensive per-file shell calls.\n */\n listFiles: (handle: ExecutionHandle, path: string, options?: ListFilesOptions) => Promise<ListFilesEntry[]>\n\n /**\n * Resolve the host-side port that a container port was published on\n * (docker context only, and only when the matching entry was created\n * with `host` omitted in `SpawnConfig.ports`).\n *\n * Resolves to `null` if the container port isn't published. Other\n * contexts (process, sandbox) don't implement this; they don't have\n * a port-mapping concept.\n */\n getMappedPort?: (handle: ExecutionHandle, containerPort: number) => Promise<number | null>\n\n /** Destroy the execution environment and clean up resources */\n destroy: (handle: ExecutionHandle) => Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Background task types\n// ---------------------------------------------------------------------------\n\n/**\n * Lifecycle status of a background task.\n *\n * - `'running'` — process is still live; `exitCode` / `signal` unset.\n * - `'exited'` — process terminated on its own (clean or non-zero exit).\n * - `'killed'` — the host issued `killBackground` (SIGTERM to the group).\n *\n * The status is a coarse-grained signal; the exit code carries the\n * fine-grained detail (e.g. `143 = SIGTERM` for `'killed'`).\n */\nexport type BackgroundTaskStatus = 'running' | 'exited' | 'killed'\n\n/**\n * Returned synchronously by `execBackground` — the handle the model and\n * the framework use to refer to the task until it terminates.\n *\n * `outputPath` is an absolute path to the log file the context is\n * appending stdout + stderr to (interleaved by emit order). The model\n * reads it via the normal `read_file` tool; no special tool is required.\n */\nexport interface TaskHandle {\n /**\n * Stable id minted by the context — typically `bash_<n>` for\n * `ProcessContext`. Sequential within a single context instance,\n * resets when a new context is constructed. Forwarded to `killBackground` /\n * `listBackground` and stamped into every `<task-notification>` block.\n */\n taskId: string\n /** OS pid of the spawned shell wrapper (process-group leader on POSIX). */\n pid: number\n /** Absolute path to the log file the context is streaming output into. */\n outputPath: string\n}\n\n/**\n * Fired exactly once per task when the child process terminates, via\n * `execBackground`'s `onExit` callback. The agent layer translates this\n * into a queued `<task-notification>` for the next turn.\n *\n * `signal` is set when the child was terminated by a signal (e.g.\n * SIGTERM from our own kill-tree, SIGKILL from oom-killer); absent on\n * natural exit. `exitCode` is `128 + signal-number` on signal-killed\n * children, matching POSIX shell conventions — `143` for SIGTERM, etc.\n */\nexport interface TaskExitInfo {\n taskId: string\n status: Exclude<BackgroundTaskStatus, 'running'>\n exitCode: number\n signal?: NodeJS.Signals\n outputPath: string\n /** `Date.now()` delta between spawn and exit. */\n durationMs: number\n /** The original command string the model invoked — useful for telemetry / banner summary. */\n command: string\n}\n\n/**\n * One row in `listBackground`'s snapshot. Living entries (status `'running'`)\n * have `exitCode` / `signal` / `endedAt` unset; terminated entries carry the\n * same data `TaskExitInfo` returned at exit time.\n */\nexport interface TaskEntry {\n taskId: string\n pid: number\n command: string\n cwd: string\n startedAt: number\n /**\n * Set when the task terminated. Lets pull-based consumers (the agent's\n * run-boundary reconcile) derive `durationMs = endedAt - startedAt`\n * without depending on when the snapshot was taken.\n */\n endedAt?: number\n outputPath: string\n status: BackgroundTaskStatus\n exitCode?: number\n signal?: NodeJS.Signals\n /** Total bytes written to the output file so far — useful for \"task X has produced N KB\" UX hints. */\n bytesWritten: number\n}\n\n/**\n * Fired by the optional stall watchdog (see `execBackground`'s\n * `stallTimeoutMs`) when a running task has produced no output for the\n * configured window. The process is still alive — this is a signal, not\n * a state transition.\n */\nexport interface TaskStallInfo {\n taskId: string\n command: string\n outputPath: string\n /** Milliseconds since the last output chunk (>= the configured window). */\n stalledForMs: number\n /** Total bytes the task has written so far. */\n bytesWritten: number\n}\n"],"mappings":"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"}
package/dist/contexts.js CHANGED
@@ -1,3 +1,3 @@
1
- import { n as createProcessContext, t as resolveDetachedTasksCapability } from "./contexts-Vone3qQQ.js";
1
+ import { n as createProcessContext, t as resolveDetachedTasksCapability } from "./contexts-BIbcfBFH.js";
2
2
  import { createSandboxContext } from "./contexts/sandbox.js";
3
3
  export { createProcessContext, createSandboxContext, resolveDetachedTasksCapability };