zidane 6.2.36 → 6.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +16 -5
  2. package/dist/{acp-DZNCQaTQ.js → acp-Cs86McDR.js} +6 -6
  3. package/dist/{acp-DZNCQaTQ.js.map → acp-Cs86McDR.js.map} +1 -1
  4. package/dist/acp-cli.js +7 -7
  5. package/dist/acp.d.ts +2 -2
  6. package/dist/acp.js +1 -1
  7. package/dist/{agent-CrPwqpLI.js → agent-AZ_crMfB.js} +168 -23
  8. package/dist/agent-AZ_crMfB.js.map +1 -0
  9. package/dist/agent.d.ts +2 -2
  10. package/dist/agent.js +4 -4
  11. package/dist/{anthropic-asnZVFS_.js → anthropic-CWwySM3p.js} +59 -24
  12. package/dist/anthropic-CWwySM3p.js.map +1 -0
  13. package/dist/{auth-C5YTniaq.js → auth-BPSnotuN.js} +5 -5
  14. package/dist/auth-BPSnotuN.js.map +1 -0
  15. package/dist/{cache-telemetry-BhTryxFT.js → cache-telemetry-DzyfqSds.js} +87 -5
  16. package/dist/cache-telemetry-DzyfqSds.js.map +1 -0
  17. package/dist/chat/pure.d.ts +3 -3
  18. package/dist/chat.d.ts +43 -5
  19. package/dist/chat.d.ts.map +1 -1
  20. package/dist/chat.js +31 -3
  21. package/dist/chat.js.map +1 -1
  22. package/dist/context-transport-64EeXTAQ.js +84 -0
  23. package/dist/context-transport-64EeXTAQ.js.map +1 -0
  24. package/dist/contexts/daytona.d.ts +33 -4
  25. package/dist/contexts/daytona.d.ts.map +1 -1
  26. package/dist/contexts/daytona.js +136 -0
  27. package/dist/contexts/daytona.js.map +1 -1
  28. package/dist/contexts/docker.d.ts +1 -1
  29. package/dist/contexts/docker.d.ts.map +1 -1
  30. package/dist/contexts/docker.js +85 -2
  31. package/dist/contexts/docker.js.map +1 -1
  32. package/dist/contexts/e2b.d.ts +34 -4
  33. package/dist/contexts/e2b.d.ts.map +1 -1
  34. package/dist/contexts/e2b.js +92 -0
  35. package/dist/contexts/e2b.js.map +1 -1
  36. package/dist/contexts/sandbox.d.ts +1 -1
  37. package/dist/contexts/sandbox.js +4 -2
  38. package/dist/contexts/sandbox.js.map +1 -1
  39. package/dist/{contexts-o8y5B3Tm.js → contexts-DbMuUPjS.js} +92 -5
  40. package/dist/contexts-DbMuUPjS.js.map +1 -0
  41. package/dist/contexts.d.ts +4 -4
  42. package/dist/contexts.js +1 -1
  43. package/dist/eval.d.ts +1 -1
  44. package/dist/eval.js +3 -3
  45. package/dist/{exec-file-size-PXkr-MGA.js → exec-file-size-BRpoB6Io.js} +3 -32
  46. package/dist/exec-file-size-BRpoB6Io.js.map +1 -0
  47. package/dist/extensions.d.ts +1 -1
  48. package/dist/extensions.js +1 -1
  49. package/dist/headless.d.ts +1 -1
  50. package/dist/headless.js +4 -4
  51. package/dist/{index-DsPQbyUn.d.ts → index-B9jSo84a.d.ts} +2 -2
  52. package/dist/index-B9jSo84a.d.ts.map +1 -0
  53. package/dist/{index-CohO0oaX.d.ts → index-D-PvuMCb.d.ts} +5607 -5188
  54. package/dist/index-D-PvuMCb.d.ts.map +1 -0
  55. package/dist/index.d.ts +6 -6
  56. package/dist/index.js +13 -13
  57. package/dist/{login-CWwu9Sex.js → login-fgr9zD6z.js} +2 -2
  58. package/dist/{login-CWwu9Sex.js.map → login-fgr9zD6z.js.map} +1 -1
  59. package/dist/{mcp-Ct5tF4oj.js → mcp-DolMi7WV.js} +77 -34
  60. package/dist/mcp-DolMi7WV.js.map +1 -0
  61. package/dist/mcp.d.ts +2 -2
  62. package/dist/mcp.js +2 -2
  63. package/dist/{messages-Cpb4g_KV.js → messages-CLkUdPeG.js} +2 -2
  64. package/dist/{messages-Cpb4g_KV.js.map → messages-CLkUdPeG.js.map} +1 -1
  65. package/dist/{oauth-CBi9eymX.js → oauth-CuFPhsHY.js} +117 -11
  66. package/dist/oauth-CuFPhsHY.js.map +1 -0
  67. package/dist/{openai-compat-C77gOaHh.js → openai-compat-DHlBIjj9.js} +186 -42
  68. package/dist/openai-compat-DHlBIjj9.js.map +1 -0
  69. package/dist/output/stream-json.d.ts +1 -1
  70. package/dist/output/terminal.d.ts +1 -1
  71. package/dist/presets.d.ts +1 -1
  72. package/dist/presets.js +1 -1
  73. package/dist/prompt.d.ts +1 -1
  74. package/dist/prompt.js +14 -0
  75. package/dist/prompt.js.map +1 -1
  76. package/dist/providers/anthropic.d.ts +2 -2
  77. package/dist/providers/anthropic.js +2 -2
  78. package/dist/providers/arcee.d.ts +1 -1
  79. package/dist/providers/arcee.js +1 -1
  80. package/dist/providers/baseten.d.ts +1 -1
  81. package/dist/providers/baseten.js +1 -1
  82. package/dist/providers/cerebras.d.ts +1 -1
  83. package/dist/providers/cerebras.js +1 -1
  84. package/dist/providers/local.d.ts +1 -1
  85. package/dist/providers/local.js +1 -1
  86. package/dist/providers/openai-compat.d.ts +1 -1
  87. package/dist/providers/openai-compat.js +1 -1
  88. package/dist/providers/openai.d.ts +1 -1
  89. package/dist/providers/openai.js +5 -5
  90. package/dist/providers/openai.js.map +1 -1
  91. package/dist/providers/openrouter.d.ts +1 -1
  92. package/dist/providers/openrouter.js +1 -1
  93. package/dist/providers/xai.d.ts +1 -1
  94. package/dist/providers/xai.js +1 -1
  95. package/dist/{providers-DnHPW-dL.js → providers-Nu5ysnXN.js} +4 -4
  96. package/dist/providers-Nu5ysnXN.js.map +1 -0
  97. package/dist/providers.d.ts +2 -2
  98. package/dist/providers.js +5 -5
  99. package/dist/{read-state-rDmfeLmz.js → read-state-CNyeMG7W.js} +7 -4
  100. package/dist/read-state-CNyeMG7W.js.map +1 -0
  101. package/dist/restate.d.ts +2 -2
  102. package/dist/restate.js +2 -2
  103. package/dist/restate.js.map +1 -1
  104. package/dist/{sandbox-CgjG2VvQ.d.ts → sandbox-DqwpKX18.d.ts} +14 -2
  105. package/dist/{sandbox-CgjG2VvQ.d.ts.map → sandbox-DqwpKX18.d.ts.map} +1 -1
  106. package/dist/session/sqlite.d.ts +1 -1
  107. package/dist/{session-DpDBnktP.js → session-vmy9jTVM.js} +2 -2
  108. package/dist/{session-DpDBnktP.js.map → session-vmy9jTVM.js.map} +1 -1
  109. package/dist/session.d.ts +1 -1
  110. package/dist/session.js +2 -2
  111. package/dist/shell-quote-BmnhZmdM.js +33 -0
  112. package/dist/shell-quote-BmnhZmdM.js.map +1 -0
  113. package/dist/skills.d.ts +1 -1
  114. package/dist/{stdio-loader-Ce68wUmM.js → stdio-D0v-W5w7.js} +8 -215
  115. package/dist/stdio-D0v-W5w7.js.map +1 -0
  116. package/dist/stdio-loader-BPUedHxT.js +217 -0
  117. package/dist/stdio-loader-BPUedHxT.js.map +1 -0
  118. package/dist/stream-queue-Cd9OJxl7.js +71 -0
  119. package/dist/stream-queue-Cd9OJxl7.js.map +1 -0
  120. package/dist/{system-prompt-BlsdjcUi.d.ts → system-prompt-MNVHatSD.d.ts} +15 -1
  121. package/dist/system-prompt-MNVHatSD.d.ts.map +1 -0
  122. package/dist/{tool-formatters-bZYz74qU.d.ts → tool-formatters-BPKA2if5.d.ts} +2 -2
  123. package/dist/{tool-formatters-bZYz74qU.d.ts.map → tool-formatters-BPKA2if5.d.ts.map} +1 -1
  124. package/dist/tools/fetch-url.d.ts +1 -1
  125. package/dist/tools/web-search.d.ts +1 -1
  126. package/dist/tools.d.ts +1 -1
  127. package/dist/tools.js +2 -2
  128. package/dist/{transcript-anchors-ZZYwCTjo.js → transcript-anchors-6-uB2Xvl.js} +19 -8
  129. package/dist/transcript-anchors-6-uB2Xvl.js.map +1 -0
  130. package/dist/{transcript-anchors-EBchHUMo.d.ts → transcript-anchors-DzuIcJpm.d.ts} +2 -2
  131. package/dist/{transcript-anchors-EBchHUMo.d.ts.map → transcript-anchors-DzuIcJpm.d.ts.map} +1 -1
  132. package/dist/tui.d.ts +3 -3
  133. package/dist/tui.d.ts.map +1 -1
  134. package/dist/tui.js +124 -45
  135. package/dist/tui.js.map +1 -1
  136. package/dist/{turn-operations-DygcPreU.d.ts → turn-operations-Dzr3QhDD.d.ts} +3 -3
  137. package/dist/{turn-operations-DygcPreU.d.ts.map → turn-operations-Dzr3QhDD.d.ts.map} +1 -1
  138. package/dist/types-BIarq1qE.js.map +1 -1
  139. package/dist/{types-BDccavwj.d.ts → types-DdZMQxfT.d.ts} +117 -2
  140. package/dist/types-DdZMQxfT.d.ts.map +1 -0
  141. package/dist/types.d.ts +3 -3
  142. package/dist/{xai-B2hIs5We.js → xai-Cn0LsHRt.js} +3 -3
  143. package/dist/xai-Cn0LsHRt.js.map +1 -0
  144. package/docs/ARCHITECTURE.md +62 -4
  145. package/docs/CHAT.md +12 -5
  146. package/docs/EXTENSIONS.md +1 -1
  147. package/docs/RESTATE.md +1 -1
  148. package/docs/SKILL.md +21 -1
  149. package/docs/TUI.md +2 -0
  150. package/package.json +2 -2
  151. package/dist/agent-CrPwqpLI.js.map +0 -1
  152. package/dist/anthropic-asnZVFS_.js.map +0 -1
  153. package/dist/auth-C5YTniaq.js.map +0 -1
  154. package/dist/cache-telemetry-BhTryxFT.js.map +0 -1
  155. package/dist/contexts-o8y5B3Tm.js.map +0 -1
  156. package/dist/exec-file-size-PXkr-MGA.js.map +0 -1
  157. package/dist/index-CohO0oaX.d.ts.map +0 -1
  158. package/dist/index-DsPQbyUn.d.ts.map +0 -1
  159. package/dist/mcp-Ct5tF4oj.js.map +0 -1
  160. package/dist/oauth-CBi9eymX.js.map +0 -1
  161. package/dist/openai-compat-C77gOaHh.js.map +0 -1
  162. package/dist/providers-DnHPW-dL.js.map +0 -1
  163. package/dist/read-state-rDmfeLmz.js.map +0 -1
  164. package/dist/stdio-loader-Ce68wUmM.js.map +0 -1
  165. package/dist/system-prompt-BlsdjcUi.d.ts.map +0 -1
  166. package/dist/transcript-anchors-ZZYwCTjo.js.map +0 -1
  167. package/dist/types-BDccavwj.d.ts.map +0 -1
  168. package/dist/xai-B2hIs5We.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { r as capBufferedExecOutput } from "../output-cap-C9zukcCj.js";
2
2
  import { r as createLogger, t as consoleSink } from "../logger-DBX9uYOw.js";
3
+ import { t as StreamQueue } from "../stream-queue-Cd9OJxl7.js";
3
4
  import { Buffer } from "node:buffer";
4
5
  //#region src/contexts/daytona.ts
5
6
  /** Per-probe timeout for the readiness `true` command. */
@@ -262,6 +263,141 @@ function createDaytonaProvider(options = {}) {
262
263
  return capExecResult(normalizeDaytonaExecError(err));
263
264
  }
264
265
  },
266
+ /**
267
+ * Live bidirectional stream — see `ExecutionContext.execStream`.
268
+ *
269
+ * PTY-backed, and not by preference: Daytona's process + session API
270
+ * exposes no stdin at all (sessions are fire-and-forget with log
271
+ * streaming), so a pseudo-terminal is the only interactive channel the
272
+ * platform offers. That forces three mitigations, all in the launch
273
+ * wrapper below, because a PTY is a terminal and a protocol stream is
274
+ * not:
275
+ *
276
+ * 1. `stty -echo` — a terminal echoes everything written to stdin back
277
+ * onto stdout. Without this the caller reads its own requests back
278
+ * as if they were responses.
279
+ * 2. `stty raw` — canonical mode rewrites `\n`→`\r\n` and caps a line
280
+ * at ~4096 bytes. Protocol frames routinely exceed that and would
281
+ * be silently truncated mid-message.
282
+ * 3. `2><file>` — a PTY has ONE stream, so the child's stderr would
283
+ * otherwise interleave into the protocol data and corrupt framing.
284
+ * It is redirected to a file inside the sandbox and surfaced on the
285
+ * returned handle by polling that file through the exec API.
286
+ *
287
+ * The mitigations must run before the child's first byte — but the
288
+ * launch line ITSELF is typed into a canonical-mode terminal, so it is
289
+ * echoed once (a short non-JSON line the transport's tolerant framing
290
+ * discards) and subject to the ~4096-byte line cap. That is why the
291
+ * real command lives in an uploaded script and the typed line stays
292
+ * short and constant: a long command line would be truncated before
293
+ * `stty` ever ran. The script also restores `sh` compound-command
294
+ * semantics that a bare `exec <command>` would break.
295
+ */
296
+ async execStream(sandboxId, command, opts) {
297
+ const { sandbox, env } = get(sandboxId);
298
+ if (!sandbox.process.createPty) throw new Error("Daytona SDK is too old: `process.createPty` is required for execStream (used to run stdio MCP servers inside the sandbox).");
299
+ opts?.signal?.throwIfAborted();
300
+ const token = Math.random().toString(36).slice(2, 10);
301
+ const stderrPath = `/tmp/.zidane-stream-${token}.err`;
302
+ const scriptPath = `/tmp/.zidane-stream-${token}.sh`;
303
+ await sandbox.fs.uploadFile(Buffer.from(`exec 2>${stderrPath}\n${command}\n`), scriptPath);
304
+ const warnBacklog = (which) => (bytes) => logger.warn("execStream consumer is not draining output", {
305
+ sandboxId,
306
+ stream: which,
307
+ bufferedBytes: bytes
308
+ });
309
+ const stdout = new StreamQueue(warnBacklog("stdout"));
310
+ const stderr = new StreamQueue(warnBacklog("stderr"));
311
+ const sentinel = `Z-${token}-READY`;
312
+ let gateOpen = false;
313
+ let gateBuffer = "";
314
+ let openGate = () => {};
315
+ const gateReady = new Promise((r) => {
316
+ openGate = r;
317
+ });
318
+ const pty = await sandbox.process.createPty({
319
+ id: `zidane-stream-${token}`,
320
+ ...opts?.cwd ? { cwd: opts.cwd } : {},
321
+ envs: resolveDaytonaEnv(env, opts?.env) ?? {},
322
+ onData: (data) => {
323
+ if (gateOpen) {
324
+ stdout.push(data);
325
+ return;
326
+ }
327
+ gateBuffer += Buffer.from(data).toString("utf8");
328
+ const at = gateBuffer.indexOf(sentinel);
329
+ if (at === -1) {
330
+ if (gateBuffer.length > 64 * 1024) gateBuffer = gateBuffer.slice(-1024);
331
+ return;
332
+ }
333
+ gateOpen = true;
334
+ openGate();
335
+ const rest = gateBuffer.slice(at + sentinel.length).replace(/^\r?\n/, "");
336
+ gateBuffer = "";
337
+ if (rest.length > 0) stdout.push(new TextEncoder().encode(rest));
338
+ }
339
+ });
340
+ await pty.waitForConnection();
341
+ await pty.sendInput(`stty raw -echo; printf 'Z-%s-READY\\n' ${token}; exec sh ${scriptPath}\n`);
342
+ let gateTimer;
343
+ await Promise.race([gateReady, new Promise((r) => {
344
+ gateTimer = setTimeout(r, 3e4);
345
+ })]);
346
+ clearTimeout(gateTimer);
347
+ if (!gateOpen) {
348
+ await (pty.kill?.() ?? Promise.resolve()).catch(() => {});
349
+ await pty.disconnect().catch(() => {});
350
+ await sandbox.process.executeCommand(`rm -f ${scriptPath} ${stderrPath}`).catch(() => {});
351
+ throw new Error(`Daytona execStream: PTY did not reach the ready sentinel within 30s (sandbox ${sandboxId}). The login shell may not have started.`);
352
+ }
353
+ let stderrOffset = 0;
354
+ let torndown = false;
355
+ const eofMark = `ZEOF-${token}:`;
356
+ const drainStderr = async () => {
357
+ try {
358
+ const res = normalizeDaytonaExecResult(await sandbox.process.executeCommand(`tail -c +${stderrOffset + 1} ${stderrPath} 2>/dev/null; printf '${eofMark}%s' "$(wc -c < ${stderrPath} 2>/dev/null || echo 0)"`));
359
+ const at = res.stdout.lastIndexOf(eofMark);
360
+ if (at === -1) return;
361
+ const content = res.stdout.slice(0, at);
362
+ const size = Number.parseInt(res.stdout.slice(at + eofMark.length).trim(), 10);
363
+ if (content.length > 0) stderr.push(new TextEncoder().encode(content));
364
+ if (Number.isFinite(size) && size > stderrOffset) stderrOffset = size;
365
+ } catch {}
366
+ };
367
+ const stderrTimer = setInterval(() => {
368
+ drainStderr();
369
+ }, 2e3);
370
+ stderrTimer.unref?.();
371
+ const teardown = async () => {
372
+ if (torndown) return;
373
+ torndown = true;
374
+ clearInterval(stderrTimer);
375
+ await drainStderr();
376
+ await sandbox.process.executeCommand(`rm -f ${scriptPath} ${stderrPath}`).catch(() => {});
377
+ stdout.close();
378
+ stderr.close();
379
+ };
380
+ const exited = pty.wait().then((result) => result?.exitCode ?? 0).catch(() => 137).finally(teardown);
381
+ const kill = async () => {
382
+ await (pty.kill?.() ?? Promise.resolve()).catch(() => {});
383
+ await pty.disconnect().catch(() => {});
384
+ await teardown();
385
+ };
386
+ if (opts?.signal) if (opts.signal.aborted) kill();
387
+ else opts.signal.addEventListener("abort", () => {
388
+ kill();
389
+ }, { once: true });
390
+ return {
391
+ pid: pty.pid ?? -1,
392
+ stdout,
393
+ stderr,
394
+ write: async (data) => {
395
+ await pty.sendInput(data);
396
+ },
397
+ kill,
398
+ exited
399
+ };
400
+ },
265
401
  async readFile(sandboxId, path) {
266
402
  return bufferToString(await get(sandboxId).sandbox.fs.downloadFile(path));
267
403
  },
@@ -1 +1 @@
1
- {"version":3,"file":"daytona.js","names":[],"sources":["../../src/contexts/daytona.ts"],"sourcesContent":["/**\n * Daytona sandbox provider.\n *\n * Implements {@link SandboxProvider} on top of Daytona sandboxes. Pair it with\n * {@link createSandboxContext} to run an agent's shell + filesystem tools\n * inside a Daytona sandbox:\n *\n * ```ts\n * import { createSandboxContext } from 'zidane/contexts'\n * import { createDaytonaProvider } from 'zidane/contexts/daytona'\n *\n * const execution = createSandboxContext(createDaytonaProvider())\n * ```\n *\n * Requires `@daytona/sdk` as an optional peer dependency:\n * `bun add @daytona/sdk`.\n */\n\nimport type { Logger } from '../logger'\nimport type { SandboxProvider } from './sandbox'\nimport type { ExecResult, SpawnConfig } from './types'\nimport { Buffer } from 'node:buffer'\nimport { consoleSink, createLogger } from '../logger'\nimport { capBufferedExecOutput } from './output-cap'\n\n// ---------------------------------------------------------------------------\n// Minimal structural views of the `@daytona/sdk` surface we touch. Declared\n// locally so zidane typechecks without the optional peer dependency installed.\n// ---------------------------------------------------------------------------\n\ninterface DaytonaExecuteResponse {\n exitCode?: number | null\n result?: string | null\n output?: string | null\n stdout?: string | null\n stderr?: string | null\n artifacts?: { stdout?: string | null }\n}\n\ninterface DaytonaFileEntry {\n name: string\n /** Present in the Daytona SDK's FileInfo; used for the `/` dir suffix. */\n isDir?: boolean\n}\n\ninterface DaytonaSandbox {\n id: string\n state?: string\n process: {\n executeCommand: (command: string, cwd?: string, env?: Record<string, string>, timeout?: number) => Promise<DaytonaExecuteResponse>\n }\n fs: {\n downloadFile: (path: string) => Promise<Uint8Array | ArrayBuffer | { toString: (encoding?: BufferEncoding) => string }>\n uploadFile: (content: Uint8Array | Buffer, path: string) => Promise<unknown>\n listFiles: (path: string) => Promise<DaytonaFileEntry[]>\n }\n getWorkDir?: () => Promise<string>\n start?: (timeout?: number) => Promise<unknown>\n delete: (timeout?: number) => Promise<unknown>\n stop?: (timeout?: number) => Promise<unknown>\n archive?: () => Promise<unknown>\n}\n\ninterface DaytonaClientConfig {\n apiKey?: string\n apiUrl?: string\n target?: string\n}\n\ninterface DaytonaResources {\n cpu?: number\n memory?: number\n disk?: number\n gpu?: number\n [key: string]: unknown\n}\n\ninterface DaytonaCreateParams {\n language?: string\n snapshot?: string\n image?: unknown\n resources?: DaytonaResources\n envVars?: Record<string, string>\n [key: string]: unknown\n}\n\ninterface DaytonaClient {\n create: (params?: DaytonaCreateParams) => Promise<DaytonaSandbox>\n get: (sandboxIdOrName: string) => Promise<DaytonaSandbox>\n}\n\ntype DaytonaConstructor = new (config?: DaytonaClientConfig) => DaytonaClient\n\n// ---------------------------------------------------------------------------\n// Provider options\n// ---------------------------------------------------------------------------\n\nexport type DaytonaOnDestroy = 'delete' | 'kill' | 'stop' | 'pause' | 'archive' | 'leave'\n\nexport interface DaytonaProviderOptions {\n /** Daytona API key. Falls back to the `DAYTONA_API_KEY` env var when omitted. */\n apiKey?: string\n /** Daytona API URL. Falls back to `DAYTONA_API_URL`, then Daytona's hosted default. */\n apiUrl?: string\n /** Daytona target / region. Falls back to `DAYTONA_TARGET`, then the org default. */\n target?: string\n /**\n * Attach to a pre-existing sandbox by id or name instead of creating a fresh\n * one. By default the provider treats attached sandboxes as externally owned\n * and leaves them running on destroy/failure (set {@link ownAttached} to take\n * over the lifecycle).\n */\n sandboxId?: string\n /**\n * Take ownership of an attached ({@link sandboxId}) sandbox's lifecycle, so\n * `destroy()` applies {@link onDestroy} to it (stop / archive / delete)\n * instead of leaving it running.\n *\n * The intended caller is a durable host that *owns* the sandbox across\n * invocations: mint it once, journal its id, re-attach via `sandboxId` on\n * every replay/resume, and clean it up at the end of the logical run. Without\n * this an attached sandbox can never be stopped/deleted through the provider —\n * a leak for host-owned sandboxes. No effect when creating a fresh sandbox\n * (those are always owned). Spawn-time failures (readiness timeout, pregame\n * error) still never delete a pre-existing attached sandbox — only sandboxes\n * this provider *created* are auto-cleaned on spawn failure.\n *\n * Default: `false`.\n */\n ownAttached?: boolean\n /** Daytona snapshot used when creating a fresh sandbox. */\n snapshot?: string\n /** Runtime used for Daytona's code tooling. Shell execution works either way. */\n language?: string\n /** Daytona image/declarative builder input used when creating a fresh sandbox. */\n image?: unknown\n /** Daytona sandbox resources. */\n resources?: DaytonaResources\n /** Default working directory, applied when a spawn doesn't set `SpawnConfig.cwd`. */\n cwd?: string\n /** Environment variables baked into the sandbox and re-sent on every command. */\n env?: Record<string, string>\n /** Logger for provider-level lifecycle lines. */\n logger?: Logger\n /** Total deadline, in seconds, for the post-create readiness probe. Defaults to `60`. */\n readinessTimeoutSeconds?: number\n /** Setup script to upload and run once the sandbox is ready. */\n pregame?: { name: string, content: string }\n /**\n * What to do with an owned sandbox on destroy:\n *\n * - `'delete'` / `'kill'` (default) — call `sandbox.delete()`.\n * - `'stop'` / `'pause'` — call `sandbox.stop()` so Daytona preserves state\n * according to the runner's stop semantics.\n * - `'archive'` — stop, then archive for longer-term persistence.\n * - `'leave'` — make no API call.\n */\n onDestroy?: DaytonaOnDestroy\n}\n\n/** Per-probe timeout for the readiness `true` command. */\nconst READINESS_PROBE_TIMEOUT_SECONDS = 5\n/** Pause between readiness probes while the sandbox is still warming up. */\nconst READINESS_BACKOFF_MS = 250\n/** Setup scripts commonly install dependencies, so give them a generous cap. */\nconst PREGAME_TIMEOUT_SECONDS = 30 * 60\n\nexport interface DaytonaReadinessOptions {\n /** Total deadline in seconds. `<= 0` disables the probe entirely. */\n timeoutSeconds: number\n /** Optional cleanup for owned sandboxes that never become ready. */\n cleanupOnTimeout?: () => Promise<void>\n /** Clock source — injectable for deterministic tests. */\n now?: () => number\n /** Sleep between probes — injectable for deterministic tests. */\n sleep?: (ms: number) => Promise<void>\n}\n\nexport function resolveDaytonaEnv(\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\nexport function normalizeDaytonaExecResult(response: DaytonaExecuteResponse): ExecResult {\n return {\n stdout: response.stdout ?? response.artifacts?.stdout ?? response.result ?? response.output ?? '',\n stderr: response.stderr ?? '',\n exitCode: typeof response.exitCode === 'number' ? response.exitCode : 0,\n }\n}\n\nfunction capExecResult(result: ExecResult): ExecResult {\n return { ...capBufferedExecOutput(result.stdout, result.stderr), exitCode: result.exitCode }\n}\n\nfunction normalizeDaytonaExecError(error: unknown): ExecResult {\n const err = error as { exitCode?: unknown, stdout?: unknown, stderr?: unknown, result?: unknown, output?: unknown, message?: unknown }\n if (typeof err.exitCode === 'number') {\n return {\n stdout: typeof err.stdout === 'string'\n ? err.stdout\n : typeof err.result === 'string'\n ? err.result\n : typeof err.output === 'string'\n ? err.output\n : '',\n stderr: typeof err.stderr === 'string' ? err.stderr : typeof err.message === 'string' ? err.message : '',\n exitCode: err.exitCode,\n }\n }\n return { stdout: '', stderr: error instanceof Error ? error.message : String(error), exitCode: 124 }\n}\n\nfunction bufferToString(value: Uint8Array | ArrayBuffer | { toString: (encoding?: BufferEncoding) => string }): string {\n if (Buffer.isBuffer(value))\n return value.toString('utf8')\n if (value instanceof ArrayBuffer)\n return new TextDecoder().decode(new Uint8Array(value))\n if (value instanceof Uint8Array)\n return new TextDecoder().decode(value)\n return value.toString('utf8')\n}\n\nfunction toClientConfig(apiKey?: string, apiUrl?: string, target?: string): DaytonaClientConfig | undefined {\n const config: DaytonaClientConfig = {}\n if (apiKey !== undefined)\n config.apiKey = apiKey\n if (apiUrl !== undefined)\n config.apiUrl = apiUrl\n if (target !== undefined)\n config.target = target\n return Object.keys(config).length > 0 ? config : undefined\n}\n\nfunction takeString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nexport function resolveDaytonaCreateParams(\n sandboxConfig: Record<string, unknown>,\n options: DaytonaProviderOptions,\n envVars: Record<string, string> | undefined,\n): DaytonaCreateParams | undefined {\n const reserved = new Set(['provider', 'apiKey', 'apiUrl', 'target', 'sandboxId'])\n const params: DaytonaCreateParams = {}\n\n for (const [key, value] of Object.entries(sandboxConfig)) {\n if (!reserved.has(key) && value !== undefined)\n params[key] = value\n }\n\n const language = takeString(sandboxConfig.language) ?? options.language\n const snapshot = takeString(sandboxConfig.snapshot) ?? options.snapshot\n const image = sandboxConfig.image ?? options.image\n const resources = (sandboxConfig.resources as DaytonaResources | undefined) ?? options.resources\n const configEnvVars = sandboxConfig.envVars as Record<string, string> | undefined\n const mergedEnvVars = resolveDaytonaEnv(envVars, configEnvVars)\n\n if (language !== undefined)\n params.language = language\n if (snapshot !== undefined)\n params.snapshot = snapshot\n if (image !== undefined)\n params.image = image\n if (resources !== undefined)\n params.resources = resources\n if (mergedEnvVars !== undefined)\n params.envVars = mergedEnvVars\n\n return Object.keys(params).length > 0 ? params : undefined\n}\n\nexport async function waitForDaytonaReady(\n sandbox: DaytonaSandbox,\n logger: Logger,\n opts: DaytonaReadinessOptions,\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 Daytona sandbox to initialize', { sandboxId: sandbox.id })\n\n while (true) {\n try {\n await sandbox.process.executeCommand('true', undefined, undefined, READINESS_PROBE_TIMEOUT_SECONDS)\n logger.info('Daytona sandbox ready', { sandboxId: sandbox.id, elapsedMs: now() - start })\n return\n }\n catch {\n if (now() >= deadline) {\n await opts.cleanupOnTimeout?.().catch(() => {})\n throw new Error(`Daytona sandbox ${sandbox.id} did not become ready within ${opts.timeoutSeconds}s`)\n }\n await sleep(READINESS_BACKOFF_MS)\n }\n }\n}\n\nexport async function runDaytonaPregame(\n sandbox: DaytonaSandbox,\n logger: Logger,\n opts: { name: string, content: string, cwd?: string, envVars?: Record<string, string> },\n): Promise<void> {\n const path = `/tmp/${opts.name}`\n logger.info('running Daytona pregame script', { sandboxId: sandbox.id, path })\n await sandbox.fs.uploadFile(Buffer.from(opts.content), path)\n\n const quoted = JSON.stringify(path)\n let result: ExecResult\n try {\n result = normalizeDaytonaExecResult(\n await sandbox.process.executeCommand(`chmod +x ${quoted} && ${quoted}`, opts.cwd, opts.envVars, PREGAME_TIMEOUT_SECONDS),\n )\n }\n catch (err) {\n result = normalizeDaytonaExecError(err)\n }\n\n if (result.exitCode !== 0) {\n logger.error('Daytona pregame script failed', { sandboxId: sandbox.id, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr })\n throw new Error(`Daytona pregame script ${opts.name} failed with exit code ${result.exitCode}`)\n }\n\n logger.info('Daytona pregame ready', { sandboxId: sandbox.id, stdout: result.stdout, stderr: result.stderr })\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport function createDaytonaProvider(options: DaytonaProviderOptions = {}): SandboxProvider {\n const live = new Map<string, { sandbox: DaytonaSandbox, env?: Record<string, string>, owned: boolean }>()\n const logger = options.logger ?? createLogger(consoleSink())\n const readinessTimeoutSeconds = options.readinessTimeoutSeconds ?? 60\n\n async function loadSdk(): Promise<DaytonaConstructor> {\n try {\n const mod = await import('@daytona/sdk') as { Daytona: DaytonaConstructor }\n return mod.Daytona\n }\n catch {\n throw new Error('@daytona/sdk is required for the Daytona sandbox provider. Install it with: bun add @daytona/sdk')\n }\n }\n\n function get(sandboxId: string): { sandbox: DaytonaSandbox, env?: Record<string, string>, owned: boolean } {\n const entry = live.get(sandboxId)\n if (!entry)\n throw new Error(`Daytona sandbox ${sandboxId} is not tracked by this provider`)\n return entry\n }\n\n async function deleteOwnedSandbox(sandbox: DaytonaSandbox): Promise<void> {\n try {\n await sandbox.delete()\n }\n catch {\n // Already gone — nothing to clean up.\n }\n }\n\n async function stopOwnedSandbox(sandbox: DaytonaSandbox): Promise<void> {\n if (!sandbox.stop) {\n logger.error('cannot stop Daytona sandbox: this @daytona/sdk install exposes no stop() method; leaving it running', { sandboxId: sandbox.id })\n return\n }\n try {\n await sandbox.stop()\n logger.info('stopped Daytona sandbox; reconnect later via the sandboxId option (CLI: --sandbox)', { sandboxId: sandbox.id })\n }\n catch (err) {\n logger.error('failed to stop Daytona sandbox; it will run until Daytona lifecycle policy stops it', { sandboxId: sandbox.id, error: err instanceof Error ? err.message : String(err) })\n }\n }\n\n async function archiveOwnedSandbox(sandbox: DaytonaSandbox): Promise<void> {\n if (!sandbox.stop || !sandbox.archive) {\n logger.error('cannot archive Daytona sandbox: this @daytona/sdk install exposes no stop() and archive() pair; leaving it running', { sandboxId: sandbox.id })\n return\n }\n try {\n await sandbox.stop()\n await sandbox.archive()\n logger.info('archived Daytona sandbox; reconnect later via the sandboxId option (CLI: --sandbox)', { sandboxId: sandbox.id })\n }\n catch (err) {\n logger.error('failed to archive Daytona sandbox; it will run until Daytona lifecycle policy stops it', { sandboxId: sandbox.id, error: err instanceof Error ? err.message : String(err) })\n }\n }\n\n async function startAttachedSandboxIfNeeded(sandbox: DaytonaSandbox): Promise<void> {\n if (sandbox.state !== 'stopped' && sandbox.state !== 'archived')\n return\n if (!sandbox.start) {\n logger.error('cannot start Daytona sandbox before attach readiness probe: this @daytona/sdk install exposes no start() method', { sandboxId: sandbox.id, state: sandbox.state })\n return\n }\n logger.info('starting Daytona sandbox before attach readiness probe', { sandboxId: sandbox.id, state: sandbox.state })\n await sandbox.start(readinessTimeoutSeconds > 0 ? readinessTimeoutSeconds : undefined)\n }\n\n return {\n name: 'daytona',\n\n async spawn(config: SpawnConfig = {}): Promise<{ id: string, cwd: string }> {\n const Daytona = await loadSdk()\n const sb: Record<string, unknown> = config.sandbox ?? {}\n\n const apiKey = takeString(sb.apiKey) ?? options.apiKey\n const apiUrl = takeString(sb.apiUrl) ?? options.apiUrl\n const target = takeString(sb.target) ?? options.target\n const connectId = takeString(sb.sandboxId) ?? options.sandboxId\n const envVars = resolveDaytonaEnv(options.env, config.env)\n\n const daytona = new Daytona(toClientConfig(apiKey, apiUrl, target))\n const attached = connectId !== undefined\n // Lifecycle ownership drives `destroy()` (onDestroy). A created sandbox is\n // always owned; an attached one is owned only when the host opts in via\n // `ownAttached`. Spawn-FAILURE cleanup is separate (see below) and keyed\n // on `!attached` so a pre-existing sandbox is never auto-deleted.\n const owned = !attached || options.ownAttached === true\n const sandbox = attached\n ? await daytona.get(connectId)\n : await daytona.create(resolveDaytonaCreateParams(sb, options, envVars))\n\n if (attached)\n await startAttachedSandboxIfNeeded(sandbox)\n\n live.set(sandbox.id, { sandbox, env: envVars, owned })\n\n try {\n await waitForDaytonaReady(sandbox, logger, {\n timeoutSeconds: readinessTimeoutSeconds,\n // Only auto-delete a sandbox we CREATED on readiness failure — never a\n // pre-existing attached one, even when we own its lifecycle.\n ...(!attached ? { cleanupOnTimeout: () => deleteOwnedSandbox(sandbox) } : {}),\n })\n }\n catch (err) {\n live.delete(sandbox.id)\n throw err\n }\n\n const requestedCwd = config.cwd ?? options.cwd\n let cwd: string\n if (requestedCwd) {\n cwd = requestedCwd\n await sandbox.process.executeCommand(`mkdir -p ${JSON.stringify(cwd)}`).catch(() => {})\n }\n else if (sandbox.getWorkDir) {\n cwd = await sandbox.getWorkDir().catch(() => '')\n if (!cwd)\n cwd = await sandbox.process.executeCommand('pwd').then(r => normalizeDaytonaExecResult(r).stdout.trim()).catch(() => '') || '/home/daytona'\n }\n else {\n cwd = await sandbox.process.executeCommand('pwd').then(r => normalizeDaytonaExecResult(r).stdout.trim()).catch(() => '') || '/home/daytona'\n }\n\n if (options.pregame) {\n try {\n await runDaytonaPregame(sandbox, logger, { name: options.pregame.name, content: options.pregame.content, cwd, envVars })\n }\n catch (err) {\n // Spawn-failure cleanup: only delete a sandbox we created, never a\n // pre-existing attached one (the host owns its prior state).\n if (!attached)\n await deleteOwnedSandbox(sandbox)\n live.delete(sandbox.id)\n throw err\n }\n }\n\n return { id: sandbox.id, cwd }\n },\n\n async exec(sandboxId, command, opts): Promise<ExecResult> {\n const { sandbox, env } = get(sandboxId)\n const timeoutSeconds = opts?.timeout ?? 30\n try {\n // The SDK buffers the streams uncapped (see the KNOWN RESIDUAL note\n // on `ExecutionContext.fileSize`) — cap host-side before the strings\n // propagate into tool results and session history.\n return capExecResult(normalizeDaytonaExecResult(\n await sandbox.process.executeCommand(\n command,\n opts?.cwd,\n resolveDaytonaEnv(env, opts?.env),\n timeoutSeconds > 0 ? timeoutSeconds : undefined,\n ),\n ))\n }\n catch (err) {\n return capExecResult(normalizeDaytonaExecError(err))\n }\n },\n\n async readFile(sandboxId, path): Promise<string> {\n return bufferToString(await get(sandboxId).sandbox.fs.downloadFile(path))\n },\n\n async writeFile(sandboxId, path, content): Promise<void> {\n await get(sandboxId).sandbox.fs.uploadFile(Buffer.from(content), path)\n },\n\n async listFiles(sandboxId, path): Promise<string[]> {\n const entries = await get(sandboxId).sandbox.fs.listFiles(path)\n // Trailing `/` on directories — parity with process/docker contexts.\n return entries.map(e => (e.isDir ? `${e.name}/` : e.name))\n },\n\n async destroy(sandboxId): Promise<void> {\n const entry = live.get(sandboxId)\n if (!entry)\n return\n if (entry.owned) {\n const action = options.onDestroy ?? 'delete'\n if (action === 'stop' || action === 'pause')\n await stopOwnedSandbox(entry.sandbox)\n else if (action === 'archive')\n await archiveOwnedSandbox(entry.sandbox)\n else if (action === 'delete' || action === 'kill')\n await deleteOwnedSandbox(entry.sandbox)\n }\n live.delete(sandboxId)\n },\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"daytona.js","names":[],"sources":["../../src/contexts/daytona.ts"],"sourcesContent":["/**\n * Daytona sandbox provider.\n *\n * Implements {@link SandboxProvider} on top of Daytona sandboxes. Pair it with\n * {@link createSandboxContext} to run an agent's shell + filesystem tools\n * inside a Daytona sandbox:\n *\n * ```ts\n * import { createSandboxContext } from 'zidane/contexts'\n * import { createDaytonaProvider } from 'zidane/contexts/daytona'\n *\n * const execution = createSandboxContext(createDaytonaProvider())\n * ```\n *\n * Requires `@daytona/sdk` as an optional peer dependency:\n * `bun add @daytona/sdk`.\n */\n\nimport type { Logger } from '../logger'\nimport type { SandboxProvider } from './sandbox'\nimport type { ExecResult, SpawnConfig, StreamHandle } from './types'\nimport { Buffer } from 'node:buffer'\nimport { consoleSink, createLogger } from '../logger'\nimport { capBufferedExecOutput } from './output-cap'\nimport { StreamQueue } from './stream-queue'\n\n// ---------------------------------------------------------------------------\n// Minimal structural views of the `@daytona/sdk` surface we touch. Declared\n// locally so zidane typechecks without the optional peer dependency installed.\n// ---------------------------------------------------------------------------\n\ninterface DaytonaExecuteResponse {\n exitCode?: number | null\n result?: string | null\n output?: string | null\n stdout?: string | null\n stderr?: string | null\n artifacts?: { stdout?: string | null }\n}\n\ninterface DaytonaFileEntry {\n name: string\n /** Present in the Daytona SDK's FileInfo; used for the `/` dir suffix. */\n isDir?: boolean\n}\n\n/**\n * Structural subset of Daytona's `PtyHandle`.\n *\n * Daytona's process/session API has NO stdin channel at all — sessions are\n * fire-and-forget with log streaming — so a PTY is the only way to reach a\n * long-lived process interactively. See the `execStream` implementation for\n * what that costs.\n */\ninterface DaytonaPtyHandle {\n pid?: number\n waitForConnection: () => Promise<unknown>\n sendInput: (data: string | Uint8Array) => Promise<unknown>\n wait: () => Promise<{ exitCode?: number }>\n kill?: () => Promise<unknown>\n disconnect: () => Promise<unknown>\n}\n\ninterface DaytonaSandbox {\n id: string\n state?: string\n process: {\n executeCommand: (command: string, cwd?: string, env?: Record<string, string>, timeout?: number) => Promise<DaytonaExecuteResponse>\n /** Present in recent SDKs; only `execStream` uses it. */\n createPty?: (options: {\n /**\n * REQUIRED by the API despite reading like a convenience handle: the\n * SDK forwards it as the session id and the control plane rejects the\n * create with 400 `session ID is required` when it is absent.\n */\n id: string\n cwd?: string\n envs?: Record<string, string>\n onData?: (data: Uint8Array) => void\n }) => Promise<DaytonaPtyHandle>\n }\n fs: {\n downloadFile: (path: string) => Promise<Uint8Array | ArrayBuffer | { toString: (encoding?: BufferEncoding) => string }>\n uploadFile: (content: Uint8Array | Buffer, path: string) => Promise<unknown>\n listFiles: (path: string) => Promise<DaytonaFileEntry[]>\n }\n getWorkDir?: () => Promise<string>\n start?: (timeout?: number) => Promise<unknown>\n delete: (timeout?: number) => Promise<unknown>\n stop?: (timeout?: number) => Promise<unknown>\n archive?: () => Promise<unknown>\n}\n\ninterface DaytonaClientConfig {\n apiKey?: string\n apiUrl?: string\n target?: string\n}\n\ninterface DaytonaResources {\n cpu?: number\n memory?: number\n disk?: number\n gpu?: number\n [key: string]: unknown\n}\n\ninterface DaytonaCreateParams {\n language?: string\n snapshot?: string\n image?: unknown\n resources?: DaytonaResources\n envVars?: Record<string, string>\n [key: string]: unknown\n}\n\ninterface DaytonaClient {\n create: (params?: DaytonaCreateParams) => Promise<DaytonaSandbox>\n get: (sandboxIdOrName: string) => Promise<DaytonaSandbox>\n}\n\ntype DaytonaConstructor = new (config?: DaytonaClientConfig) => DaytonaClient\n\n// ---------------------------------------------------------------------------\n// Provider options\n// ---------------------------------------------------------------------------\n\nexport type DaytonaOnDestroy = 'delete' | 'kill' | 'stop' | 'pause' | 'archive' | 'leave'\n\nexport interface DaytonaProviderOptions {\n /** Daytona API key. Falls back to the `DAYTONA_API_KEY` env var when omitted. */\n apiKey?: string\n /** Daytona API URL. Falls back to `DAYTONA_API_URL`, then Daytona's hosted default. */\n apiUrl?: string\n /** Daytona target / region. Falls back to `DAYTONA_TARGET`, then the org default. */\n target?: string\n /**\n * Attach to a pre-existing sandbox by id or name instead of creating a fresh\n * one. By default the provider treats attached sandboxes as externally owned\n * and leaves them running on destroy/failure (set {@link ownAttached} to take\n * over the lifecycle).\n */\n sandboxId?: string\n /**\n * Take ownership of an attached ({@link sandboxId}) sandbox's lifecycle, so\n * `destroy()` applies {@link onDestroy} to it (stop / archive / delete)\n * instead of leaving it running.\n *\n * The intended caller is a durable host that *owns* the sandbox across\n * invocations: mint it once, journal its id, re-attach via `sandboxId` on\n * every replay/resume, and clean it up at the end of the logical run. Without\n * this an attached sandbox can never be stopped/deleted through the provider —\n * a leak for host-owned sandboxes. No effect when creating a fresh sandbox\n * (those are always owned). Spawn-time failures (readiness timeout, pregame\n * error) still never delete a pre-existing attached sandbox — only sandboxes\n * this provider *created* are auto-cleaned on spawn failure.\n *\n * Default: `false`.\n */\n ownAttached?: boolean\n /** Daytona snapshot used when creating a fresh sandbox. */\n snapshot?: string\n /** Runtime used for Daytona's code tooling. Shell execution works either way. */\n language?: string\n /** Daytona image/declarative builder input used when creating a fresh sandbox. */\n image?: unknown\n /** Daytona sandbox resources. */\n resources?: DaytonaResources\n /** Default working directory, applied when a spawn doesn't set `SpawnConfig.cwd`. */\n cwd?: string\n /** Environment variables baked into the sandbox and re-sent on every command. */\n env?: Record<string, string>\n /** Logger for provider-level lifecycle lines. */\n logger?: Logger\n /** Total deadline, in seconds, for the post-create readiness probe. Defaults to `60`. */\n readinessTimeoutSeconds?: number\n /** Setup script to upload and run once the sandbox is ready. */\n pregame?: { name: string, content: string }\n /**\n * What to do with an owned sandbox on destroy:\n *\n * - `'delete'` / `'kill'` (default) — call `sandbox.delete()`.\n * - `'stop'` / `'pause'` — call `sandbox.stop()` so Daytona preserves state\n * according to the runner's stop semantics.\n * - `'archive'` — stop, then archive for longer-term persistence.\n * - `'leave'` — make no API call.\n */\n onDestroy?: DaytonaOnDestroy\n}\n\n/** Per-probe timeout for the readiness `true` command. */\nconst READINESS_PROBE_TIMEOUT_SECONDS = 5\n/** Pause between readiness probes while the sandbox is still warming up. */\nconst READINESS_BACKOFF_MS = 250\n/** Setup scripts commonly install dependencies, so give them a generous cap. */\nconst PREGAME_TIMEOUT_SECONDS = 30 * 60\n\nexport interface DaytonaReadinessOptions {\n /** Total deadline in seconds. `<= 0` disables the probe entirely. */\n timeoutSeconds: number\n /** Optional cleanup for owned sandboxes that never become ready. */\n cleanupOnTimeout?: () => Promise<void>\n /** Clock source — injectable for deterministic tests. */\n now?: () => number\n /** Sleep between probes — injectable for deterministic tests. */\n sleep?: (ms: number) => Promise<void>\n}\n\nexport function resolveDaytonaEnv(\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\nexport function normalizeDaytonaExecResult(response: DaytonaExecuteResponse): ExecResult {\n return {\n stdout: response.stdout ?? response.artifacts?.stdout ?? response.result ?? response.output ?? '',\n stderr: response.stderr ?? '',\n exitCode: typeof response.exitCode === 'number' ? response.exitCode : 0,\n }\n}\n\nfunction capExecResult(result: ExecResult): ExecResult {\n return { ...capBufferedExecOutput(result.stdout, result.stderr), exitCode: result.exitCode }\n}\n\nfunction normalizeDaytonaExecError(error: unknown): ExecResult {\n const err = error as { exitCode?: unknown, stdout?: unknown, stderr?: unknown, result?: unknown, output?: unknown, message?: unknown }\n if (typeof err.exitCode === 'number') {\n return {\n stdout: typeof err.stdout === 'string'\n ? err.stdout\n : typeof err.result === 'string'\n ? err.result\n : typeof err.output === 'string'\n ? err.output\n : '',\n stderr: typeof err.stderr === 'string' ? err.stderr : typeof err.message === 'string' ? err.message : '',\n exitCode: err.exitCode,\n }\n }\n return { stdout: '', stderr: error instanceof Error ? error.message : String(error), exitCode: 124 }\n}\n\nfunction bufferToString(value: Uint8Array | ArrayBuffer | { toString: (encoding?: BufferEncoding) => string }): string {\n if (Buffer.isBuffer(value))\n return value.toString('utf8')\n if (value instanceof ArrayBuffer)\n return new TextDecoder().decode(new Uint8Array(value))\n if (value instanceof Uint8Array)\n return new TextDecoder().decode(value)\n return value.toString('utf8')\n}\n\nfunction toClientConfig(apiKey?: string, apiUrl?: string, target?: string): DaytonaClientConfig | undefined {\n const config: DaytonaClientConfig = {}\n if (apiKey !== undefined)\n config.apiKey = apiKey\n if (apiUrl !== undefined)\n config.apiUrl = apiUrl\n if (target !== undefined)\n config.target = target\n return Object.keys(config).length > 0 ? config : undefined\n}\n\nfunction takeString(value: unknown): string | undefined {\n return typeof value === 'string' && value.length > 0 ? value : undefined\n}\n\nexport function resolveDaytonaCreateParams(\n sandboxConfig: Record<string, unknown>,\n options: DaytonaProviderOptions,\n envVars: Record<string, string> | undefined,\n): DaytonaCreateParams | undefined {\n const reserved = new Set(['provider', 'apiKey', 'apiUrl', 'target', 'sandboxId'])\n const params: DaytonaCreateParams = {}\n\n for (const [key, value] of Object.entries(sandboxConfig)) {\n if (!reserved.has(key) && value !== undefined)\n params[key] = value\n }\n\n const language = takeString(sandboxConfig.language) ?? options.language\n const snapshot = takeString(sandboxConfig.snapshot) ?? options.snapshot\n const image = sandboxConfig.image ?? options.image\n const resources = (sandboxConfig.resources as DaytonaResources | undefined) ?? options.resources\n const configEnvVars = sandboxConfig.envVars as Record<string, string> | undefined\n const mergedEnvVars = resolveDaytonaEnv(envVars, configEnvVars)\n\n if (language !== undefined)\n params.language = language\n if (snapshot !== undefined)\n params.snapshot = snapshot\n if (image !== undefined)\n params.image = image\n if (resources !== undefined)\n params.resources = resources\n if (mergedEnvVars !== undefined)\n params.envVars = mergedEnvVars\n\n return Object.keys(params).length > 0 ? params : undefined\n}\n\nexport async function waitForDaytonaReady(\n sandbox: DaytonaSandbox,\n logger: Logger,\n opts: DaytonaReadinessOptions,\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 Daytona sandbox to initialize', { sandboxId: sandbox.id })\n\n while (true) {\n try {\n await sandbox.process.executeCommand('true', undefined, undefined, READINESS_PROBE_TIMEOUT_SECONDS)\n logger.info('Daytona sandbox ready', { sandboxId: sandbox.id, elapsedMs: now() - start })\n return\n }\n catch {\n if (now() >= deadline) {\n await opts.cleanupOnTimeout?.().catch(() => {})\n throw new Error(`Daytona sandbox ${sandbox.id} did not become ready within ${opts.timeoutSeconds}s`)\n }\n await sleep(READINESS_BACKOFF_MS)\n }\n }\n}\n\nexport async function runDaytonaPregame(\n sandbox: DaytonaSandbox,\n logger: Logger,\n opts: { name: string, content: string, cwd?: string, envVars?: Record<string, string> },\n): Promise<void> {\n const path = `/tmp/${opts.name}`\n logger.info('running Daytona pregame script', { sandboxId: sandbox.id, path })\n await sandbox.fs.uploadFile(Buffer.from(opts.content), path)\n\n const quoted = JSON.stringify(path)\n let result: ExecResult\n try {\n result = normalizeDaytonaExecResult(\n await sandbox.process.executeCommand(`chmod +x ${quoted} && ${quoted}`, opts.cwd, opts.envVars, PREGAME_TIMEOUT_SECONDS),\n )\n }\n catch (err) {\n result = normalizeDaytonaExecError(err)\n }\n\n if (result.exitCode !== 0) {\n logger.error('Daytona pregame script failed', { sandboxId: sandbox.id, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr })\n throw new Error(`Daytona pregame script ${opts.name} failed with exit code ${result.exitCode}`)\n }\n\n logger.info('Daytona pregame ready', { sandboxId: sandbox.id, stdout: result.stdout, stderr: result.stderr })\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport function createDaytonaProvider(options: DaytonaProviderOptions = {}): SandboxProvider {\n const live = new Map<string, { sandbox: DaytonaSandbox, env?: Record<string, string>, owned: boolean }>()\n const logger = options.logger ?? createLogger(consoleSink())\n const readinessTimeoutSeconds = options.readinessTimeoutSeconds ?? 60\n\n async function loadSdk(): Promise<DaytonaConstructor> {\n try {\n const mod = await import('@daytona/sdk') as { Daytona: DaytonaConstructor }\n return mod.Daytona\n }\n catch {\n throw new Error('@daytona/sdk is required for the Daytona sandbox provider. Install it with: bun add @daytona/sdk')\n }\n }\n\n function get(sandboxId: string): { sandbox: DaytonaSandbox, env?: Record<string, string>, owned: boolean } {\n const entry = live.get(sandboxId)\n if (!entry)\n throw new Error(`Daytona sandbox ${sandboxId} is not tracked by this provider`)\n return entry\n }\n\n async function deleteOwnedSandbox(sandbox: DaytonaSandbox): Promise<void> {\n try {\n await sandbox.delete()\n }\n catch {\n // Already gone — nothing to clean up.\n }\n }\n\n async function stopOwnedSandbox(sandbox: DaytonaSandbox): Promise<void> {\n if (!sandbox.stop) {\n logger.error('cannot stop Daytona sandbox: this @daytona/sdk install exposes no stop() method; leaving it running', { sandboxId: sandbox.id })\n return\n }\n try {\n await sandbox.stop()\n logger.info('stopped Daytona sandbox; reconnect later via the sandboxId option (CLI: --sandbox)', { sandboxId: sandbox.id })\n }\n catch (err) {\n logger.error('failed to stop Daytona sandbox; it will run until Daytona lifecycle policy stops it', { sandboxId: sandbox.id, error: err instanceof Error ? err.message : String(err) })\n }\n }\n\n async function archiveOwnedSandbox(sandbox: DaytonaSandbox): Promise<void> {\n if (!sandbox.stop || !sandbox.archive) {\n logger.error('cannot archive Daytona sandbox: this @daytona/sdk install exposes no stop() and archive() pair; leaving it running', { sandboxId: sandbox.id })\n return\n }\n try {\n await sandbox.stop()\n await sandbox.archive()\n logger.info('archived Daytona sandbox; reconnect later via the sandboxId option (CLI: --sandbox)', { sandboxId: sandbox.id })\n }\n catch (err) {\n logger.error('failed to archive Daytona sandbox; it will run until Daytona lifecycle policy stops it', { sandboxId: sandbox.id, error: err instanceof Error ? err.message : String(err) })\n }\n }\n\n async function startAttachedSandboxIfNeeded(sandbox: DaytonaSandbox): Promise<void> {\n if (sandbox.state !== 'stopped' && sandbox.state !== 'archived')\n return\n if (!sandbox.start) {\n logger.error('cannot start Daytona sandbox before attach readiness probe: this @daytona/sdk install exposes no start() method', { sandboxId: sandbox.id, state: sandbox.state })\n return\n }\n logger.info('starting Daytona sandbox before attach readiness probe', { sandboxId: sandbox.id, state: sandbox.state })\n await sandbox.start(readinessTimeoutSeconds > 0 ? readinessTimeoutSeconds : undefined)\n }\n\n return {\n name: 'daytona',\n\n async spawn(config: SpawnConfig = {}): Promise<{ id: string, cwd: string }> {\n const Daytona = await loadSdk()\n const sb: Record<string, unknown> = config.sandbox ?? {}\n\n const apiKey = takeString(sb.apiKey) ?? options.apiKey\n const apiUrl = takeString(sb.apiUrl) ?? options.apiUrl\n const target = takeString(sb.target) ?? options.target\n const connectId = takeString(sb.sandboxId) ?? options.sandboxId\n const envVars = resolveDaytonaEnv(options.env, config.env)\n\n const daytona = new Daytona(toClientConfig(apiKey, apiUrl, target))\n const attached = connectId !== undefined\n // Lifecycle ownership drives `destroy()` (onDestroy). A created sandbox is\n // always owned; an attached one is owned only when the host opts in via\n // `ownAttached`. Spawn-FAILURE cleanup is separate (see below) and keyed\n // on `!attached` so a pre-existing sandbox is never auto-deleted.\n const owned = !attached || options.ownAttached === true\n const sandbox = attached\n ? await daytona.get(connectId)\n : await daytona.create(resolveDaytonaCreateParams(sb, options, envVars))\n\n if (attached)\n await startAttachedSandboxIfNeeded(sandbox)\n\n live.set(sandbox.id, { sandbox, env: envVars, owned })\n\n try {\n await waitForDaytonaReady(sandbox, logger, {\n timeoutSeconds: readinessTimeoutSeconds,\n // Only auto-delete a sandbox we CREATED on readiness failure — never a\n // pre-existing attached one, even when we own its lifecycle.\n ...(!attached ? { cleanupOnTimeout: () => deleteOwnedSandbox(sandbox) } : {}),\n })\n }\n catch (err) {\n live.delete(sandbox.id)\n throw err\n }\n\n const requestedCwd = config.cwd ?? options.cwd\n let cwd: string\n if (requestedCwd) {\n cwd = requestedCwd\n await sandbox.process.executeCommand(`mkdir -p ${JSON.stringify(cwd)}`).catch(() => {})\n }\n else if (sandbox.getWorkDir) {\n cwd = await sandbox.getWorkDir().catch(() => '')\n if (!cwd)\n cwd = await sandbox.process.executeCommand('pwd').then(r => normalizeDaytonaExecResult(r).stdout.trim()).catch(() => '') || '/home/daytona'\n }\n else {\n cwd = await sandbox.process.executeCommand('pwd').then(r => normalizeDaytonaExecResult(r).stdout.trim()).catch(() => '') || '/home/daytona'\n }\n\n if (options.pregame) {\n try {\n await runDaytonaPregame(sandbox, logger, { name: options.pregame.name, content: options.pregame.content, cwd, envVars })\n }\n catch (err) {\n // Spawn-failure cleanup: only delete a sandbox we created, never a\n // pre-existing attached one (the host owns its prior state).\n if (!attached)\n await deleteOwnedSandbox(sandbox)\n live.delete(sandbox.id)\n throw err\n }\n }\n\n return { id: sandbox.id, cwd }\n },\n\n async exec(sandboxId, command, opts): Promise<ExecResult> {\n const { sandbox, env } = get(sandboxId)\n const timeoutSeconds = opts?.timeout ?? 30\n try {\n // The SDK buffers the streams uncapped (see the KNOWN RESIDUAL note\n // on `ExecutionContext.fileSize`) — cap host-side before the strings\n // propagate into tool results and session history.\n return capExecResult(normalizeDaytonaExecResult(\n await sandbox.process.executeCommand(\n command,\n opts?.cwd,\n resolveDaytonaEnv(env, opts?.env),\n timeoutSeconds > 0 ? timeoutSeconds : undefined,\n ),\n ))\n }\n catch (err) {\n return capExecResult(normalizeDaytonaExecError(err))\n }\n },\n\n /**\n * Live bidirectional stream — see `ExecutionContext.execStream`.\n *\n * PTY-backed, and not by preference: Daytona's process + session API\n * exposes no stdin at all (sessions are fire-and-forget with log\n * streaming), so a pseudo-terminal is the only interactive channel the\n * platform offers. That forces three mitigations, all in the launch\n * wrapper below, because a PTY is a terminal and a protocol stream is\n * not:\n *\n * 1. `stty -echo` — a terminal echoes everything written to stdin back\n * onto stdout. Without this the caller reads its own requests back\n * as if they were responses.\n * 2. `stty raw` — canonical mode rewrites `\\n`→`\\r\\n` and caps a line\n * at ~4096 bytes. Protocol frames routinely exceed that and would\n * be silently truncated mid-message.\n * 3. `2><file>` — a PTY has ONE stream, so the child's stderr would\n * otherwise interleave into the protocol data and corrupt framing.\n * It is redirected to a file inside the sandbox and surfaced on the\n * returned handle by polling that file through the exec API.\n *\n * The mitigations must run before the child's first byte — but the\n * launch line ITSELF is typed into a canonical-mode terminal, so it is\n * echoed once (a short non-JSON line the transport's tolerant framing\n * discards) and subject to the ~4096-byte line cap. That is why the\n * real command lives in an uploaded script and the typed line stays\n * short and constant: a long command line would be truncated before\n * `stty` ever ran. The script also restores `sh` compound-command\n * semantics that a bare `exec <command>` would break.\n */\n async execStream(sandboxId, command, opts): Promise<StreamHandle> {\n const { sandbox, env } = get(sandboxId)\n\n if (!sandbox.process.createPty) {\n throw new Error(\n 'Daytona SDK is too old: `process.createPty` is required for execStream '\n + '(used to run stdio MCP servers inside the sandbox).',\n )\n }\n\n opts?.signal?.throwIfAborted()\n\n const token = Math.random().toString(36).slice(2, 10)\n const stderrPath = `/tmp/.zidane-stream-${token}.err`\n const scriptPath = `/tmp/.zidane-stream-${token}.sh`\n\n // `exec 2>` rebinds the shell's stderr before the command runs, so\n // the command text needs no rewriting and stays compound-safe.\n await sandbox.fs.uploadFile(\n Buffer.from(`exec 2>${stderrPath}\\n${command}\\n`),\n scriptPath,\n )\n\n // See the E2B counterpart: the PTY's `onData` is a sync `void`\n // callback, so a stalled consumer can only be reported, never slowed.\n const warnBacklog = (which: string) => (bytes: number) =>\n logger.warn('execStream consumer is not draining output', { sandboxId, stream: which, bufferedBytes: bytes })\n const stdout = new StreamQueue(warnBacklog('stdout'))\n const stderr = new StreamQueue(warnBacklog('stderr'))\n\n // Everything the shell emits before the child is actually running is\n // NOISE that must never reach the caller: the echo of our own launch\n // line (typed before `-echo` takes effect), the login shell's prompt,\n // and its line-editor redraw escapes. `createPty` is `lazyStart`, so\n // the shell frequently does not exist yet when we type — it boots\n // afterwards and REPLAYS the buffered input through its line editor.\n // Observed on a stock sandbox: the stream opened with the echoed\n // launch line, the echoed first request, `\\u001b[?2004h` bracketed\n // paste, and a `%` prompt. An MCP client parses that echoed request\n // as a message and the handshake collapses.\n //\n // So: gate the stream on a sentinel the CHILD side prints, and drop\n // every byte before it. The sentinel is assembled by `printf` at\n // runtime (`Z-%s-READY` + token) so the echoed *source* line, which\n // contains the format string rather than the result, cannot match it.\n const sentinel = `Z-${token}-READY`\n let gateOpen = false\n let gateBuffer = ''\n let openGate: () => void = () => {}\n const gateReady = new Promise<void>((r) => { openGate = r })\n\n const pty = await sandbox.process.createPty({\n id: `zidane-stream-${token}`,\n ...(opts?.cwd ? { cwd: opts.cwd } : {}),\n envs: resolveDaytonaEnv(env, opts?.env) ?? {},\n onData: (data: Uint8Array) => {\n if (gateOpen) {\n stdout.push(data)\n return\n }\n gateBuffer += Buffer.from(data).toString('utf8')\n const at = gateBuffer.indexOf(sentinel)\n if (at === -1) {\n // Bounded: never let pre-gate noise grow without limit, but keep\n // a tail longer than the sentinel so it can't be split away.\n if (gateBuffer.length > 64 * 1024)\n gateBuffer = gateBuffer.slice(-1024)\n return\n }\n gateOpen = true\n openGate()\n // Everything after the sentinel's trailing newline is real output.\n const rest = gateBuffer.slice(at + sentinel.length).replace(/^\\r?\\n/, '')\n gateBuffer = ''\n if (rest.length > 0)\n stdout.push(new TextEncoder().encode(rest))\n },\n })\n await pty.waitForConnection()\n\n // Short, constant, and under any canonical line cap. `exec` replaces\n // the login shell so the PTY's lifetime is the script's lifetime and\n // no shell prompt can reach stdout.\n await pty.sendInput(`stty raw -echo; printf 'Z-%s-READY\\\\n' ${token}; exec sh ${scriptPath}\\n`)\n\n // Block until the child is genuinely live. Without this the caller can\n // write its first frame into a shell that hasn't started, where it is\n // echoed back and misread as a response.\n let gateTimer: ReturnType<typeof setTimeout> | undefined\n await Promise.race([\n gateReady,\n new Promise<void>((r) => { gateTimer = setTimeout(r, 30_000) }),\n ])\n clearTimeout(gateTimer)\n if (!gateOpen) {\n // Full teardown, not just a disconnect: Daytona PTY sessions persist\n // server-side after the websocket drops, so a bare disconnect leaks\n // one login shell plus two /tmp files per attempt — a bootstrap\n // retry loop against a broken image would stack them until the\n // sandbox dies.\n await (pty.kill?.() ?? Promise.resolve()).catch(() => {})\n await pty.disconnect().catch(() => {})\n await sandbox.process.executeCommand(`rm -f ${scriptPath} ${stderrPath}`).catch(() => {})\n throw new Error(\n `Daytona execStream: PTY did not reach the ready sentinel within 30s `\n + `(sandbox ${sandboxId}). The login shell may not have started.`,\n )\n }\n\n // stderr file → handle.stderr. Poll rather than stream: the exec API\n // is the only other channel into the sandbox, and diagnostics don't\n // need latency. Byte offsets keep re-reads incremental.\n //\n // The offset comes from the FILE (`wc -c`), never from measuring the\n // returned content: an exec API that trims a trailing newline from\n // command output would make a content-derived offset under-advance by\n // one byte per poll, re-emitting the tail byte forever. The size rides\n // back on the same round trip as a `ZEOF-<token>:` suffix; content is\n // whatever precedes it, so content-side transforms can't drift the\n // cursor either.\n let stderrOffset = 0\n let torndown = false\n const eofMark = `ZEOF-${token}:`\n const drainStderr = async (): Promise<void> => {\n try {\n const res = normalizeDaytonaExecResult(await sandbox.process.executeCommand(\n `tail -c +${stderrOffset + 1} ${stderrPath} 2>/dev/null; printf '${eofMark}%s' \"$(wc -c < ${stderrPath} 2>/dev/null || echo 0)\"`,\n ))\n const at = res.stdout.lastIndexOf(eofMark)\n if (at === -1)\n return\n const content = res.stdout.slice(0, at)\n const size = Number.parseInt(res.stdout.slice(at + eofMark.length).trim(), 10)\n if (content.length > 0)\n stderr.push(new TextEncoder().encode(content))\n if (Number.isFinite(size) && size > stderrOffset)\n stderrOffset = size\n }\n catch {\n // Diagnostic channel — polling failures must never surface as\n // protocol errors.\n }\n }\n const stderrTimer = setInterval(() => { void drainStderr() }, 2000)\n // Guarded: Bun/Node timers support unref, test fakes may not.\n ;(stderrTimer as { unref?: () => void }).unref?.()\n\n // Shared teardown for both exit paths (natural exit and kill): one\n // final stderr drain so a crash message isn't lost, then remove the\n // script + stderr files that would otherwise accumulate in /tmp for\n // the sandbox's lifetime.\n const teardown = async (): Promise<void> => {\n if (torndown)\n return\n torndown = true\n clearInterval(stderrTimer)\n await drainStderr()\n await sandbox.process.executeCommand(`rm -f ${scriptPath} ${stderrPath}`).catch(() => {})\n stdout.close()\n stderr.close()\n }\n\n const exited = pty.wait()\n .then(result => result?.exitCode ?? 0)\n .catch(() => 137)\n .finally(teardown)\n\n const kill = async (): Promise<void> => {\n // A PTY login shell never exits on its own — killing it IS the\n // normal termination path here, not an error path.\n await (pty.kill?.() ?? Promise.resolve()).catch(() => {})\n await pty.disconnect().catch(() => {})\n await teardown()\n }\n\n if (opts?.signal) {\n if (opts.signal.aborted)\n void kill()\n else\n opts.signal.addEventListener('abort', () => { void kill() }, { once: true })\n }\n\n return {\n pid: pty.pid ?? -1,\n stdout,\n stderr,\n write: async (data: Uint8Array): Promise<void> => {\n await pty.sendInput(data)\n },\n kill,\n exited,\n }\n },\n\n async readFile(sandboxId, path): Promise<string> {\n return bufferToString(await get(sandboxId).sandbox.fs.downloadFile(path))\n },\n\n async writeFile(sandboxId, path, content): Promise<void> {\n await get(sandboxId).sandbox.fs.uploadFile(Buffer.from(content), path)\n },\n\n async listFiles(sandboxId, path): Promise<string[]> {\n const entries = await get(sandboxId).sandbox.fs.listFiles(path)\n // Trailing `/` on directories — parity with process/docker contexts.\n return entries.map(e => (e.isDir ? `${e.name}/` : e.name))\n },\n\n async destroy(sandboxId): Promise<void> {\n const entry = live.get(sandboxId)\n if (!entry)\n return\n if (entry.owned) {\n const action = options.onDestroy ?? 'delete'\n if (action === 'stop' || action === 'pause')\n await stopOwnedSandbox(entry.sandbox)\n else if (action === 'archive')\n await archiveOwnedSandbox(entry.sandbox)\n else if (action === 'delete' || action === 'kill')\n await deleteOwnedSandbox(entry.sandbox)\n }\n live.delete(sandboxId)\n },\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { c as ExecutionContext, p as SpawnConfig } from "../types-BDccavwj.js";
1
+ import { c as ExecutionContext, p as SpawnConfig } from "../types-DdZMQxfT.js";
2
2
 
3
3
  //#region src/contexts/docker.d.ts
4
4
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"docker.d.ts","names":[],"sources":["../../src/contexts/docker.ts"],"mappings":";;;;;;;;;;cAwBa,oBAAA;AAAA,iBAmBG,mBAAA,CAAoB,MAAA,GAAS,WAAA,GAAc,gBAAgB;;iBAqZ3D,cAAA,CAAe,MAAA,EAAQ,WAAW,CAAC,WAAA"}
1
+ {"version":3,"file":"docker.d.ts","names":[],"sources":["../../src/contexts/docker.ts"],"mappings":";;;;;;;;;;cAwBa,oBAAA;AAAA,iBAmBG,mBAAA,CAAoB,MAAA,GAAS,WAAA,GAAc,gBAAgB;;iBAwiB3D,cAAA,CAAe,MAAA,EAAQ,WAAW,CAAC,WAAA"}
@@ -1,5 +1,6 @@
1
- import { i as globFilesViaShell, n as alwaysQuote, t as fileSizeViaExec } from "../exec-file-size-PXkr-MGA.js";
1
+ import { n as globFilesViaShell, t as fileSizeViaExec } from "../exec-file-size-BRpoB6Io.js";
2
2
  import { a as outputExceededMessage, n as appendCapped, t as DEFAULT_MAX_OUTPUT_BUFFER } from "../output-cap-C9zukcCj.js";
3
+ import { t as alwaysQuote } from "../shell-quote-BmnhZmdM.js";
3
4
  import { Buffer } from "node:buffer";
4
5
  //#region src/contexts/docker.ts
5
6
  /**
@@ -168,7 +169,8 @@ function createDockerContext(config) {
168
169
  filesystem: true,
169
170
  glob: true,
170
171
  network: true,
171
- gpu: false
172
+ gpu: false,
173
+ stream: true
172
174
  },
173
175
  async spawn(overrides) {
174
176
  const docker = await getDockerode();
@@ -260,6 +262,87 @@ function createDockerContext(config) {
260
262
  async exec(handle, command, options) {
261
263
  return execCapped(handle, command, options, DEFAULT_MAX_OUTPUT_BUFFER);
262
264
  },
265
+ /**
266
+ * Live bidirectional stream to a process in the container — see
267
+ * `ExecutionContext.execStream` for the seam contract.
268
+ *
269
+ * Spawns the `docker exec -i` CLI (see the runtime note in the body for
270
+ * why dockerode's hijack API is off the table), which adds a requirement
271
+ * the rest of this context doesn't have: the docker CLI must be on
272
+ * PATH — everything else here talks straight to the daemon socket.
273
+ * Deliberately NOT wrapped in coreutils `timeout` the way `execCapped`
274
+ * is — this seam's whole purpose is processes that outlive a single
275
+ * request.
276
+ */
277
+ async execStream(handle, command, options) {
278
+ const ref = containers.get(handle.id);
279
+ if (!ref) throw new Error(`Container ${handle.id} not found`);
280
+ options?.signal?.throwIfAborted();
281
+ const spawnChild = (await import("node:child_process")).spawn;
282
+ const { randomBytes } = await import("node:crypto");
283
+ const pidFile = `/tmp/.zidane-stream-${randomBytes(6).toString("hex")}.pid`;
284
+ const envPrefix = Object.entries(options?.env ?? {}).map(([k, v]) => `${k}=${alwaysQuote(v)} `).join("");
285
+ const quotedLeader = alwaysQuote(`echo $$ > ${pidFile}; exec ${envPrefix ? `env ${envPrefix}` : ""}sh -c ${alwaysQuote(command)}`);
286
+ const inner = `if setsid -w true 2>/dev/null; then exec setsid -w sh -c ${quotedLeader}; else exec sh -c ${quotedLeader}; fi`;
287
+ const child = spawnChild("docker", [
288
+ "exec",
289
+ "-i",
290
+ "-w",
291
+ options?.cwd ?? handle.cwd,
292
+ ref.container.id,
293
+ "sh",
294
+ "-c",
295
+ inner
296
+ ], { stdio: [
297
+ "pipe",
298
+ "pipe",
299
+ "pipe"
300
+ ] });
301
+ let spawnError = null;
302
+ child.on("error", (err) => {
303
+ spawnError = err;
304
+ });
305
+ const exited = new Promise((resolveExit) => {
306
+ child.once("close", (code, signalName) => {
307
+ resolveExit(code ?? (signalName === "SIGTERM" ? 143 : signalName ? 128 : 1));
308
+ });
309
+ });
310
+ let pid = -1;
311
+ for (let attempt = 0; attempt < 20 && pid <= 0; attempt++) {
312
+ const probe = await execCapped(handle, `cat ${pidFile} 2>/dev/null || true`, void 0, 4096).catch(() => null);
313
+ const parsed = Number.parseInt(probe?.stdout.trim() ?? "", 10);
314
+ if (Number.isFinite(parsed) && parsed > 0) {
315
+ pid = parsed;
316
+ break;
317
+ }
318
+ if (child.exitCode !== null) break;
319
+ await new Promise((r) => setTimeout(r, 50));
320
+ }
321
+ const kill = async () => {
322
+ if (pid > 0) await execCapped(handle, `kill -TERM -${pid} 2>/dev/null || kill -TERM ${pid} 2>/dev/null || true`, void 0, 4096).catch(() => {});
323
+ await execCapped(handle, `rm -f ${pidFile}`, void 0, 4096).catch(() => {});
324
+ if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
325
+ };
326
+ if (options?.signal) if (options.signal.aborted) kill();
327
+ else options.signal.addEventListener("abort", () => {
328
+ kill();
329
+ }, { once: true });
330
+ return {
331
+ pid,
332
+ stdout: child.stdout,
333
+ stderr: child.stderr,
334
+ write: async (data) => {
335
+ if (spawnError) throw spawnError;
336
+ const stdin = child.stdin;
337
+ if (!stdin || stdin.destroyed) throw new Error(`execStream: stdin is closed (container ${handle.id})`);
338
+ await new Promise((resolveWrite, rejectWrite) => {
339
+ stdin.write(data, (err) => err ? rejectWrite(err) : resolveWrite());
340
+ });
341
+ },
342
+ kill,
343
+ exited
344
+ };
345
+ },
263
346
  async fileSize(handle, path) {
264
347
  return fileSizeViaExec((command) => execCapped(handle, command, void 0, DEFAULT_MAX_OUTPUT_BUFFER), path);
265
348
  },
@@ -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 // `-p` suffixes directories with `/` — parity with the process\n // context's dirent-aware listing.\n const result = await ctx.exec(handle, `ls -1p ${alwaysQuote(path)}`)\n // Surface `ls` stderr instead of masking failures as an empty dir —\n // the `list_files` tool differentiates \"No such file\" / \"Not a\n // directory\" / \"Permission denied\" from its catch handler.\n if (result.exitCode !== 0)\n throw new Error(result.stderr.trim() || `ls failed with exit ${result.exitCode}`)\n return result.stdout.trim().split('\\n').filter(Boolean)\n },\n\n async getMappedPort(handle: ExecutionHandle, containerPort: number): Promise<number | null> {\n const ref = containers.get(handle.id)\n if (!ref)\n return null\n\n const info = await ref.container.inspect()\n const bindings = info?.NetworkSettings?.Ports ?? {}\n\n for (const proto of ['tcp', 'udp']) {\n const list = bindings[`${containerPort}/${proto}`]\n if (Array.isArray(list) && list.length > 0 && list[0]?.HostPort) {\n return Number.parseInt(list[0].HostPort, 10)\n }\n }\n\n return null\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n const ref = containers.get(handle.id)\n if (!ref)\n return\n\n try {\n try {\n await ref.container.stop({ t: 5 })\n }\n catch {\n // 304 (already stopped) / 404 (already gone) are expected here.\n // Any other stop failure must not prevent the remove({force})\n // below — force removal also works on running containers.\n }\n try {\n await ref.container.remove({ force: true })\n }\n catch (err) {\n if (!isGoneOrAlreadyStopped(err))\n throw err\n }\n }\n finally {\n containers.delete(handle.id)\n }\n },\n }\n\n return ctx\n}\n\n/**\n * Format one {@link ContextMount} as a dockerode `HostConfig.Binds` entry.\n * Supports read-only (`:ro`) and SELinux shared-label (`:z`) options; the two\n * are mutually exclusive (a read-only mount can't also be a shared rw mount).\n */\nfunction formatBindMount(mount: NonNullable<SpawnConfig['mounts']>[number]): string {\n if (!mount.source)\n throw new Error('Docker mount source is required')\n if (!mount.target || !mount.target.startsWith('/'))\n throw new Error(`Docker mount target must be an absolute path: ${mount.target}`)\n if (mount.readonly && mount.shared)\n throw new Error(`Docker mount cannot be both readonly and shared: ${mount.target}`)\n const opts = mount.readonly ? ':ro' : mount.shared ? ':z' : ''\n return `${mount.source}:${mount.target}${opts}`\n}\n\n/** Assemble dockerode `HostConfig.Binds` from the context's `mounts`. */\nexport function buildBindSpecs(mounts: NonNullable<SpawnConfig['mounts']>): string[] {\n return mounts.map(formatBindMount)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"docker.js","names":[],"sources":["../../src/contexts/docker.ts"],"sourcesContent":["/**\n * Docker execution context.\n *\n * Runs tools inside a Docker container via dockerode.\n * Full isolation with configurable resource limits.\n *\n * Requires `dockerode` as an optional peer dependency.\n */\n\nimport type { CappedSlot } from './output-cap'\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, ListFilesEntry, SpawnConfig, StreamHandle } from './types'\nimport { Buffer } from 'node:buffer'\nimport { alwaysQuote } from '../tools/shell-quote'\nimport { fileSizeViaExec } from './exec-file-size'\nimport { globFilesViaShell } from './glob'\nimport { appendCapped, DEFAULT_MAX_OUTPUT_BUFFER, outputExceededMessage } from './output-cap'\n\n/**\n * `readFile` transport cap: headroom above the whole-read tool ceiling\n * (256MB in tools/whole-read-guard.ts) so tool policy — not the docker\n * transport — decides read limits, keeping refusal behavior consistent\n * across contexts. Still bounded: a probe-failure path can't buffer\n * more than this into host memory.\n */\nexport const READ_FILE_MAX_BUFFER = 512 * 1024 * 1024\n\nconst SINGLE_QUOTE_RE = /'/g\n\n/**\n * dockerode surfaces HTTP errors with a `statusCode`. 304 = container\n * already stopped, 404 = already gone — both are fine during teardown.\n */\nfunction isGoneOrAlreadyStopped(err: unknown): boolean {\n const code = (err as { statusCode?: number } | null)?.statusCode\n return code === 304 || code === 404\n}\n\ninterface ContainerRef {\n handle: ExecutionHandle\n container: any\n docker: any\n}\n\nexport function createDockerContext(config?: SpawnConfig): ExecutionContext {\n let counter = 0\n const containers = new Map<string, ContainerRef>()\n const defaultImage = config?.image ?? 'oven/bun:latest'\n const defaultCwd = config?.cwd ?? '/workspace'\n const defaultEnv = config?.env\n const defaultLimits = config?.limits\n const defaultMounts = config?.mounts ?? []\n const defaultName = config?.name\n\n async function getDockerode() {\n try {\n const Dockerode = (await import('dockerode')).default\n return new Dockerode()\n }\n catch {\n throw new Error('dockerode is required for Docker execution context. Install it with: bun add dockerode')\n }\n }\n\n /**\n * The exec transport with an explicit output cap. The public `exec`\n * uses the shared 10MB default; `readFile` passes headroom above the\n * whole-read tool ceiling so docker's EFFECTIVE read limit matches\n * every other context (the fileSize probe refuses oversized files\n * before this ever runs — the transport cap is the safety net for\n * probe-failure paths, not the primary bound).\n */\n async function execCapped(handle: ExecutionHandle, command: string, options: { cwd?: string, env?: Record<string, string>, timeout?: number } | undefined, maxBuffer: number): Promise<ExecResult> {\n const ref = containers.get(handle.id)\n if (!ref)\n throw new Error(`Container ${handle.id} not found`)\n\n const execCwd = options?.cwd ?? handle.cwd\n const env = options?.env\n ? Object.entries(options.env).map(([k, v]) => `${k}=${v}`)\n : []\n\n const timeout = options?.timeout ?? defaultLimits?.timeout ?? 30\n\n const exec = await ref.container.exec({\n // Wrap with coreutils `timeout` so a runaway command is killed\n // server-side too — the host-side timer below only detaches the\n // stream; without this the process would keep running in the\n // container until destroy(). Exec-array form avoids re-quoting the\n // command. `timeout` exits 124 on expiry, matching the host\n // sentinel. (Default image oven/bun is Debian-based → coreutils.)\n Cmd: ['timeout', String(timeout), 'sh', '-c', command],\n WorkingDir: execCwd,\n Env: env,\n AttachStdout: true,\n AttachStderr: true,\n })\n\n const stream = await exec.start({ Detach: false })\n\n return new Promise<ExecResult>((resolve) => {\n // Byte-capped accumulation via the shared exec-transport primitive\n // (contexts/output-cap.ts) — the docker `readFile` is `cat` through\n // this very path, so an uncapped sink would buffer a container's\n // multi-GB file straight into HOST memory. On overflow the stream\n // is destroyed and the exec resolves exit 143 with a message\n // (mirroring the process context), so `readFile` THROWS instead of\n // silently returning truncated content — silent truncation would\n // corrupt `edit`'s read-modify-write into data loss.\n const stdoutSlot: CappedSlot = { text: '', bytes: 0 }\n const stderrSlot: CappedSlot = { text: '', bytes: 0 }\n let resolved = false\n\n const overflow = (): void => {\n if (resolved)\n return\n resolved = true\n try {\n stream.destroy?.()\n }\n catch { /* swallow */ }\n resolve({\n stdout: stdoutSlot.text,\n stderr: `${stderrSlot.text ? `${stderrSlot.text}\\n` : ''}${outputExceededMessage(maxBuffer, 'stream detached (server-side timeout reaps the process)')}`,\n exitCode: 143,\n })\n }\n\n const cappedWrite = (slot: CappedSlot) => (chunk: Buffer | string): boolean => {\n const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf-8') : chunk\n if (appendCapped(slot, buf, maxBuffer) !== 'ok')\n overflow()\n return true\n }\n\n // Docker's exec stream is a multiplexed protocol — a single byte\n // stream that interleaves stdout/stderr frames. `demuxStream`\n // splits the two onto separate writable sinks. Without it, every\n // byte landed on `stdout` and `stderr` was permanently empty.\n const stdoutSink = {\n write: cappedWrite(stdoutSlot),\n end() {},\n on() {},\n once() {},\n emit() { return true },\n }\n const stderrSink = {\n write: cappedWrite(stderrSlot),\n end() {},\n on() {},\n once() {},\n emit() { return true },\n }\n try {\n ref.docker.modem.demuxStream(stream, stdoutSink, stderrSink)\n }\n catch {\n // Older / different transports may not be multiplexed (TTY mode);\n // fall back to a raw stdout reader so we don't lose output entirely.\n // Same cap: TTY mode is still a host-memory sink.\n stream.on('data', (chunk: Buffer) => {\n cappedWrite(stdoutSlot)(chunk)\n })\n }\n\n // Host-side fallback timer. The server-side `timeout` wrapper is\n // the primary kill mechanism; give it a grace second so the\n // stream-end path (which carries the real exit code) usually wins.\n const timer = setTimeout(() => {\n if (resolved)\n return\n resolved = true\n // Best-effort: detach the stream so we stop accumulating bytes.\n // The server-side `timeout` wrapper kills the process itself.\n try { stream.destroy?.() }\n catch { /* swallow */ }\n resolve({ stdout: stdoutSlot.text, stderr: stderrSlot.text ? `${stderrSlot.text}\\n[timeout]` : '[timeout]', exitCode: 124 })\n }, timeout * 1000 + 1000)\n\n stream.on('end', async () => {\n if (resolved)\n return\n resolved = true\n clearTimeout(timer)\n let exitCode: number\n let inspectNote = ''\n try {\n const inspect = await exec.inspect()\n // `ExitCode: null` means Docker hasn't recorded an exit yet —\n // the outcome is unknown, not success. Use a sentinel so\n // callers don't mistake it for a clean exit.\n if (inspect.ExitCode == null) {\n exitCode = -1\n inspectNote = '[exit code unavailable: exec still marked running by daemon]'\n }\n else {\n exitCode = inspect.ExitCode\n }\n }\n catch (err) {\n exitCode = -1\n inspectNote = `[exit code unavailable: exec inspect failed: ${(err as Error)?.message ?? String(err)}]`\n }\n resolve({\n stdout: stdoutSlot.text,\n stderr: inspectNote ? (stderrSlot.text ? `${stderrSlot.text}\\n${inspectNote}` : inspectNote) : stderrSlot.text,\n exitCode,\n })\n })\n\n stream.on('error', (err: Error) => {\n if (resolved)\n return\n resolved = true\n clearTimeout(timer)\n resolve({ stdout: stdoutSlot.text, stderr: stderrSlot.text ? `${stderrSlot.text}\\n${err.message}` : err.message, exitCode: 1 })\n })\n })\n }\n\n const ctx: ExecutionContext = {\n type: 'docker',\n\n capabilities: {\n shell: true,\n filesystem: true,\n glob: true,\n network: true,\n gpu: false,\n stream: true,\n } satisfies ContextCapabilities,\n\n async spawn(overrides?: SpawnConfig): Promise<ExecutionHandle> {\n const docker = await getDockerode()\n const id = `docker-${++counter}`\n const image = overrides?.image ?? defaultImage\n const cwd = overrides?.cwd ?? defaultCwd\n const mounts = [...defaultMounts, ...(overrides?.mounts ?? [])]\n const name = overrides?.name ?? defaultName\n\n // Pull image if not available\n try {\n await docker.getImage(image).inspect()\n }\n catch {\n await new Promise<void>((resolve, reject) => {\n docker.pull(image, (err: Error | null, stream: NodeJS.ReadableStream) => {\n if (err)\n return reject(err)\n docker.modem.followProgress(stream, (err2: Error | null) => {\n err2 ? reject(err2) : resolve()\n })\n })\n })\n }\n\n const limits = { ...defaultLimits, ...overrides?.limits }\n const hostConfig: Record<string, unknown> = {}\n\n if (limits?.memory) {\n hostConfig.Memory = limits.memory * 1024 * 1024\n }\n if (limits?.cpu) {\n hostConfig.NanoCpus = Number.parseFloat(limits.cpu) * 1e9\n }\n\n // Opt-in hardening (all OFF by default — see ContextHardening).\n const hardening = { ...config?.hardening, ...overrides?.hardening }\n if (hardening.dropAllCapabilities)\n hostConfig.CapDrop = ['ALL']\n if (hardening.noNewPrivileges)\n hostConfig.SecurityOpt = ['no-new-privileges']\n if (hardening.readonlyRootfs)\n hostConfig.ReadonlyRootfs = true\n if (hardening.pidsLimit != null)\n hostConfig.PidsLimit = hardening.pidsLimit\n\n const env = { ...defaultEnv, ...overrides?.env }\n\n const bindSpecs = buildBindSpecs(mounts)\n if (bindSpecs.length > 0) {\n hostConfig.Binds = bindSpecs\n }\n\n const portSpec = overrides?.ports ?? config?.ports\n const exposedPorts: Record<string, Record<string, never>> = {}\n const portBindings: Record<string, Array<{ HostPort: string }>> = {}\n\n if (portSpec?.length) {\n for (const p of portSpec) {\n const key = `${p.container}/${p.proto ?? 'tcp'}`\n exposedPorts[key] = {}\n portBindings[key] = [{ HostPort: p.host == null ? '' : String(p.host) }]\n }\n hostConfig.PortBindings = portBindings\n }\n\n const user = overrides?.user ?? config?.user\n const network = overrides?.network ?? config?.network\n if (network) {\n hostConfig.NetworkMode = network\n }\n\n const labels = { ...config?.labels, ...overrides?.labels }\n\n const container = await docker.createContainer({\n // Suffix with a short random token so a crashed run's leftover container\n // (same prefix) never blocks a fresh one with HTTP 409 name conflict.\n ...(name ? { name: `${name}-${counter}-${Math.random().toString(36).slice(2, 8)}` } : {}),\n Image: image,\n Cmd: ['sleep', 'infinity'],\n WorkingDir: cwd,\n Env: Object.entries(env).map(([k, v]) => `${k}=${v}`),\n HostConfig: hostConfig,\n ...(Object.keys(exposedPorts).length ? { ExposedPorts: exposedPorts } : {}),\n ...(user ? { User: user } : {}),\n ...(Object.keys(labels).length ? { Labels: labels } : {}),\n })\n\n try {\n await container.start()\n }\n catch (err) {\n // The container was created but never started — remove it so a\n // failed spawn doesn't leak a stopped container on the daemon.\n await container.remove({ force: true }).catch(() => {})\n throw err\n }\n\n const handle: ExecutionHandle = { id, type: 'docker', cwd }\n containers.set(id, { handle, container, docker })\n\n return handle\n },\n\n async exec(handle: ExecutionHandle, command: string, options?: { cwd?: string, env?: Record<string, string>, timeout?: number }): Promise<ExecResult> {\n return execCapped(handle, command, options, DEFAULT_MAX_OUTPUT_BUFFER)\n },\n\n /**\n * Live bidirectional stream to a process in the container — see\n * `ExecutionContext.execStream` for the seam contract.\n *\n * Spawns the `docker exec -i` CLI (see the runtime note in the body for\n * why dockerode's hijack API is off the table), which adds a requirement\n * the rest of this context doesn't have: the docker CLI must be on\n * PATH — everything else here talks straight to the daemon socket.\n * Deliberately NOT wrapped in coreutils `timeout` the way `execCapped`\n * is — this seam's whole purpose is processes that outlive a single\n * request.\n */\n async execStream(\n handle: ExecutionHandle,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, signal?: AbortSignal },\n ): Promise<StreamHandle> {\n const ref = containers.get(handle.id)\n if (!ref)\n throw new Error(`Container ${handle.id} not found`)\n\n options?.signal?.throwIfAborted()\n\n // Deliberately the `docker exec -i` CLI rather than dockerode, which\n // every other method here uses. Dockerode's hijacked exec (the only\n // way to get a writable stdin out of the HTTP API) rides on\n // docker-modem's HTTP 101 connection-upgrade handling, and that does\n // not work under Bun: the modem either reports the 101 as an\n // unexpected status or its start callback never fires at all. Same\n // code path is fine under Node, so this is a runtime incompatibility\n // rather than a misuse. The CLI gives real OS pipes, split streams,\n // and identical behaviour on both runtimes; it honours DOCKER_HOST\n // and friends, so remote daemons still work.\n const spawnChild = (await import('node:child_process')).spawn\n const { randomBytes } = await import('node:crypto')\n\n // `docker exec` does not propagate a signal to the in-container\n // process when the CLI dies, so killing the local child would leave\n // the server running in the container. Park the real PID in a file\n // and signal it from a second exec. A file rather than a stdout\n // marker, because stdout here is a protocol stream that must not\n // carry our bookkeeping.\n //\n // `setsid -w` puts the command in a fresh session: the inner shell\n // calls setsid() so its `$` IS the process-group id, and signalling\n // `-$pid` (see `kill` below) reaches the whole tree. Without a fresh group,\n // signalling the recorded pid alone hits only the sh that exec'd\n // into the server — wrapper launchers like `npx` fork the real\n // server as a child, which would survive the kill and pile up in a\n // long-lived container across MCP reconnects.\n //\n // The `-w` matters: bare `setsid` forks and its parent exits\n // immediately, so `docker exec` sees its process die and tears the\n // stream down (observed: instant exit 0, stdin closed). `set -m`\n // job control is no alternative either — dash turns it off without\n // a tty and reroutes the background job's stdin to /dev/null. The\n // runtime probe covers images whose setsid lacks `-w`: they fall\n // back to no process group, where the pid-only kill fallback below\n // still reaches the direct child.\n const pidFile = `/tmp/.zidane-stream-${randomBytes(6).toString('hex')}.pid`\n const envPrefix = Object.entries(options?.env ?? {})\n .map(([k, v]) => `${k}=${alwaysQuote(v)} `)\n .join('')\n const leaderScript = `echo $$ > ${pidFile}; exec ${envPrefix ? `env ${envPrefix}` : ''}sh -c ${alwaysQuote(command)}`\n const quotedLeader = alwaysQuote(leaderScript)\n const inner = `if setsid -w true 2>/dev/null; then exec setsid -w sh -c ${quotedLeader}; else exec sh -c ${quotedLeader}; fi`\n\n const child = spawnChild('docker', [\n 'exec',\n '-i',\n '-w',\n options?.cwd ?? handle.cwd,\n ref.container.id,\n 'sh',\n '-c',\n inner,\n ], { stdio: ['pipe', 'pipe', 'pipe'] })\n\n let spawnError: Error | null = null\n child.on('error', (err) => { spawnError = err })\n\n const exited = new Promise<number>((resolveExit) => {\n child.once('close', (code, signalName) => {\n resolveExit(code ?? (signalName === 'SIGTERM' ? 143 : signalName ? 128 : 1))\n })\n })\n\n // Read back the in-container PID the wrapper just recorded. The seam\n // promises a PID that is meaningful INSIDE the context, so returning\n // the local CLI's pid would be a lie.\n let pid = -1\n for (let attempt = 0; attempt < 20 && pid <= 0; attempt++) {\n const probe = await execCapped(handle, `cat ${pidFile} 2>/dev/null || true`, undefined, 4096)\n .catch(() => null)\n const parsed = Number.parseInt(probe?.stdout.trim() ?? '', 10)\n if (Number.isFinite(parsed) && parsed > 0) {\n pid = parsed\n break\n }\n if (child.exitCode !== null)\n break\n await new Promise(r => setTimeout(r, 50))\n }\n\n const kill = async (): Promise<void> => {\n if (pid > 0) {\n // Group first (`setsid -w` above makes `pid` the PGID), pid-only\n // as a fallback for images that took the no-setsid launch branch.\n // No `--` before the negative pid: dash's kill builtin rejects it\n // (\"Illegal number: -\"), while both dash and busybox ash accept\n // the bare `-pgid` after an explicit signal (verified on oven/bun\n // and alpine).\n await execCapped(handle, `kill -TERM -${pid} 2>/dev/null || kill -TERM ${pid} 2>/dev/null || true`, undefined, 4096)\n .catch(() => {})\n }\n await execCapped(handle, `rm -f ${pidFile}`, undefined, 4096).catch(() => {})\n if (child.exitCode === null && child.signalCode === null)\n child.kill('SIGTERM')\n }\n\n if (options?.signal) {\n if (options.signal.aborted)\n void kill()\n else\n options.signal.addEventListener('abort', () => { void kill() }, { once: true })\n }\n\n return {\n pid,\n stdout: child.stdout!,\n stderr: child.stderr!,\n write: async (data: Uint8Array): Promise<void> => {\n if (spawnError)\n throw spawnError\n const stdin = child.stdin\n if (!stdin || stdin.destroyed)\n throw new Error(`execStream: stdin is closed (container ${handle.id})`)\n await new Promise<void>((resolveWrite, rejectWrite) => {\n stdin.write(data, err => err ? rejectWrite(err) : resolveWrite())\n })\n },\n kill,\n exited,\n }\n },\n\n async fileSize(handle: ExecutionHandle, path: string): Promise<number | null> {\n // Shared exec-probed capability (see exec-file-size.ts) — the same\n // implementation as the sandbox contexts, for a CONSISTENT\n // whole-read experience: tools refuse >ceiling files with the same\n // message everywhere, instead of docker surfacing a transport\n // error at a different threshold.\n return fileSizeViaExec(command => execCapped(handle, command, undefined, DEFAULT_MAX_OUTPUT_BUFFER), path)\n },\n\n // Paths are model-supplied — single-quote them (`alwaysQuote`) rather\n // than JSON.stringify: `$(…)` and backticks still expand inside the\n // double quotes JSON produces, which was a command-injection vector.\n async readFile(handle: ExecutionHandle, path: string): Promise<string> {\n // Transport headroom ABOVE the whole-read tool ceiling (256MB,\n // enforced via the fileSize probe BEFORE this runs): without it,\n // docker's effective read limit would be the 10MB exec default —\n // inconsistent with every other context and surfaced as a\n // confusing transport error instead of the uniform refusal.\n const result = await execCapped(handle, `cat ${alwaysQuote(path)}`, undefined, READ_FILE_MAX_BUFFER)\n if (result.exitCode !== 0)\n throw new Error(`Failed to read file: ${result.stderr}`)\n return result.stdout\n },\n\n async writeFile(handle: ExecutionHandle, path: string, content: string): Promise<void> {\n const escaped = content.replace(SINGLE_QUOTE_RE, String.raw`'\\''`)\n const result = await ctx.exec(handle, `mkdir -p \"$(dirname ${alwaysQuote(path)})\" && printf '%s' '${escaped}' > ${alwaysQuote(path)}`)\n if (result.exitCode !== 0)\n throw new Error(`Failed to write file: ${result.stderr}`)\n },\n\n async listFiles(handle: ExecutionHandle, path: string, options): Promise<ListFilesEntry[]> {\n if (options?.glob)\n return globFilesViaShell(path, command => ctx.exec(handle, command), options?.limit ?? 1000)\n // `-p` suffixes directories with `/` — parity with the process\n // context's dirent-aware listing.\n const result = await ctx.exec(handle, `ls -1p ${alwaysQuote(path)}`)\n // Surface `ls` stderr instead of masking failures as an empty dir —\n // the `list_files` tool differentiates \"No such file\" / \"Not a\n // directory\" / \"Permission denied\" from its catch handler.\n if (result.exitCode !== 0)\n throw new Error(result.stderr.trim() || `ls failed with exit ${result.exitCode}`)\n return result.stdout.trim().split('\\n').filter(Boolean)\n },\n\n async getMappedPort(handle: ExecutionHandle, containerPort: number): Promise<number | null> {\n const ref = containers.get(handle.id)\n if (!ref)\n return null\n\n const info = await ref.container.inspect()\n const bindings = info?.NetworkSettings?.Ports ?? {}\n\n for (const proto of ['tcp', 'udp']) {\n const list = bindings[`${containerPort}/${proto}`]\n if (Array.isArray(list) && list.length > 0 && list[0]?.HostPort) {\n return Number.parseInt(list[0].HostPort, 10)\n }\n }\n\n return null\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n const ref = containers.get(handle.id)\n if (!ref)\n return\n\n try {\n try {\n await ref.container.stop({ t: 5 })\n }\n catch {\n // 304 (already stopped) / 404 (already gone) are expected here.\n // Any other stop failure must not prevent the remove({force})\n // below — force removal also works on running containers.\n }\n try {\n await ref.container.remove({ force: true })\n }\n catch (err) {\n if (!isGoneOrAlreadyStopped(err))\n throw err\n }\n }\n finally {\n containers.delete(handle.id)\n }\n },\n }\n\n return ctx\n}\n\n/**\n * Format one {@link ContextMount} as a dockerode `HostConfig.Binds` entry.\n * Supports read-only (`:ro`) and SELinux shared-label (`:z`) options; the two\n * are mutually exclusive (a read-only mount can't also be a shared rw mount).\n */\nfunction formatBindMount(mount: NonNullable<SpawnConfig['mounts']>[number]): string {\n if (!mount.source)\n throw new Error('Docker mount source is required')\n if (!mount.target || !mount.target.startsWith('/'))\n throw new Error(`Docker mount target must be an absolute path: ${mount.target}`)\n if (mount.readonly && mount.shared)\n throw new Error(`Docker mount cannot be both readonly and shared: ${mount.target}`)\n const opts = mount.readonly ? ':ro' : mount.shared ? ':z' : ''\n return `${mount.source}:${mount.target}${opts}`\n}\n\n/** Assemble dockerode `HostConfig.Binds` from the context's `mounts`. */\nexport function buildBindSpecs(mounts: NonNullable<SpawnConfig['mounts']>): string[] {\n return mounts.map(formatBindMount)\n}\n"],"mappings":"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"}