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.
- package/README.md +16 -5
- package/dist/{acp-DZNCQaTQ.js → acp-Cs86McDR.js} +6 -6
- package/dist/{acp-DZNCQaTQ.js.map → acp-Cs86McDR.js.map} +1 -1
- package/dist/acp-cli.js +7 -7
- package/dist/acp.d.ts +2 -2
- package/dist/acp.js +1 -1
- package/dist/{agent-CrPwqpLI.js → agent-AZ_crMfB.js} +168 -23
- package/dist/agent-AZ_crMfB.js.map +1 -0
- package/dist/agent.d.ts +2 -2
- package/dist/agent.js +4 -4
- package/dist/{anthropic-asnZVFS_.js → anthropic-CWwySM3p.js} +59 -24
- package/dist/anthropic-CWwySM3p.js.map +1 -0
- package/dist/{auth-C5YTniaq.js → auth-BPSnotuN.js} +5 -5
- package/dist/auth-BPSnotuN.js.map +1 -0
- package/dist/{cache-telemetry-BhTryxFT.js → cache-telemetry-DzyfqSds.js} +87 -5
- package/dist/cache-telemetry-DzyfqSds.js.map +1 -0
- package/dist/chat/pure.d.ts +3 -3
- package/dist/chat.d.ts +43 -5
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +31 -3
- package/dist/chat.js.map +1 -1
- package/dist/context-transport-64EeXTAQ.js +84 -0
- package/dist/context-transport-64EeXTAQ.js.map +1 -0
- package/dist/contexts/daytona.d.ts +33 -4
- package/dist/contexts/daytona.d.ts.map +1 -1
- package/dist/contexts/daytona.js +136 -0
- package/dist/contexts/daytona.js.map +1 -1
- package/dist/contexts/docker.d.ts +1 -1
- package/dist/contexts/docker.d.ts.map +1 -1
- package/dist/contexts/docker.js +85 -2
- package/dist/contexts/docker.js.map +1 -1
- package/dist/contexts/e2b.d.ts +34 -4
- package/dist/contexts/e2b.d.ts.map +1 -1
- package/dist/contexts/e2b.js +92 -0
- package/dist/contexts/e2b.js.map +1 -1
- package/dist/contexts/sandbox.d.ts +1 -1
- package/dist/contexts/sandbox.js +4 -2
- package/dist/contexts/sandbox.js.map +1 -1
- package/dist/{contexts-o8y5B3Tm.js → contexts-DbMuUPjS.js} +92 -5
- package/dist/contexts-DbMuUPjS.js.map +1 -0
- package/dist/contexts.d.ts +4 -4
- package/dist/contexts.js +1 -1
- package/dist/eval.d.ts +1 -1
- package/dist/eval.js +3 -3
- package/dist/{exec-file-size-PXkr-MGA.js → exec-file-size-BRpoB6Io.js} +3 -32
- package/dist/exec-file-size-BRpoB6Io.js.map +1 -0
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.js +1 -1
- package/dist/headless.d.ts +1 -1
- package/dist/headless.js +4 -4
- package/dist/{index-DsPQbyUn.d.ts → index-B9jSo84a.d.ts} +2 -2
- package/dist/index-B9jSo84a.d.ts.map +1 -0
- package/dist/{index-CohO0oaX.d.ts → index-D-PvuMCb.d.ts} +5607 -5188
- package/dist/index-D-PvuMCb.d.ts.map +1 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +13 -13
- package/dist/{login-CWwu9Sex.js → login-fgr9zD6z.js} +2 -2
- package/dist/{login-CWwu9Sex.js.map → login-fgr9zD6z.js.map} +1 -1
- package/dist/{mcp-Ct5tF4oj.js → mcp-DolMi7WV.js} +77 -34
- package/dist/mcp-DolMi7WV.js.map +1 -0
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/{messages-Cpb4g_KV.js → messages-CLkUdPeG.js} +2 -2
- package/dist/{messages-Cpb4g_KV.js.map → messages-CLkUdPeG.js.map} +1 -1
- package/dist/{oauth-CBi9eymX.js → oauth-CuFPhsHY.js} +117 -11
- package/dist/oauth-CuFPhsHY.js.map +1 -0
- package/dist/{openai-compat-C77gOaHh.js → openai-compat-DHlBIjj9.js} +186 -42
- package/dist/openai-compat-DHlBIjj9.js.map +1 -0
- package/dist/output/stream-json.d.ts +1 -1
- package/dist/output/terminal.d.ts +1 -1
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/prompt.d.ts +1 -1
- package/dist/prompt.js +14 -0
- package/dist/prompt.js.map +1 -1
- package/dist/providers/anthropic.d.ts +2 -2
- package/dist/providers/anthropic.js +2 -2
- package/dist/providers/arcee.d.ts +1 -1
- package/dist/providers/arcee.js +1 -1
- package/dist/providers/baseten.d.ts +1 -1
- package/dist/providers/baseten.js +1 -1
- package/dist/providers/cerebras.d.ts +1 -1
- package/dist/providers/cerebras.js +1 -1
- package/dist/providers/local.d.ts +1 -1
- package/dist/providers/local.js +1 -1
- package/dist/providers/openai-compat.d.ts +1 -1
- package/dist/providers/openai-compat.js +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.js +5 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/openrouter.d.ts +1 -1
- package/dist/providers/openrouter.js +1 -1
- package/dist/providers/xai.d.ts +1 -1
- package/dist/providers/xai.js +1 -1
- package/dist/{providers-DnHPW-dL.js → providers-Nu5ysnXN.js} +4 -4
- package/dist/providers-Nu5ysnXN.js.map +1 -0
- package/dist/providers.d.ts +2 -2
- package/dist/providers.js +5 -5
- package/dist/{read-state-rDmfeLmz.js → read-state-CNyeMG7W.js} +7 -4
- package/dist/read-state-CNyeMG7W.js.map +1 -0
- package/dist/restate.d.ts +2 -2
- package/dist/restate.js +2 -2
- package/dist/restate.js.map +1 -1
- package/dist/{sandbox-CgjG2VvQ.d.ts → sandbox-DqwpKX18.d.ts} +14 -2
- package/dist/{sandbox-CgjG2VvQ.d.ts.map → sandbox-DqwpKX18.d.ts.map} +1 -1
- package/dist/session/sqlite.d.ts +1 -1
- package/dist/{session-DpDBnktP.js → session-vmy9jTVM.js} +2 -2
- package/dist/{session-DpDBnktP.js.map → session-vmy9jTVM.js.map} +1 -1
- package/dist/session.d.ts +1 -1
- package/dist/session.js +2 -2
- package/dist/shell-quote-BmnhZmdM.js +33 -0
- package/dist/shell-quote-BmnhZmdM.js.map +1 -0
- package/dist/skills.d.ts +1 -1
- package/dist/{stdio-loader-Ce68wUmM.js → stdio-D0v-W5w7.js} +8 -215
- package/dist/stdio-D0v-W5w7.js.map +1 -0
- package/dist/stdio-loader-BPUedHxT.js +217 -0
- package/dist/stdio-loader-BPUedHxT.js.map +1 -0
- package/dist/stream-queue-Cd9OJxl7.js +71 -0
- package/dist/stream-queue-Cd9OJxl7.js.map +1 -0
- package/dist/{system-prompt-BlsdjcUi.d.ts → system-prompt-MNVHatSD.d.ts} +15 -1
- package/dist/system-prompt-MNVHatSD.d.ts.map +1 -0
- package/dist/{tool-formatters-bZYz74qU.d.ts → tool-formatters-BPKA2if5.d.ts} +2 -2
- package/dist/{tool-formatters-bZYz74qU.d.ts.map → tool-formatters-BPKA2if5.d.ts.map} +1 -1
- package/dist/tools/fetch-url.d.ts +1 -1
- package/dist/tools/web-search.d.ts +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +2 -2
- package/dist/{transcript-anchors-ZZYwCTjo.js → transcript-anchors-6-uB2Xvl.js} +19 -8
- package/dist/transcript-anchors-6-uB2Xvl.js.map +1 -0
- package/dist/{transcript-anchors-EBchHUMo.d.ts → transcript-anchors-DzuIcJpm.d.ts} +2 -2
- package/dist/{transcript-anchors-EBchHUMo.d.ts.map → transcript-anchors-DzuIcJpm.d.ts.map} +1 -1
- package/dist/tui.d.ts +3 -3
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +124 -45
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-DygcPreU.d.ts → turn-operations-Dzr3QhDD.d.ts} +3 -3
- package/dist/{turn-operations-DygcPreU.d.ts.map → turn-operations-Dzr3QhDD.d.ts.map} +1 -1
- package/dist/types-BIarq1qE.js.map +1 -1
- package/dist/{types-BDccavwj.d.ts → types-DdZMQxfT.d.ts} +117 -2
- package/dist/types-DdZMQxfT.d.ts.map +1 -0
- package/dist/types.d.ts +3 -3
- package/dist/{xai-B2hIs5We.js → xai-Cn0LsHRt.js} +3 -3
- package/dist/xai-Cn0LsHRt.js.map +1 -0
- package/docs/ARCHITECTURE.md +62 -4
- package/docs/CHAT.md +12 -5
- package/docs/EXTENSIONS.md +1 -1
- package/docs/RESTATE.md +1 -1
- package/docs/SKILL.md +21 -1
- package/docs/TUI.md +2 -0
- package/package.json +2 -2
- package/dist/agent-CrPwqpLI.js.map +0 -1
- package/dist/anthropic-asnZVFS_.js.map +0 -1
- package/dist/auth-C5YTniaq.js.map +0 -1
- package/dist/cache-telemetry-BhTryxFT.js.map +0 -1
- package/dist/contexts-o8y5B3Tm.js.map +0 -1
- package/dist/exec-file-size-PXkr-MGA.js.map +0 -1
- package/dist/index-CohO0oaX.d.ts.map +0 -1
- package/dist/index-DsPQbyUn.d.ts.map +0 -1
- package/dist/mcp-Ct5tF4oj.js.map +0 -1
- package/dist/oauth-CBi9eymX.js.map +0 -1
- package/dist/openai-compat-C77gOaHh.js.map +0 -1
- package/dist/providers-DnHPW-dL.js.map +0 -1
- package/dist/read-state-rDmfeLmz.js.map +0 -1
- package/dist/stdio-loader-Ce68wUmM.js.map +0 -1
- package/dist/system-prompt-BlsdjcUi.d.ts.map +0 -1
- package/dist/transcript-anchors-ZZYwCTjo.js.map +0 -1
- package/dist/types-BDccavwj.d.ts.map +0 -1
- package/dist/xai-B2hIs5We.js.map +0 -1
package/dist/contexts/e2b.d.ts
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { t as SandboxProvider } from "../sandbox-DqwpKX18.js";
|
|
2
|
+
import { lu as Logger } from "../index-D-PvuMCb.js";
|
|
3
3
|
|
|
4
4
|
//#region src/contexts/e2b.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Options that flip `commands.run` into background mode. Split out so the
|
|
7
|
+
* overload above can discriminate on it: `background: true` changes the
|
|
8
|
+
* return type from a finished result to a live handle.
|
|
9
|
+
*
|
|
10
|
+
* `stdin: true` and a huge `timeoutMs` are not incidental — the SDK
|
|
11
|
+
* defaults are `false` and 60s, which would respectively hand the child an
|
|
12
|
+
* immediate EOF and kill a session-lifetime process after a minute. See
|
|
13
|
+
* {@link E2B_STREAM_TIMEOUT_MS} for why the timeout is a large number and
|
|
14
|
+
* not `0`.
|
|
15
|
+
*/
|
|
16
|
+
interface E2BBackgroundOptions {
|
|
17
|
+
background: true;
|
|
18
|
+
stdin: true;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
cwd?: string;
|
|
21
|
+
envs?: Record<string, string>;
|
|
22
|
+
onStdout?: (data: string) => void;
|
|
23
|
+
onStderr?: (data: string) => void;
|
|
24
|
+
}
|
|
5
25
|
interface E2BCommandResult {
|
|
6
26
|
stdout: string;
|
|
7
27
|
stderr: string;
|
|
@@ -12,14 +32,24 @@ interface E2BEntry {
|
|
|
12
32
|
/** Present in the E2B SDK's EntryInfo; used for the `/` dir suffix. */
|
|
13
33
|
type?: string;
|
|
14
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Handle returned by a `background: true` command. Structural subset of the
|
|
37
|
+
* SDK's `CommandHandle` — only what `execStream` needs.
|
|
38
|
+
*/
|
|
39
|
+
interface E2BCommandHandle {
|
|
40
|
+
pid: number;
|
|
41
|
+
wait: () => Promise<E2BCommandResult>;
|
|
42
|
+
}
|
|
15
43
|
interface E2BSandbox {
|
|
16
44
|
sandboxId: string;
|
|
17
45
|
commands: {
|
|
18
|
-
run: (cmd: string, opts?: {
|
|
46
|
+
run: ((cmd: string, opts?: {
|
|
19
47
|
cwd?: string;
|
|
20
48
|
envs?: Record<string, string>;
|
|
21
49
|
timeoutMs?: number;
|
|
22
|
-
}) => Promise<E2BCommandResult
|
|
50
|
+
}) => Promise<E2BCommandResult>) & ((cmd: string, opts: E2BBackgroundOptions) => Promise<E2BCommandHandle>); /** Present from the SDK's Commands module; used only by `execStream`. */
|
|
51
|
+
sendStdin?: (pid: number, data: string | Uint8Array) => Promise<void>;
|
|
52
|
+
kill?: (pid: number) => Promise<boolean>;
|
|
23
53
|
};
|
|
24
54
|
files: {
|
|
25
55
|
read: (path: string) => Promise<string>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"e2b.d.ts","names":[],"sources":["../../src/contexts/e2b.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"e2b.d.ts","names":[],"sources":["../../src/contexts/e2b.ts"],"mappings":";;;;;;;;;;AAuGsC;AAAA;;;;UArC5B,oBAAA;EACR,UAAA;EACA,KAAA;EACA,SAAA;EACA,GAAA;EACA,IAAA,GAAO,MAAM;EACb,QAAA,IAAY,IAAA;EACZ,QAAA,IAAY,IAAA;AAAA;AAAA,UAYJ,gBAAA;EACR,MAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,UAGQ,QAAA;EACR,IAAA;EAkCyB;EAhCzB,IAAI;AAAA;;;;;UAOI,gBAAA;EACR,GAAA;EACA,IAAA,QAAY,OAAO,CAAC,gBAAA;AAAA;AAAA,UAGZ,UAAA;EACR,SAAA;EACA,QAAA;IACE,GAAA,IAAO,GAAA,UAAa,IAAA;MAAS,GAAA;MAAc,IAAA,GAAO,MAAA;MAAwB,SAAA;IAAA,MAAyB,OAAA,CAAQ,gBAAA,OACrG,GAAA,UAAa,IAAA,EAAM,oBAAA,KAAyB,OAAA,CAAQ,gBAAA,IAEjB;IAAzC,SAAA,IAAa,GAAA,UAAa,IAAA,WAAe,UAAA,KAAe,OAAA;IACxD,IAAA,IAAQ,GAAA,aAAgB,OAAA;EAAA;EAE1B,KAAA;IACE,IAAA,GAAO,IAAA,aAAiB,OAAA;IACxB,KAAA,GAAQ,IAAA,UAAc,IAAA,aAAiB,OAAA;IACvC,IAAA,GAAO,IAAA,aAAiB,OAAA,CAAQ,QAAA;EAAA;EAElC,IAAA,QAAY,OAAA;EAKZ,KAAA,SAAc,OAAA;EACd,SAAA,SAAkB,OAAA;AAAA;AAAA,UA0BH,kBAAA;EAlCb;EAoCF,MAAA;EApC0B;;;;;EA0C1B,MAAA;EAlCA;;;AAAyB;AA0B3B;EAcE,QAAA;;;;;;;;;EASA,SAAA;EAgBM;;;;EAXN,GAAA;EAkCY;;;;EA7BZ,cAAA;EA0EoB;;;;;EApEpB,GAAA,GAAM,MAAA;EAwEL;;;;;EAlED,MAAA,GAAS,MAAM;EAgEf;;;;;;;EAxDA,uBAAA;EA0DQ;AAAA;AA8BV;;;;;;EA/EE,OAAA;IAAY,IAAA;IAAc,OAAA;EAAA;EA4FK;AAAA;AAgBjC;;;;;;;;;;;;;;EA3FE,SAAA;AAAA;;AA+FQ;AAmCV;;;;;;;;;AAGuD;iBAzGjC,aAAA,CACpB,OAAA,EAAS,UAAA,EACT,MAAA,EAAQ,MAAA,EACR,IAAA;EAAQ,IAAA;EAAc,OAAA;EAAiB,GAAA;EAAc,IAAA,GAAO,MAAA;AAAA,IAC3D,OAAA;AAAA,UA8Bc,mBAAA;EAqFR;EAnFP,cAAA;EAiFA;;;;;AAEO;EA5EP,aAAA;EA4F+B;EA1F/B,GAAA;EA0FkF;EAxFlF,KAAA,IAAS,EAAA,aAAe,OAAO;AAAA;;;AAwFmD;;;;;;;;;;;iBAxE9D,eAAA,CACpB,OAAA,EAAS,UAAA,EACT,MAAA,EAAQ,MAAA,EACR,IAAA,EAAM,mBAAA,GACL,OAAA;;;;;;;;iBAmCa,kBAAA,CACd,gBAAA,sBACA,cAAA,sBACA,GAAA,GAAK,MAAM;;;;;;;iBAWG,aAAA,CACd,SAAA,EAAW,MAAA,8BACX,WAAA,EAAa,MAAA,+BACZ,MAAA;;;;;;;;iBAgBa,iBAAA,CAAkB,OAAA,GAAS,kBAAA,GAA0B,eAAe"}
|
package/dist/contexts/e2b.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { r as capBufferedExecOutput } from "../output-cap-C9zukcCj.js";
|
|
2
|
+
import { t as alwaysQuote } from "../shell-quote-BmnhZmdM.js";
|
|
2
3
|
import { r as createLogger, t as consoleSink } from "../logger-DBX9uYOw.js";
|
|
4
|
+
import { t as StreamQueue } from "../stream-queue-Cd9OJxl7.js";
|
|
3
5
|
//#region src/contexts/e2b.ts
|
|
6
|
+
/**
|
|
7
|
+
* "Effectively unlimited" command timeout for `execStream`. 2^31-1 ms
|
|
8
|
+
* (~24.8 days — far beyond any sandbox lifetime) because both edges are
|
|
9
|
+
* cliffs: `0` is treated by `@connectrpc/connect`'s deadline signal as
|
|
10
|
+
* already expired (instant DeadlineExceeded), and anything above 2^31-1
|
|
11
|
+
* overflows `setTimeout` and fires immediately.
|
|
12
|
+
*/
|
|
13
|
+
const E2B_STREAM_TIMEOUT_MS = 2 ** 31 - 1;
|
|
4
14
|
/** Per-probe timeout for the readiness `true` command. */
|
|
5
15
|
const READINESS_PROBE_TIMEOUT_MS = 5e3;
|
|
6
16
|
/** Pause between readiness probes while the sandbox is still warming up. */
|
|
@@ -231,6 +241,88 @@ function createE2BProvider(options = {}) {
|
|
|
231
241
|
cwd
|
|
232
242
|
};
|
|
233
243
|
},
|
|
244
|
+
/**
|
|
245
|
+
* Live bidirectional stream — see `ExecutionContext.execStream`.
|
|
246
|
+
*
|
|
247
|
+
* Backed by E2B's pipe-based command API rather than `sandbox.pty`.
|
|
248
|
+
* Both can carry stdin, but a PTY would force us to defeat terminal
|
|
249
|
+
* semantics we never wanted (input echo, `\n`→`\r\n` rewriting, the
|
|
250
|
+
* ~4096-byte canonical line cap, and stdout/stderr merged into one
|
|
251
|
+
* stream). Pipes give split streams and byte-faithful framing for free.
|
|
252
|
+
*
|
|
253
|
+
* KNOWN RESIDUAL: `onStdout` / `onStderr` deliver `string`, already
|
|
254
|
+
* decoded by the SDK. A chunk boundary that splits a multibyte
|
|
255
|
+
* codepoint is therefore corrupted before it reaches us — we re-encode
|
|
256
|
+
* to bytes to satisfy the seam, but cannot recover what the decoder
|
|
257
|
+
* dropped. Newline framing and ASCII structure are unaffected, so
|
|
258
|
+
* JSON-RPC still parses; the damage would be confined to non-ASCII
|
|
259
|
+
* inside string values. If that ever bites, `sandbox.pty.create`
|
|
260
|
+
* hands back raw `Uint8Array` and the Daytona PTY path in
|
|
261
|
+
* `./daytona.ts` is the template for adopting it here.
|
|
262
|
+
*/
|
|
263
|
+
async execStream(sandboxId, command, opts) {
|
|
264
|
+
const { sandbox, env } = get(sandboxId);
|
|
265
|
+
opts?.signal?.throwIfAborted();
|
|
266
|
+
const warnBacklog = (which) => (bytes) => logger.warn("execStream consumer is not draining output", {
|
|
267
|
+
sandboxId,
|
|
268
|
+
stream: which,
|
|
269
|
+
bufferedBytes: bytes
|
|
270
|
+
});
|
|
271
|
+
const stdout = new StreamQueue(warnBacklog("stdout"));
|
|
272
|
+
const stderr = new StreamQueue(warnBacklog("stderr"));
|
|
273
|
+
const pidFile = `/tmp/.zidane-stream-${Math.random().toString(36).slice(2, 10)}.pid`;
|
|
274
|
+
const leader = `echo \$\$ > ${pidFile}; exec sh -c ${alwaysQuote(command)}`;
|
|
275
|
+
const wrapped = `if setsid -w true 2>/dev/null; then exec setsid -w sh -c ${alwaysQuote(leader)}; else exec sh -c ${alwaysQuote(leader)}; fi`;
|
|
276
|
+
const handle = await sandbox.commands.run(wrapped, {
|
|
277
|
+
background: true,
|
|
278
|
+
stdin: true,
|
|
279
|
+
timeoutMs: E2B_STREAM_TIMEOUT_MS,
|
|
280
|
+
...opts?.cwd ? { cwd: opts.cwd } : {},
|
|
281
|
+
envs: resolveE2BEnv(env, opts?.env),
|
|
282
|
+
onStdout: (data) => stdout.push(new TextEncoder().encode(data)),
|
|
283
|
+
onStderr: (data) => stderr.push(new TextEncoder().encode(data))
|
|
284
|
+
});
|
|
285
|
+
const pid = handle.pid;
|
|
286
|
+
const exited = handle.wait().then((result) => result?.exitCode ?? 0).catch((err) => typeof err?.exitCode === "number" ? err.exitCode : 137).finally(() => {
|
|
287
|
+
stdout.close();
|
|
288
|
+
stderr.close();
|
|
289
|
+
});
|
|
290
|
+
let commandExited = false;
|
|
291
|
+
exited.then(() => {
|
|
292
|
+
commandExited = true;
|
|
293
|
+
});
|
|
294
|
+
const readLeaderPid = async () => {
|
|
295
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
296
|
+
const probe = await sandbox.commands.run(`cat ${pidFile} 2>/dev/null || true`).catch(() => null);
|
|
297
|
+
const parsed = Number.parseInt(probe?.stdout?.trim() ?? "", 10);
|
|
298
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
299
|
+
if (commandExited) break;
|
|
300
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
301
|
+
}
|
|
302
|
+
return -1;
|
|
303
|
+
};
|
|
304
|
+
const leaderPid = await readLeaderPid();
|
|
305
|
+
const kill = async () => {
|
|
306
|
+
if (leaderPid > 0) await sandbox.commands.run(`kill -TERM -${leaderPid} 2>/dev/null || kill -TERM ${leaderPid} 2>/dev/null || true`).catch(() => {});
|
|
307
|
+
await sandbox.commands.run(`rm -f ${pidFile}`).catch(() => {});
|
|
308
|
+
await sandbox.commands.kill?.(pid).catch(() => {});
|
|
309
|
+
};
|
|
310
|
+
if (opts?.signal) if (opts.signal.aborted) kill();
|
|
311
|
+
else opts.signal.addEventListener("abort", () => {
|
|
312
|
+
kill();
|
|
313
|
+
}, { once: true });
|
|
314
|
+
return {
|
|
315
|
+
pid,
|
|
316
|
+
stdout,
|
|
317
|
+
stderr,
|
|
318
|
+
write: async (data) => {
|
|
319
|
+
if (!sandbox.commands.sendStdin) throw new Error("E2B SDK is too old: `commands.sendStdin` is required for execStream");
|
|
320
|
+
await sandbox.commands.sendStdin(pid, data);
|
|
321
|
+
},
|
|
322
|
+
kill,
|
|
323
|
+
exited
|
|
324
|
+
};
|
|
325
|
+
},
|
|
234
326
|
async exec(sandboxId, command, opts) {
|
|
235
327
|
const { sandbox, env } = get(sandboxId);
|
|
236
328
|
const timeoutMs = (opts?.timeout ?? 30) * 1e3;
|
package/dist/contexts/e2b.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"e2b.js","names":[],"sources":["../../src/contexts/e2b.ts"],"sourcesContent":["/**\n * E2B sandbox provider.\n *\n * Implements {@link SandboxProvider} on top of E2B's firecracker-backed\n * micro-VMs. Pair it with {@link createSandboxContext} to run an agent's\n * shell + filesystem tools inside an ephemeral E2B sandbox:\n *\n * ```ts\n * import { createSandboxContext } from 'zidane/contexts'\n * import { createE2BProvider } from 'zidane/contexts/e2b'\n *\n * const execution = createSandboxContext(createE2BProvider())\n * ```\n *\n * ## Pointing at your own E2B\n *\n * The two knobs that select *which* E2B you talk to are `apiKey` and\n * `domain`. Self-hosted / on-prem E2B clusters expose a custom domain; pass\n * it here (or via the `E2B_DOMAIN` env var) and the SDK routes every call to\n * your cluster instead of E2B's hosted API. Precedence, highest first:\n *\n * 1. Per-spawn `SpawnConfig.sandbox` fields (`apiKey`, `domain`, `template`)\n * 2. {@link E2BProviderOptions} passed to this factory\n * 3. `E2B_API_KEY` / `E2B_DOMAIN` env vars (read by the SDK itself), and\n * `E2B_TEMPLATE` (read by this provider — the SDK does not read it)\n *\n * The default `base` template does not exist on self-hosted clusters, so\n * picking the right template matters: an invalid template still returns a\n * sandbox id from the control plane, but its envd never boots and every\n * `exec`/file op then hangs until it times out. Set `E2B_TEMPLATE` (or pass\n * `template`) to a template that actually exists on your cluster.\n *\n * Requires `e2b@^2` as an optional peer dependency:\n * `bun add e2b`.\n */\n\nimport type { Logger } from '../logger'\nimport type { SandboxProvider } from './sandbox'\nimport type { ExecResult, SpawnConfig } from './types'\nimport { consoleSink, createLogger } from '../logger'\nimport { capBufferedExecOutput } from './output-cap'\n\n// ---------------------------------------------------------------------------\n// Minimal structural views of the `e2b` SDK surface we touch. Declared locally\n// (rather than imported) so the package typechecks without the optional peer\n// dependency installed — mirroring how `docker.ts` keeps `dockerode` at arm's\n// length. The real SDK satisfies these shapes. The peer dependency floor is\n// `e2b@^2`: every member used here exists across 2.x, but the pause surface\n// shifted within it — `betaPause()` from 2.0.0, the stable `pause()` from\n// 2.27.1 — so `onDestroy: 'pause'` prefers `pause()` and falls back to\n// `betaPause()` (both optional below, resolved at runtime in `destroy`).\n// ---------------------------------------------------------------------------\n\ninterface E2BCommandResult {\n stdout: string\n stderr: string\n exitCode: number\n}\n\ninterface E2BEntry {\n name: string\n /** Present in the E2B SDK's EntryInfo; used for the `/` dir suffix. */\n type?: string\n}\n\ninterface E2BSandbox {\n sandboxId: string\n commands: {\n run: (cmd: string, opts?: { cwd?: string, envs?: Record<string, string>, timeoutMs?: number }) => Promise<E2BCommandResult>\n }\n files: {\n read: (path: string) => Promise<string>\n write: (path: string, data: string) => Promise<unknown>\n list: (path: string) => Promise<E2BEntry[]>\n }\n kill: () => Promise<unknown>\n // The pause surface shifted within 2.x: `betaPause` from 2.0.0, the stable\n // `pause` from 2.27.1. Both are optional so `destroy` can prefer `pause`,\n // fall back to `betaPause`, and stay defensive if a mismatched install\n // exposes neither. Both resolve to whether the sandbox was newly paused.\n pause?: () => Promise<boolean>\n betaPause?: () => Promise<boolean>\n}\n\ninterface E2BCreateOptions {\n apiKey?: string\n domain?: string\n timeoutMs?: number\n envs?: Record<string, string>\n metadata?: Record<string, string>\n}\n\ninterface E2BConnectOptions {\n apiKey?: string\n domain?: string\n timeoutMs?: number\n}\n\ninterface E2BSandboxStatic {\n create: (templateOrOpts?: string | E2BCreateOptions, opts?: E2BCreateOptions) => Promise<E2BSandbox>\n connect: (sandboxId: string, opts?: E2BConnectOptions) => Promise<E2BSandbox>\n}\n\n// ---------------------------------------------------------------------------\n// Provider options\n// ---------------------------------------------------------------------------\n\nexport interface E2BProviderOptions {\n /** E2B API key. Falls back to the `E2B_API_KEY` env var when omitted. */\n apiKey?: string\n /**\n * E2B API domain. Set this to point at a self-hosted / on-prem cluster\n * (e.g. `e2b.my-company.internal`). Falls back to the `E2B_DOMAIN` env var,\n * then to E2B's hosted default.\n */\n domain?: string\n /**\n * Sandbox template id / name to launch. Falls back to the `E2B_TEMPLATE`\n * env var, then to E2B's hosted `base` template. Note that `base` does not\n * exist on self-hosted clusters — supply a template that does.\n */\n template?: string\n /**\n * Connect to a pre-existing sandbox by id instead of creating a fresh one\n * (E2B's `Sandbox.connect`). When set, `template` is ignored — you're\n * attaching to a sandbox that already exists. The provider treats such a\n * sandbox as externally owned: it is NOT killed on `destroy`, nor torn down\n * if the readiness probe / pregame fails, since the caller manages its\n * lifecycle. Per-spawn `SpawnConfig.sandbox.sandboxId` overrides this.\n */\n sandboxId?: string\n /**\n * Default working directory, applied when a spawn doesn't set its own\n * `SpawnConfig.cwd`. Created on spawn. Defaults to E2B's `/home/user`.\n */\n cwd?: string\n /**\n * Sandbox lifetime in seconds before E2B auto-kills it. Defaults to the\n * SDK default (300s). Per-spawn `SpawnConfig.limits.timeout` overrides this.\n */\n timeoutSeconds?: number\n /**\n * Environment variables baked into every sandbox at create time (passed to\n * E2B's `Sandbox.create({ envs })`), so every command run in the sandbox\n * sees them. Per-spawn `SpawnConfig.env` is merged over these.\n */\n env?: Record<string, string>\n /**\n * Logger for provider-level lifecycle lines (notably the readiness wait).\n * Defaults to a {@link consoleSink}-backed logger so the messages reach\n * stderr without the caller wiring one up.\n */\n logger?: Logger\n /**\n * Total deadline, in seconds, for the post-create readiness probe (see\n * {@link waitForE2BReady}). A freshly-created sandbox can return its id\n * before `envd` accepts commands; `spawn` blocks until a trivial probe\n * succeeds so the first real command never absorbs the cold start. Defaults\n * to `60`. Set to `0` to disable the gate.\n */\n readinessTimeoutSeconds?: number\n /**\n * A setup script to upload and run once the sandbox is ready, before any\n * prompting begins (see {@link runE2BPregame}). `name` is the basename used\n * for the uploaded file; `content` is the script body. The script is run\n * directly so its shebang selects the interpreter. A non-zero exit aborts\n * `spawn` (and tears the sandbox down) — a broken setup must not silently\n * hand back a half-provisioned sandbox.\n */\n pregame?: { name: string, content: string }\n /**\n * What to do with an owned sandbox when `destroy` is called:\n *\n * - `'kill'` (default) — call `sandbox.kill()`. The sandbox is deleted and\n * its state is gone.\n * - `'pause'` — call `sandbox.pause()` (falling back to the older\n * `betaPause()` on `e2b` 2.0–2.26). The sandbox's filesystem and process\n * state are preserved; reconnect later with the `sandboxId` option (E2B's\n * `Sandbox.connect` auto-resumes a paused sandbox).\n * - `'leave'` — drop the local handle without making any API call. The\n * sandbox keeps running until E2B's own timeout fires.\n *\n * Has no effect on sandboxes the provider only connected to (the `sandboxId`\n * option) — those are always left untouched on destroy since the caller owns\n * their lifecycle.\n */\n onDestroy?: 'kill' | 'pause' | 'leave'\n}\n\n/** Per-probe timeout for the readiness `true` command. */\nconst READINESS_PROBE_TIMEOUT_MS = 5000\n/** Pause between readiness probes while the sandbox is still warming up. */\nconst READINESS_BACKOFF_MS = 250\n/**\n * Command timeout for the pregame setup script. E2B's `commands.run` defaults\n * to 60s and offers no true \"unlimited\" — a `timeoutMs` of 0 is a 0ms deadline\n * (instant timeout), not \"off\". Setup scripts (`apt`/`pip` installs, builds)\n * routinely exceed 60s, so we pass a deliberately generous 30-minute ceiling.\n * The sandbox's own lifetime independently bounds anything longer.\n */\nconst PREGAME_TIMEOUT_MS = 30 * 60 * 1000\n\n/**\n * Upload a setup (\"pregame\") script into a ready sandbox and execute it.\n *\n * Writes the script to `/tmp/<name>`, then `chmod +x`'s and runs it directly\n * (so a `#!/bin/bash` / `#!/usr/bin/env python3` shebang chooses the\n * interpreter) in `cwd` with `envs` applied — the same env the sandbox was\n * created with, so the script sees the run's `--env` / `--pass-env`. A\n * generous {@link PREGAME_TIMEOUT_MS} ceiling is used so long `apt`/`pip`\n * installs aren't killed by E2B's 60s command default. A non-zero exit\n * (whether E2B returns it or raises on it) throws, so the caller can tear the\n * sandbox down rather than prompt against a broken environment.\n */\nexport async function runE2BPregame(\n sandbox: E2BSandbox,\n logger: Logger,\n opts: { name: string, content: string, cwd?: string, envs?: Record<string, string> },\n): Promise<void> {\n const path = `/tmp/${opts.name}`\n logger.info('running E2B pregame script', { sandboxId: sandbox.sandboxId, path })\n await sandbox.files.write(path, opts.content)\n\n const quoted = JSON.stringify(path)\n let result: E2BCommandResult\n try {\n result = await sandbox.commands.run(`chmod +x ${quoted} && ${quoted}`, {\n cwd: opts.cwd,\n envs: opts.envs,\n timeoutMs: PREGAME_TIMEOUT_MS,\n })\n }\n catch (err: any) {\n // E2B raises CommandExitError (carrying the captured streams) on a\n // non-zero exit. Surface its output, then fail the spawn.\n const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 124\n logger.error('E2B pregame script failed', { sandboxId: sandbox.sandboxId, exitCode, stdout: err?.stdout ?? '', stderr: err?.stderr ?? err?.message ?? '' })\n throw new Error(`E2B pregame script ${opts.name} failed with exit code ${exitCode}`)\n }\n\n if (result.exitCode !== 0) {\n logger.error('E2B pregame script failed', { sandboxId: sandbox.sandboxId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr })\n throw new Error(`E2B pregame script ${opts.name} failed with exit code ${result.exitCode}`)\n }\n\n logger.info('E2B pregame ready', { sandboxId: sandbox.sandboxId, stdout: result.stdout, stderr: result.stderr })\n}\n\nexport interface E2BReadinessOptions {\n /** Total deadline in seconds. `<= 0` disables the probe entirely. */\n timeoutSeconds: number\n /**\n * Whether to kill the sandbox if it never becomes ready. Defaults to `true`\n * (we created it, so a dead sandbox is ours to clean up). Pass `false` when\n * attaching to an externally-owned sandbox — failing the probe shouldn't tear\n * down a sandbox the caller is managing.\n */\n killOnTimeout?: boolean\n /** Clock source — injectable so tests can drive the deadline deterministically. */\n now?: () => number\n /** Sleep between probes — injectable so tests can advance the clock without waiting. */\n sleep?: (ms: number) => Promise<void>\n}\n\n/**\n * Block until an E2B sandbox is ready to run commands, or throw if it never\n * becomes ready within the deadline.\n *\n * The E2B control plane can hand back a sandbox id before the sandbox's `envd`\n * is accepting commands; the first real command then absorbs that cold start\n * and may time out. This polls a trivial `true` command on a short backoff,\n * logging a `waiting…` line up front and a `ready` line (with `elapsedMs`) on\n * success. On deadline exhaustion it best-effort kills the sandbox and throws.\n *\n * `now`/`sleep` are injectable purely for deterministic tests; in production\n * they default to `Date.now` and a real timer.\n */\nexport async function waitForE2BReady(\n sandbox: E2BSandbox,\n logger: Logger,\n opts: E2BReadinessOptions,\n): Promise<void> {\n if (opts.timeoutSeconds <= 0)\n return\n\n const now = opts.now ?? Date.now\n const sleep = opts.sleep ?? (ms => new Promise<void>(resolve => setTimeout(resolve, ms)))\n const start = now()\n const deadline = start + opts.timeoutSeconds * 1000\n\n logger.info('waiting for E2B sandbox to initialize', { sandboxId: sandbox.sandboxId })\n\n while (true) {\n try {\n await sandbox.commands.run('true', { timeoutMs: READINESS_PROBE_TIMEOUT_MS })\n logger.info('E2B sandbox ready', { sandboxId: sandbox.sandboxId, elapsedMs: now() - start })\n return\n }\n catch {\n if (now() >= deadline) {\n if (opts.killOnTimeout !== false)\n await sandbox.kill().catch(() => {})\n throw new Error(`E2B sandbox ${sandbox.sandboxId} did not become ready within ${opts.timeoutSeconds}s`)\n }\n await sleep(READINESS_BACKOFF_MS)\n }\n }\n}\n\n/**\n * Resolve the effective E2B template, highest precedence first: the per-spawn\n * `SpawnConfig.sandbox.template`, then the factory `template` option, then the\n * `E2B_TEMPLATE` env var. Returns `undefined` (→ SDK's `base` default) when\n * none is set. The E2B SDK reads `E2B_API_KEY`/`E2B_DOMAIN` from the env but\n * NOT `E2B_TEMPLATE`, so this provider has to honor it itself.\n */\nexport function resolveE2BTemplate(\n perSpawnTemplate: string | undefined,\n optionTemplate: string | undefined,\n env: Record<string, string | undefined> = process.env,\n): string | undefined {\n return perSpawnTemplate || optionTemplate || env.E2B_TEMPLATE || undefined\n}\n\n/**\n * Merge the factory-level {@link E2BProviderOptions.env} with the per-spawn\n * `SpawnConfig.env`, per-spawn winning on key conflicts. Returns `undefined`\n * when the merge is empty so callers can skip setting `envs` on the create\n * options entirely (rather than handing the SDK an empty object).\n */\nexport function resolveE2BEnv(\n optionEnv: Record<string, string> | undefined,\n perSpawnEnv: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n const merged = { ...optionEnv, ...perSpawnEnv }\n return Object.keys(merged).length > 0 ? merged : undefined\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\n/**\n * Build a {@link SandboxProvider} backed by E2B.\n *\n * The provider owns a registry of live {@link E2BSandbox} instances keyed by\n * sandbox id, so the per-call `exec`/`readFile`/… hooks reuse the same warm\n * connection rather than reconnecting each time.\n */\nexport function createE2BProvider(options: E2BProviderOptions = {}): SandboxProvider {\n // Track the live sandbox, the env it was created with, and whether WE own it.\n // We re-send that env on every `commands.run` rather than relying on\n // `Sandbox.create`'s `envs` alone — a warm / pre-provisioned sandbox handed\n // back by the control plane may never have run our create-time env injection.\n // `owned` is false for sandboxes we attached to via `Sandbox.connect`: those\n // are never killed on teardown/failure because the caller manages them.\n const live = new Map<string, { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean }>()\n\n // Resolve the readiness logger once: caller-supplied, else a console-backed\n // default so the \"waiting for sandbox\" line still reaches stderr.\n const logger = options.logger ?? createLogger(consoleSink())\n const readinessTimeoutSeconds = options.readinessTimeoutSeconds ?? 60\n\n async function loadSdk(): Promise<E2BSandboxStatic> {\n try {\n const mod = await import('e2b') as { Sandbox: E2BSandboxStatic }\n return mod.Sandbox\n }\n catch {\n throw new Error('e2b is required for the E2B sandbox provider. Install it with: bun add e2b')\n }\n }\n\n function get(sandboxId: string): { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean } {\n const entry = live.get(sandboxId)\n if (!entry)\n throw new Error(`E2B sandbox ${sandboxId} is not tracked by this provider`)\n return entry\n }\n\n // Best-effort kill of an owned sandbox. Failures are swallowed: the sandbox\n // may already be gone (timed out, or killed out-of-band).\n async function killOwnedSandbox(sandbox: E2BSandbox): Promise<void> {\n try {\n await sandbox.kill()\n }\n catch {\n // Already gone — nothing to clean up.\n }\n }\n\n // Pause an owned sandbox so its filesystem/process state survives for a later\n // reconnect. Prefers the stable `pause()` (e2b@2.27.1+), falling back to the\n // deprecated `betaPause()` (e2b@2.0.0+) across the rest of the supported 2.x\n // range. If a mismatched install exposes neither, surface it loudly rather\n // than let a bare `catch` swallow the \"not a function\" error — which would\n // leave the caller believing state was preserved while the sandbox is really\n // about to time out. A genuine pause failure is logged the same way; we\n // deliberately do NOT fall back to kill, since that would destroy the very\n // state the caller asked to keep.\n async function pauseOwnedSandbox(sandbox: E2BSandbox): Promise<void> {\n try {\n if (sandbox.pause) {\n await sandbox.pause()\n }\n else if (sandbox.betaPause) {\n await sandbox.betaPause()\n }\n else {\n logger.error('cannot pause E2B sandbox: this e2b SDK exposes neither pause() nor betaPause() (expected e2b@^2); leaving it to time out', { sandboxId: sandbox.sandboxId })\n return\n }\n logger.info('paused E2B sandbox; reconnect later via the sandboxId option (CLI: --sandbox)', { sandboxId: sandbox.sandboxId })\n }\n catch (err) {\n logger.error('failed to pause E2B sandbox; it will run until its timeout', { sandboxId: sandbox.sandboxId, error: err instanceof Error ? err.message : String(err) })\n }\n }\n\n return {\n name: 'e2b',\n\n async spawn(config: SpawnConfig): Promise<{ id: string, cwd: string }> {\n const Sandbox = await loadSdk()\n const sb: Record<string, unknown> = config.sandbox ?? {}\n\n // Per-spawn overrides win over factory options win over SDK env defaults.\n const apiKey = (sb.apiKey as string | undefined) ?? options.apiKey\n const domain = (sb.domain as string | undefined) ?? options.domain\n const template = resolveE2BTemplate(sb.template as string | undefined, options.template)\n const timeoutSeconds = config.limits?.timeout ?? options.timeoutSeconds\n const connectId = (sb.sandboxId as string | undefined) ?? options.sandboxId\n\n const envs = resolveE2BEnv(options.env, config.env)\n\n // Attach to an existing sandbox when a sandbox id was supplied; otherwise\n // create a fresh one. A connected sandbox is externally owned (`owned`\n // false) so we never kill it on failure or teardown. The create-time\n // `envs`/`template` don't apply to an attach — env is still re-sent on\n // every command, so a connected sandbox isn't left without it.\n let sandbox: E2BSandbox\n const owned = connectId === undefined\n if (connectId !== undefined) {\n const connectOpts: E2BConnectOptions = {}\n if (apiKey !== undefined)\n connectOpts.apiKey = apiKey\n if (domain !== undefined)\n connectOpts.domain = domain\n if (timeoutSeconds !== undefined)\n connectOpts.timeoutMs = timeoutSeconds * 1000\n sandbox = await Sandbox.connect(connectId, connectOpts)\n }\n else {\n const createOpts: E2BCreateOptions = {}\n if (apiKey !== undefined)\n createOpts.apiKey = apiKey\n if (domain !== undefined)\n createOpts.domain = domain\n if (envs)\n createOpts.envs = envs\n if (timeoutSeconds !== undefined)\n createOpts.timeoutMs = timeoutSeconds * 1000\n sandbox = template !== undefined\n ? await Sandbox.create(template, createOpts)\n : await Sandbox.create(createOpts)\n }\n\n live.set(sandbox.sandboxId, { sandbox, env: envs, owned })\n\n // Wait for envd to actually accept commands before handing back the\n // handle — otherwise the cwd mkdir below (and the agent's first turn)\n // races the sandbox's cold start. On failure a sandbox we created is\n // already killed; a connected one is left alone (`killOnTimeout`). Either\n // way, stop tracking it and propagate.\n try {\n await waitForE2BReady(sandbox, logger, { timeoutSeconds: readinessTimeoutSeconds, killOnTimeout: owned })\n }\n catch (err) {\n live.delete(sandbox.sandboxId)\n throw err\n }\n\n // Resolve the working directory. An explicit cwd (per-spawn, else the\n // provider default) is the caller's responsibility — create it up front\n // so relative ops have somewhere to land. With no explicit cwd, DON'T\n // assume E2B's hosted `/home/user`: self-hosted templates may run as a\n // different user whose home lives elsewhere and which we lack permission\n // to create under `/home`. Handing envd a non-existent cwd makes it fail\n // process spawn with ENOENT (\"No such file or directory\"), breaking the\n // pregame script and the agent's first shell turn alike. Instead, ask the\n // sandbox where a login shell actually lands (a cwd-less `pwd`, which\n // can't itself hit the ENOENT) and use that.\n const requestedCwd = config.cwd ?? options.cwd\n let cwd: string\n if (requestedCwd) {\n cwd = requestedCwd\n // Best-effort: the cwd usually already exists (or is creatable), and a\n // genuine failure surfaces as ENOENT on the very next command anyway.\n await sandbox.commands.run(`mkdir -p ${JSON.stringify(cwd)}`).catch(() => {})\n }\n else {\n const discovered = await sandbox.commands.run('pwd')\n .then(r => r.stdout.trim())\n .catch(() => '')\n cwd = discovered || '/home/user'\n }\n\n // Pre-provision the environment before any prompting: upload + run the\n // setup script in the now-ready sandbox. A failure tears the sandbox\n // down (mirroring the readiness gate) rather than handing back a\n // half-provisioned environment.\n if (options.pregame) {\n try {\n await runE2BPregame(sandbox, logger, { name: options.pregame.name, content: options.pregame.content, cwd, envs })\n }\n catch (err) {\n // Only tear down a sandbox we created; a connected one is the\n // caller's to manage even when our setup script fails.\n if (owned)\n await sandbox.kill().catch(() => {})\n live.delete(sandbox.sandboxId)\n throw err\n }\n }\n\n return { id: sandbox.sandboxId, cwd }\n },\n\n async exec(sandboxId, command, opts): Promise<ExecResult> {\n const { sandbox, env } = get(sandboxId)\n // Match the process/docker contexts: `timeout` is in seconds, default 30.\n const timeoutMs = (opts?.timeout ?? 30) * 1000\n try {\n const result = await sandbox.commands.run(command, {\n cwd: opts?.cwd,\n // Re-send the sandbox's configured env on every command (warm\n // sandboxes may lack the create-time injection), with the per-call\n // env layered on top.\n envs: resolveE2BEnv(env, opts?.env),\n timeoutMs: timeoutMs > 0 ? timeoutMs : undefined,\n })\n // The SDK buffers the streams uncapped (see the KNOWN RESIDUAL note\n // on `ExecutionContext.fileSize`) — cap host-side before the strings\n // propagate into tool results and session history.\n return { ...capBufferedExecOutput(result.stdout, result.stderr), exitCode: result.exitCode }\n }\n catch (err: any) {\n // E2B throws on non-zero exit (CommandExitError carries the captured\n // streams) and on timeout. Normalize both into an ExecResult so the\n // shell tool sees a clean exit code rather than a thrown error.\n if (typeof err?.exitCode === 'number') {\n return {\n ...capBufferedExecOutput(err.stdout ?? '', err.stderr ?? err.message ?? ''),\n exitCode: err.exitCode,\n }\n }\n return { stdout: '', stderr: err?.message ?? String(err), exitCode: 124 }\n }\n },\n\n async readFile(sandboxId, path): Promise<string> {\n return get(sandboxId).sandbox.files.read(path)\n },\n\n async writeFile(sandboxId, path, content): Promise<void> {\n await get(sandboxId).sandbox.files.write(path, content)\n },\n\n async listFiles(sandboxId, path): Promise<string[]> {\n const entries = await get(sandboxId).sandbox.files.list(path)\n // Trailing `/` on directories — parity with process/docker/daytona.\n return entries.map(e => (e.type === 'dir' ? `${e.name}/` : e.name))\n },\n\n async destroy(sandboxId): Promise<void> {\n const entry = live.get(sandboxId)\n if (!entry)\n return\n // Never kill/pause a sandbox we only connected to — the caller owns its\n // lifecycle. Just drop our handle to it.\n if (entry.owned) {\n const action = options.onDestroy ?? 'kill'\n if (action === 'pause')\n await pauseOwnedSandbox(entry.sandbox)\n else if (action === 'kill')\n await killOwnedSandbox(entry.sandbox)\n // 'leave' makes no API call — the sandbox runs until E2B's own timeout.\n }\n live.delete(sandboxId)\n },\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"e2b.js","names":[],"sources":["../../src/contexts/e2b.ts"],"sourcesContent":["/**\n * E2B sandbox provider.\n *\n * Implements {@link SandboxProvider} on top of E2B's firecracker-backed\n * micro-VMs. Pair it with {@link createSandboxContext} to run an agent's\n * shell + filesystem tools inside an ephemeral E2B sandbox:\n *\n * ```ts\n * import { createSandboxContext } from 'zidane/contexts'\n * import { createE2BProvider } from 'zidane/contexts/e2b'\n *\n * const execution = createSandboxContext(createE2BProvider())\n * ```\n *\n * ## Pointing at your own E2B\n *\n * The two knobs that select *which* E2B you talk to are `apiKey` and\n * `domain`. Self-hosted / on-prem E2B clusters expose a custom domain; pass\n * it here (or via the `E2B_DOMAIN` env var) and the SDK routes every call to\n * your cluster instead of E2B's hosted API. Precedence, highest first:\n *\n * 1. Per-spawn `SpawnConfig.sandbox` fields (`apiKey`, `domain`, `template`)\n * 2. {@link E2BProviderOptions} passed to this factory\n * 3. `E2B_API_KEY` / `E2B_DOMAIN` env vars (read by the SDK itself), and\n * `E2B_TEMPLATE` (read by this provider — the SDK does not read it)\n *\n * The default `base` template does not exist on self-hosted clusters, so\n * picking the right template matters: an invalid template still returns a\n * sandbox id from the control plane, but its envd never boots and every\n * `exec`/file op then hangs until it times out. Set `E2B_TEMPLATE` (or pass\n * `template`) to a template that actually exists on your cluster.\n *\n * Requires `e2b@^2` as an optional peer dependency:\n * `bun add e2b`.\n */\n\nimport type { Logger } from '../logger'\nimport type { SandboxProvider } from './sandbox'\nimport type { ExecResult, SpawnConfig, StreamHandle } from './types'\nimport { consoleSink, createLogger } from '../logger'\nimport { alwaysQuote } from '../tools/shell-quote'\nimport { capBufferedExecOutput } from './output-cap'\nimport { StreamQueue } from './stream-queue'\n\n// ---------------------------------------------------------------------------\n// Minimal structural views of the `e2b` SDK surface we touch. Declared locally\n// (rather than imported) so the package typechecks without the optional peer\n// dependency installed — mirroring how `docker.ts` keeps `dockerode` at arm's\n// length. The real SDK satisfies these shapes. The peer dependency floor is\n// `e2b@^2`: every member used here exists across 2.x, but the pause surface\n// shifted within it — `betaPause()` from 2.0.0, the stable `pause()` from\n// 2.27.1 — so `onDestroy: 'pause'` prefers `pause()` and falls back to\n// `betaPause()` (both optional below, resolved at runtime in `destroy`).\n// ---------------------------------------------------------------------------\n\n/**\n * Options that flip `commands.run` into background mode. Split out so the\n * overload above can discriminate on it: `background: true` changes the\n * return type from a finished result to a live handle.\n *\n * `stdin: true` and a huge `timeoutMs` are not incidental — the SDK\n * defaults are `false` and 60s, which would respectively hand the child an\n * immediate EOF and kill a session-lifetime process after a minute. See\n * {@link E2B_STREAM_TIMEOUT_MS} for why the timeout is a large number and\n * not `0`.\n */\ninterface E2BBackgroundOptions {\n background: true\n stdin: true\n timeoutMs: number\n cwd?: string\n envs?: Record<string, string>\n onStdout?: (data: string) => void\n onStderr?: (data: string) => void\n}\n\n/**\n * \"Effectively unlimited\" command timeout for `execStream`. 2^31-1 ms\n * (~24.8 days — far beyond any sandbox lifetime) because both edges are\n * cliffs: `0` is treated by `@connectrpc/connect`'s deadline signal as\n * already expired (instant DeadlineExceeded), and anything above 2^31-1\n * overflows `setTimeout` and fires immediately.\n */\nconst E2B_STREAM_TIMEOUT_MS = 2 ** 31 - 1\n\ninterface E2BCommandResult {\n stdout: string\n stderr: string\n exitCode: number\n}\n\ninterface E2BEntry {\n name: string\n /** Present in the E2B SDK's EntryInfo; used for the `/` dir suffix. */\n type?: string\n}\n\n/**\n * Handle returned by a `background: true` command. Structural subset of the\n * SDK's `CommandHandle` — only what `execStream` needs.\n */\ninterface E2BCommandHandle {\n pid: number\n wait: () => Promise<E2BCommandResult>\n}\n\ninterface E2BSandbox {\n sandboxId: string\n commands: {\n run: ((cmd: string, opts?: { cwd?: string, envs?: Record<string, string>, timeoutMs?: number }) => Promise<E2BCommandResult>)\n & ((cmd: string, opts: E2BBackgroundOptions) => Promise<E2BCommandHandle>)\n /** Present from the SDK's Commands module; used only by `execStream`. */\n sendStdin?: (pid: number, data: string | Uint8Array) => Promise<void>\n kill?: (pid: number) => Promise<boolean>\n }\n files: {\n read: (path: string) => Promise<string>\n write: (path: string, data: string) => Promise<unknown>\n list: (path: string) => Promise<E2BEntry[]>\n }\n kill: () => Promise<unknown>\n // The pause surface shifted within 2.x: `betaPause` from 2.0.0, the stable\n // `pause` from 2.27.1. Both are optional so `destroy` can prefer `pause`,\n // fall back to `betaPause`, and stay defensive if a mismatched install\n // exposes neither. Both resolve to whether the sandbox was newly paused.\n pause?: () => Promise<boolean>\n betaPause?: () => Promise<boolean>\n}\n\ninterface E2BCreateOptions {\n apiKey?: string\n domain?: string\n timeoutMs?: number\n envs?: Record<string, string>\n metadata?: Record<string, string>\n}\n\ninterface E2BConnectOptions {\n apiKey?: string\n domain?: string\n timeoutMs?: number\n}\n\ninterface E2BSandboxStatic {\n create: (templateOrOpts?: string | E2BCreateOptions, opts?: E2BCreateOptions) => Promise<E2BSandbox>\n connect: (sandboxId: string, opts?: E2BConnectOptions) => Promise<E2BSandbox>\n}\n\n// ---------------------------------------------------------------------------\n// Provider options\n// ---------------------------------------------------------------------------\n\nexport interface E2BProviderOptions {\n /** E2B API key. Falls back to the `E2B_API_KEY` env var when omitted. */\n apiKey?: string\n /**\n * E2B API domain. Set this to point at a self-hosted / on-prem cluster\n * (e.g. `e2b.my-company.internal`). Falls back to the `E2B_DOMAIN` env var,\n * then to E2B's hosted default.\n */\n domain?: string\n /**\n * Sandbox template id / name to launch. Falls back to the `E2B_TEMPLATE`\n * env var, then to E2B's hosted `base` template. Note that `base` does not\n * exist on self-hosted clusters — supply a template that does.\n */\n template?: string\n /**\n * Connect to a pre-existing sandbox by id instead of creating a fresh one\n * (E2B's `Sandbox.connect`). When set, `template` is ignored — you're\n * attaching to a sandbox that already exists. The provider treats such a\n * sandbox as externally owned: it is NOT killed on `destroy`, nor torn down\n * if the readiness probe / pregame fails, since the caller manages its\n * lifecycle. Per-spawn `SpawnConfig.sandbox.sandboxId` overrides this.\n */\n sandboxId?: string\n /**\n * Default working directory, applied when a spawn doesn't set its own\n * `SpawnConfig.cwd`. Created on spawn. Defaults to E2B's `/home/user`.\n */\n cwd?: string\n /**\n * Sandbox lifetime in seconds before E2B auto-kills it. Defaults to the\n * SDK default (300s). Per-spawn `SpawnConfig.limits.timeout` overrides this.\n */\n timeoutSeconds?: number\n /**\n * Environment variables baked into every sandbox at create time (passed to\n * E2B's `Sandbox.create({ envs })`), so every command run in the sandbox\n * sees them. Per-spawn `SpawnConfig.env` is merged over these.\n */\n env?: Record<string, string>\n /**\n * Logger for provider-level lifecycle lines (notably the readiness wait).\n * Defaults to a {@link consoleSink}-backed logger so the messages reach\n * stderr without the caller wiring one up.\n */\n logger?: Logger\n /**\n * Total deadline, in seconds, for the post-create readiness probe (see\n * {@link waitForE2BReady}). A freshly-created sandbox can return its id\n * before `envd` accepts commands; `spawn` blocks until a trivial probe\n * succeeds so the first real command never absorbs the cold start. Defaults\n * to `60`. Set to `0` to disable the gate.\n */\n readinessTimeoutSeconds?: number\n /**\n * A setup script to upload and run once the sandbox is ready, before any\n * prompting begins (see {@link runE2BPregame}). `name` is the basename used\n * for the uploaded file; `content` is the script body. The script is run\n * directly so its shebang selects the interpreter. A non-zero exit aborts\n * `spawn` (and tears the sandbox down) — a broken setup must not silently\n * hand back a half-provisioned sandbox.\n */\n pregame?: { name: string, content: string }\n /**\n * What to do with an owned sandbox when `destroy` is called:\n *\n * - `'kill'` (default) — call `sandbox.kill()`. The sandbox is deleted and\n * its state is gone.\n * - `'pause'` — call `sandbox.pause()` (falling back to the older\n * `betaPause()` on `e2b` 2.0–2.26). The sandbox's filesystem and process\n * state are preserved; reconnect later with the `sandboxId` option (E2B's\n * `Sandbox.connect` auto-resumes a paused sandbox).\n * - `'leave'` — drop the local handle without making any API call. The\n * sandbox keeps running until E2B's own timeout fires.\n *\n * Has no effect on sandboxes the provider only connected to (the `sandboxId`\n * option) — those are always left untouched on destroy since the caller owns\n * their lifecycle.\n */\n onDestroy?: 'kill' | 'pause' | 'leave'\n}\n\n/** Per-probe timeout for the readiness `true` command. */\nconst READINESS_PROBE_TIMEOUT_MS = 5000\n/** Pause between readiness probes while the sandbox is still warming up. */\nconst READINESS_BACKOFF_MS = 250\n/**\n * Command timeout for the pregame setup script. E2B's `commands.run` defaults\n * to 60s and offers no true \"unlimited\" — a `timeoutMs` of 0 is a 0ms deadline\n * (instant timeout), not \"off\". Setup scripts (`apt`/`pip` installs, builds)\n * routinely exceed 60s, so we pass a deliberately generous 30-minute ceiling.\n * The sandbox's own lifetime independently bounds anything longer.\n */\nconst PREGAME_TIMEOUT_MS = 30 * 60 * 1000\n\n/**\n * Upload a setup (\"pregame\") script into a ready sandbox and execute it.\n *\n * Writes the script to `/tmp/<name>`, then `chmod +x`'s and runs it directly\n * (so a `#!/bin/bash` / `#!/usr/bin/env python3` shebang chooses the\n * interpreter) in `cwd` with `envs` applied — the same env the sandbox was\n * created with, so the script sees the run's `--env` / `--pass-env`. A\n * generous {@link PREGAME_TIMEOUT_MS} ceiling is used so long `apt`/`pip`\n * installs aren't killed by E2B's 60s command default. A non-zero exit\n * (whether E2B returns it or raises on it) throws, so the caller can tear the\n * sandbox down rather than prompt against a broken environment.\n */\nexport async function runE2BPregame(\n sandbox: E2BSandbox,\n logger: Logger,\n opts: { name: string, content: string, cwd?: string, envs?: Record<string, string> },\n): Promise<void> {\n const path = `/tmp/${opts.name}`\n logger.info('running E2B pregame script', { sandboxId: sandbox.sandboxId, path })\n await sandbox.files.write(path, opts.content)\n\n const quoted = JSON.stringify(path)\n let result: E2BCommandResult\n try {\n result = await sandbox.commands.run(`chmod +x ${quoted} && ${quoted}`, {\n cwd: opts.cwd,\n envs: opts.envs,\n timeoutMs: PREGAME_TIMEOUT_MS,\n })\n }\n catch (err: any) {\n // E2B raises CommandExitError (carrying the captured streams) on a\n // non-zero exit. Surface its output, then fail the spawn.\n const exitCode = typeof err?.exitCode === 'number' ? err.exitCode : 124\n logger.error('E2B pregame script failed', { sandboxId: sandbox.sandboxId, exitCode, stdout: err?.stdout ?? '', stderr: err?.stderr ?? err?.message ?? '' })\n throw new Error(`E2B pregame script ${opts.name} failed with exit code ${exitCode}`)\n }\n\n if (result.exitCode !== 0) {\n logger.error('E2B pregame script failed', { sandboxId: sandbox.sandboxId, exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr })\n throw new Error(`E2B pregame script ${opts.name} failed with exit code ${result.exitCode}`)\n }\n\n logger.info('E2B pregame ready', { sandboxId: sandbox.sandboxId, stdout: result.stdout, stderr: result.stderr })\n}\n\nexport interface E2BReadinessOptions {\n /** Total deadline in seconds. `<= 0` disables the probe entirely. */\n timeoutSeconds: number\n /**\n * Whether to kill the sandbox if it never becomes ready. Defaults to `true`\n * (we created it, so a dead sandbox is ours to clean up). Pass `false` when\n * attaching to an externally-owned sandbox — failing the probe shouldn't tear\n * down a sandbox the caller is managing.\n */\n killOnTimeout?: boolean\n /** Clock source — injectable so tests can drive the deadline deterministically. */\n now?: () => number\n /** Sleep between probes — injectable so tests can advance the clock without waiting. */\n sleep?: (ms: number) => Promise<void>\n}\n\n/**\n * Block until an E2B sandbox is ready to run commands, or throw if it never\n * becomes ready within the deadline.\n *\n * The E2B control plane can hand back a sandbox id before the sandbox's `envd`\n * is accepting commands; the first real command then absorbs that cold start\n * and may time out. This polls a trivial `true` command on a short backoff,\n * logging a `waiting…` line up front and a `ready` line (with `elapsedMs`) on\n * success. On deadline exhaustion it best-effort kills the sandbox and throws.\n *\n * `now`/`sleep` are injectable purely for deterministic tests; in production\n * they default to `Date.now` and a real timer.\n */\nexport async function waitForE2BReady(\n sandbox: E2BSandbox,\n logger: Logger,\n opts: E2BReadinessOptions,\n): Promise<void> {\n if (opts.timeoutSeconds <= 0)\n return\n\n const now = opts.now ?? Date.now\n const sleep = opts.sleep ?? (ms => new Promise<void>(resolve => setTimeout(resolve, ms)))\n const start = now()\n const deadline = start + opts.timeoutSeconds * 1000\n\n logger.info('waiting for E2B sandbox to initialize', { sandboxId: sandbox.sandboxId })\n\n while (true) {\n try {\n await sandbox.commands.run('true', { timeoutMs: READINESS_PROBE_TIMEOUT_MS })\n logger.info('E2B sandbox ready', { sandboxId: sandbox.sandboxId, elapsedMs: now() - start })\n return\n }\n catch {\n if (now() >= deadline) {\n if (opts.killOnTimeout !== false)\n await sandbox.kill().catch(() => {})\n throw new Error(`E2B sandbox ${sandbox.sandboxId} did not become ready within ${opts.timeoutSeconds}s`)\n }\n await sleep(READINESS_BACKOFF_MS)\n }\n }\n}\n\n/**\n * Resolve the effective E2B template, highest precedence first: the per-spawn\n * `SpawnConfig.sandbox.template`, then the factory `template` option, then the\n * `E2B_TEMPLATE` env var. Returns `undefined` (→ SDK's `base` default) when\n * none is set. The E2B SDK reads `E2B_API_KEY`/`E2B_DOMAIN` from the env but\n * NOT `E2B_TEMPLATE`, so this provider has to honor it itself.\n */\nexport function resolveE2BTemplate(\n perSpawnTemplate: string | undefined,\n optionTemplate: string | undefined,\n env: Record<string, string | undefined> = process.env,\n): string | undefined {\n return perSpawnTemplate || optionTemplate || env.E2B_TEMPLATE || undefined\n}\n\n/**\n * Merge the factory-level {@link E2BProviderOptions.env} with the per-spawn\n * `SpawnConfig.env`, per-spawn winning on key conflicts. Returns `undefined`\n * when the merge is empty so callers can skip setting `envs` on the create\n * options entirely (rather than handing the SDK an empty object).\n */\nexport function resolveE2BEnv(\n optionEnv: Record<string, string> | undefined,\n perSpawnEnv: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n const merged = { ...optionEnv, ...perSpawnEnv }\n return Object.keys(merged).length > 0 ? merged : undefined\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\n/**\n * Build a {@link SandboxProvider} backed by E2B.\n *\n * The provider owns a registry of live {@link E2BSandbox} instances keyed by\n * sandbox id, so the per-call `exec`/`readFile`/… hooks reuse the same warm\n * connection rather than reconnecting each time.\n */\nexport function createE2BProvider(options: E2BProviderOptions = {}): SandboxProvider {\n // Track the live sandbox, the env it was created with, and whether WE own it.\n // We re-send that env on every `commands.run` rather than relying on\n // `Sandbox.create`'s `envs` alone — a warm / pre-provisioned sandbox handed\n // back by the control plane may never have run our create-time env injection.\n // `owned` is false for sandboxes we attached to via `Sandbox.connect`: those\n // are never killed on teardown/failure because the caller manages them.\n const live = new Map<string, { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean }>()\n\n // Resolve the readiness logger once: caller-supplied, else a console-backed\n // default so the \"waiting for sandbox\" line still reaches stderr.\n const logger = options.logger ?? createLogger(consoleSink())\n const readinessTimeoutSeconds = options.readinessTimeoutSeconds ?? 60\n\n async function loadSdk(): Promise<E2BSandboxStatic> {\n try {\n const mod = await import('e2b') as { Sandbox: E2BSandboxStatic }\n return mod.Sandbox\n }\n catch {\n throw new Error('e2b is required for the E2B sandbox provider. Install it with: bun add e2b')\n }\n }\n\n function get(sandboxId: string): { sandbox: E2BSandbox, env?: Record<string, string>, owned: boolean } {\n const entry = live.get(sandboxId)\n if (!entry)\n throw new Error(`E2B sandbox ${sandboxId} is not tracked by this provider`)\n return entry\n }\n\n // Best-effort kill of an owned sandbox. Failures are swallowed: the sandbox\n // may already be gone (timed out, or killed out-of-band).\n async function killOwnedSandbox(sandbox: E2BSandbox): Promise<void> {\n try {\n await sandbox.kill()\n }\n catch {\n // Already gone — nothing to clean up.\n }\n }\n\n // Pause an owned sandbox so its filesystem/process state survives for a later\n // reconnect. Prefers the stable `pause()` (e2b@2.27.1+), falling back to the\n // deprecated `betaPause()` (e2b@2.0.0+) across the rest of the supported 2.x\n // range. If a mismatched install exposes neither, surface it loudly rather\n // than let a bare `catch` swallow the \"not a function\" error — which would\n // leave the caller believing state was preserved while the sandbox is really\n // about to time out. A genuine pause failure is logged the same way; we\n // deliberately do NOT fall back to kill, since that would destroy the very\n // state the caller asked to keep.\n async function pauseOwnedSandbox(sandbox: E2BSandbox): Promise<void> {\n try {\n if (sandbox.pause) {\n await sandbox.pause()\n }\n else if (sandbox.betaPause) {\n await sandbox.betaPause()\n }\n else {\n logger.error('cannot pause E2B sandbox: this e2b SDK exposes neither pause() nor betaPause() (expected e2b@^2); leaving it to time out', { sandboxId: sandbox.sandboxId })\n return\n }\n logger.info('paused E2B sandbox; reconnect later via the sandboxId option (CLI: --sandbox)', { sandboxId: sandbox.sandboxId })\n }\n catch (err) {\n logger.error('failed to pause E2B sandbox; it will run until its timeout', { sandboxId: sandbox.sandboxId, error: err instanceof Error ? err.message : String(err) })\n }\n }\n\n return {\n name: 'e2b',\n\n async spawn(config: SpawnConfig): Promise<{ id: string, cwd: string }> {\n const Sandbox = await loadSdk()\n const sb: Record<string, unknown> = config.sandbox ?? {}\n\n // Per-spawn overrides win over factory options win over SDK env defaults.\n const apiKey = (sb.apiKey as string | undefined) ?? options.apiKey\n const domain = (sb.domain as string | undefined) ?? options.domain\n const template = resolveE2BTemplate(sb.template as string | undefined, options.template)\n const timeoutSeconds = config.limits?.timeout ?? options.timeoutSeconds\n const connectId = (sb.sandboxId as string | undefined) ?? options.sandboxId\n\n const envs = resolveE2BEnv(options.env, config.env)\n\n // Attach to an existing sandbox when a sandbox id was supplied; otherwise\n // create a fresh one. A connected sandbox is externally owned (`owned`\n // false) so we never kill it on failure or teardown. The create-time\n // `envs`/`template` don't apply to an attach — env is still re-sent on\n // every command, so a connected sandbox isn't left without it.\n let sandbox: E2BSandbox\n const owned = connectId === undefined\n if (connectId !== undefined) {\n const connectOpts: E2BConnectOptions = {}\n if (apiKey !== undefined)\n connectOpts.apiKey = apiKey\n if (domain !== undefined)\n connectOpts.domain = domain\n if (timeoutSeconds !== undefined)\n connectOpts.timeoutMs = timeoutSeconds * 1000\n sandbox = await Sandbox.connect(connectId, connectOpts)\n }\n else {\n const createOpts: E2BCreateOptions = {}\n if (apiKey !== undefined)\n createOpts.apiKey = apiKey\n if (domain !== undefined)\n createOpts.domain = domain\n if (envs)\n createOpts.envs = envs\n if (timeoutSeconds !== undefined)\n createOpts.timeoutMs = timeoutSeconds * 1000\n sandbox = template !== undefined\n ? await Sandbox.create(template, createOpts)\n : await Sandbox.create(createOpts)\n }\n\n live.set(sandbox.sandboxId, { sandbox, env: envs, owned })\n\n // Wait for envd to actually accept commands before handing back the\n // handle — otherwise the cwd mkdir below (and the agent's first turn)\n // races the sandbox's cold start. On failure a sandbox we created is\n // already killed; a connected one is left alone (`killOnTimeout`). Either\n // way, stop tracking it and propagate.\n try {\n await waitForE2BReady(sandbox, logger, { timeoutSeconds: readinessTimeoutSeconds, killOnTimeout: owned })\n }\n catch (err) {\n live.delete(sandbox.sandboxId)\n throw err\n }\n\n // Resolve the working directory. An explicit cwd (per-spawn, else the\n // provider default) is the caller's responsibility — create it up front\n // so relative ops have somewhere to land. With no explicit cwd, DON'T\n // assume E2B's hosted `/home/user`: self-hosted templates may run as a\n // different user whose home lives elsewhere and which we lack permission\n // to create under `/home`. Handing envd a non-existent cwd makes it fail\n // process spawn with ENOENT (\"No such file or directory\"), breaking the\n // pregame script and the agent's first shell turn alike. Instead, ask the\n // sandbox where a login shell actually lands (a cwd-less `pwd`, which\n // can't itself hit the ENOENT) and use that.\n const requestedCwd = config.cwd ?? options.cwd\n let cwd: string\n if (requestedCwd) {\n cwd = requestedCwd\n // Best-effort: the cwd usually already exists (or is creatable), and a\n // genuine failure surfaces as ENOENT on the very next command anyway.\n await sandbox.commands.run(`mkdir -p ${JSON.stringify(cwd)}`).catch(() => {})\n }\n else {\n const discovered = await sandbox.commands.run('pwd')\n .then(r => r.stdout.trim())\n .catch(() => '')\n cwd = discovered || '/home/user'\n }\n\n // Pre-provision the environment before any prompting: upload + run the\n // setup script in the now-ready sandbox. A failure tears the sandbox\n // down (mirroring the readiness gate) rather than handing back a\n // half-provisioned environment.\n if (options.pregame) {\n try {\n await runE2BPregame(sandbox, logger, { name: options.pregame.name, content: options.pregame.content, cwd, envs })\n }\n catch (err) {\n // Only tear down a sandbox we created; a connected one is the\n // caller's to manage even when our setup script fails.\n if (owned)\n await sandbox.kill().catch(() => {})\n live.delete(sandbox.sandboxId)\n throw err\n }\n }\n\n return { id: sandbox.sandboxId, cwd }\n },\n\n /**\n * Live bidirectional stream — see `ExecutionContext.execStream`.\n *\n * Backed by E2B's pipe-based command API rather than `sandbox.pty`.\n * Both can carry stdin, but a PTY would force us to defeat terminal\n * semantics we never wanted (input echo, `\\n`→`\\r\\n` rewriting, the\n * ~4096-byte canonical line cap, and stdout/stderr merged into one\n * stream). Pipes give split streams and byte-faithful framing for free.\n *\n * KNOWN RESIDUAL: `onStdout` / `onStderr` deliver `string`, already\n * decoded by the SDK. A chunk boundary that splits a multibyte\n * codepoint is therefore corrupted before it reaches us — we re-encode\n * to bytes to satisfy the seam, but cannot recover what the decoder\n * dropped. Newline framing and ASCII structure are unaffected, so\n * JSON-RPC still parses; the damage would be confined to non-ASCII\n * inside string values. If that ever bites, `sandbox.pty.create`\n * hands back raw `Uint8Array` and the Daytona PTY path in\n * `./daytona.ts` is the template for adopting it here.\n */\n async execStream(sandboxId, command, opts): Promise<StreamHandle> {\n const { sandbox, env } = get(sandboxId)\n\n opts?.signal?.throwIfAborted()\n\n // A stalled consumer cannot be signalled back to the E2B SDK (its\n // output callbacks are sync `void`), so surface the backlog instead of\n // letting it read as an unexplained host OOM.\n const warnBacklog = (which: string) => (bytes: number) =>\n logger.warn('execStream consumer is not draining output', { sandboxId, stream: which, bufferedBytes: bytes })\n const stdout = new StreamQueue(warnBacklog('stdout'))\n const stderr = new StreamQueue(warnBacklog('stderr'))\n\n // `background: true` returns a handle instead of awaiting completion;\n // `stdin: true` is required (it defaults to FALSE) or the child gets\n // EOF immediately and a JSON-RPC server exits before its first read.\n // `timeoutMs` overrides E2B's 60s default command timeout, which\n // would otherwise kill a session-lifetime server mid-run. NOT `0`:\n // the SDK forwards the value into `@connectrpc/connect`, whose\n // deadline signal treats `timeoutMs <= 0` as ALREADY EXPIRED and\n // aborts the call instantly (the PTY docs' \"0 = indefinite\" does not\n // hold for `commands.run` on this SDK). So: the largest value a JS\n // timer accepts (~24.8 days) — anything above 2^31-1 overflows\n // setTimeout and fires immediately, which is just `0` with extra\n // steps.\n // Kill semantics need a process GROUP, and E2B does not give us one:\n // the reported pid runs in envd's own group (observed live: pid 2003,\n // pgid 1235 = envd). Signalling that group would kill the sandbox\n // agent itself, and signalling the pid alone leaves grandchildren\n // running — a wrapper launcher (npx, uv, a shell one-liner) keeps the\n // real server alive to accumulate across reconnects. So put the child\n // in its own session with `setsid -w` and record ITS pid, which is\n // then the group id. `-w` makes setsid wait and propagate the exit\n // code; bare setsid's parent exits immediately and the stream tears\n // down. The probe falls back to a plain exec on images without `-w`,\n // where the pid-only kill still reaches the direct child.\n const pidFile = `/tmp/.zidane-stream-${Math.random().toString(36).slice(2, 10)}.pid`\n // `sh -c <quoted>` rather than a bare `exec ${command}`: with a\n // compound command (`a; b >&2; c`) a bare exec binds to the FIRST\n // command only and silently drops the rest.\n const leader = `echo ${'$'}${'$'} > ${pidFile}; exec sh -c ${alwaysQuote(command)}`\n const wrapped = `if setsid -w true 2>/dev/null; then exec setsid -w sh -c ${alwaysQuote(leader)}; else exec sh -c ${alwaysQuote(leader)}; fi`\n\n // Typed via the `background: true` overload on `E2BSandbox['commands']`\n // — that is what the overload exists for, so no cast is needed here.\n const handle: E2BCommandHandle = await sandbox.commands.run(wrapped, {\n background: true,\n stdin: true,\n timeoutMs: E2B_STREAM_TIMEOUT_MS,\n ...(opts?.cwd ? { cwd: opts.cwd } : {}),\n envs: resolveE2BEnv(env, opts?.env),\n onStdout: (data: string) => stdout.push(new TextEncoder().encode(data)),\n onStderr: (data: string) => stderr.push(new TextEncoder().encode(data)),\n })\n\n const pid: number = handle.pid\n\n const exited = handle.wait()\n .then(result => result?.exitCode ?? 0)\n // E2B throws CommandExitError on a non-zero exit; the seam promises\n // a never-rejecting `exited`, so translate rather than propagate.\n .catch((err: any) => typeof err?.exitCode === 'number' ? err.exitCode : 137)\n .finally(() => { stdout.close(); stderr.close() })\n\n // Read back the session leader's pid, which `setsid -w` made the\n // group id. Separate one-shot command: the API exposes no group verb.\n let commandExited = false\n void exited.then(() => { commandExited = true })\n const readLeaderPid = async (): Promise<number> => {\n for (let attempt = 0; attempt < 20; attempt++) {\n const probe = await sandbox.commands\n .run(`cat ${pidFile} 2>/dev/null || true`)\n .catch(() => null)\n const parsed = Number.parseInt(probe?.stdout?.trim() ?? '', 10)\n if (Number.isFinite(parsed) && parsed > 0)\n return parsed\n // A dead command will never write the pidfile — a missing binary\n // in the image (the most likely user-facing failure) exits\n // immediately, and burning the remaining attempts would cost up\n // to 20 sequential sandbox round-trips before bootstrap can\n // surface the real error. One more probe already ran above, so\n // nothing written before death is missed.\n if (commandExited)\n break\n await new Promise(r => setTimeout(r, 50))\n }\n return -1\n }\n const leaderPid = await readLeaderPid()\n\n const kill = async (): Promise<void> => {\n if (leaderPid > 0) {\n // Group first (the leader IS the pgid), pid-only as the fallback\n // for images that took the no-setsid branch. NEVER a bare group\n // kill on the reported pid: that group belongs to envd.\n await sandbox.commands\n .run(`kill -TERM -${leaderPid} 2>/dev/null || kill -TERM ${leaderPid} 2>/dev/null || true`)\n .catch(() => {})\n }\n await sandbox.commands.run(`rm -f ${pidFile}`).catch(() => {})\n await sandbox.commands.kill?.(pid).catch(() => {})\n }\n\n if (opts?.signal) {\n if (opts.signal.aborted)\n void kill()\n else\n opts.signal.addEventListener('abort', () => { void kill() }, { once: true })\n }\n\n return {\n pid,\n stdout,\n stderr,\n write: async (data: Uint8Array): Promise<void> => {\n if (!sandbox.commands.sendStdin)\n throw new Error('E2B SDK is too old: `commands.sendStdin` is required for execStream')\n await sandbox.commands.sendStdin(pid, data)\n },\n kill,\n exited,\n }\n },\n\n async exec(sandboxId, command, opts): Promise<ExecResult> {\n const { sandbox, env } = get(sandboxId)\n // Match the process/docker contexts: `timeout` is in seconds, default 30.\n const timeoutMs = (opts?.timeout ?? 30) * 1000\n try {\n const result = await sandbox.commands.run(command, {\n cwd: opts?.cwd,\n // Re-send the sandbox's configured env on every command (warm\n // sandboxes may lack the create-time injection), with the per-call\n // env layered on top.\n envs: resolveE2BEnv(env, opts?.env),\n timeoutMs: timeoutMs > 0 ? timeoutMs : undefined,\n })\n // The SDK buffers the streams uncapped (see the KNOWN RESIDUAL note\n // on `ExecutionContext.fileSize`) — cap host-side before the strings\n // propagate into tool results and session history.\n return { ...capBufferedExecOutput(result.stdout, result.stderr), exitCode: result.exitCode }\n }\n catch (err: any) {\n // E2B throws on non-zero exit (CommandExitError carries the captured\n // streams) and on timeout. Normalize both into an ExecResult so the\n // shell tool sees a clean exit code rather than a thrown error.\n if (typeof err?.exitCode === 'number') {\n return {\n ...capBufferedExecOutput(err.stdout ?? '', err.stderr ?? err.message ?? ''),\n exitCode: err.exitCode,\n }\n }\n return { stdout: '', stderr: err?.message ?? String(err), exitCode: 124 }\n }\n },\n\n async readFile(sandboxId, path): Promise<string> {\n return get(sandboxId).sandbox.files.read(path)\n },\n\n async writeFile(sandboxId, path, content): Promise<void> {\n await get(sandboxId).sandbox.files.write(path, content)\n },\n\n async listFiles(sandboxId, path): Promise<string[]> {\n const entries = await get(sandboxId).sandbox.files.list(path)\n // Trailing `/` on directories — parity with process/docker/daytona.\n return entries.map(e => (e.type === 'dir' ? `${e.name}/` : e.name))\n },\n\n async destroy(sandboxId): Promise<void> {\n const entry = live.get(sandboxId)\n if (!entry)\n return\n // Never kill/pause a sandbox we only connected to — the caller owns its\n // lifecycle. Just drop our handle to it.\n if (entry.owned) {\n const action = options.onDestroy ?? 'kill'\n if (action === 'pause')\n await pauseOwnedSandbox(entry.sandbox)\n else if (action === 'kill')\n await killOwnedSandbox(entry.sandbox)\n // 'leave' makes no API call — the sandbox runs until E2B's own timeout.\n }\n live.delete(sandboxId)\n },\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as createSandboxContext, t as SandboxProvider } from "../sandbox-
|
|
1
|
+
import { n as createSandboxContext, t as SandboxProvider } from "../sandbox-DqwpKX18.js";
|
|
2
2
|
export { SandboxProvider, createSandboxContext };
|
package/dist/contexts/sandbox.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as globFilesViaShell, t as fileSizeViaExec } from "../exec-file-size-BRpoB6Io.js";
|
|
2
2
|
//#region src/contexts/sandbox.ts
|
|
3
3
|
function createSandboxContext(provider) {
|
|
4
4
|
const sandboxes = /* @__PURE__ */ new Map();
|
|
@@ -14,7 +14,8 @@ function createSandboxContext(provider) {
|
|
|
14
14
|
filesystem: true,
|
|
15
15
|
glob: true,
|
|
16
16
|
network: true,
|
|
17
|
-
gpu: false
|
|
17
|
+
gpu: false,
|
|
18
|
+
stream: provider.execStream !== void 0
|
|
18
19
|
},
|
|
19
20
|
async spawn(config) {
|
|
20
21
|
const result = await provider.spawn(config ?? {});
|
|
@@ -29,6 +30,7 @@ function createSandboxContext(provider) {
|
|
|
29
30
|
async exec(handle, command, options) {
|
|
30
31
|
return provider.exec(getSandboxId(handle), command, options);
|
|
31
32
|
},
|
|
33
|
+
...provider.execStream ? { execStream: async (handle, command, options) => provider.execStream(getSandboxId(handle), command, options) } : {},
|
|
32
34
|
async readFile(handle, path) {
|
|
33
35
|
return provider.readFile(getSandboxId(handle), path);
|
|
34
36
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox.js","names":[],"sources":["../../src/contexts/sandbox.ts"],"sourcesContent":["/**\n * Remote sandbox execution context.\n *\n * Offloads execution to a remote sandbox API (e.g. Rivet, E2B).\n * Specific providers implement the SandboxProvider interface.\n */\n\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, ListFilesOptions, SpawnConfig } from './types'\nimport { fileSizeViaExec } from './exec-file-size'\nimport { globFilesViaShell } from './glob-shell'\n\n// ---------------------------------------------------------------------------\n// Sandbox provider interface\n// ---------------------------------------------------------------------------\n\nexport interface SandboxProvider {\n name: string\n spawn: (config: SpawnConfig) => Promise<{ id: string, cwd: string }>\n exec: (sandboxId: string, command: string, options?: { cwd?: string, env?: Record<string, string>, timeout?: number }) => Promise<ExecResult>\n readFile: (sandboxId: string, path: string) => Promise<string>\n writeFile: (sandboxId: string, path: string, content: string) => Promise<void>\n listFiles: (sandboxId: string, path: string, options?: ListFilesOptions) => Promise<string[]>\n destroy: (sandboxId: string) => Promise<void>\n}\n\n// ---------------------------------------------------------------------------\n// Sandbox execution context\n// ---------------------------------------------------------------------------\n\nexport function createSandboxContext(provider: SandboxProvider): ExecutionContext {\n const sandboxes = new Map<string, string>()\n const destroyed = new Set<string>()\n\n // The handle id IS the sandbox id (set in `spawn` below), so the map is an\n // identity record of sandboxes this context instance spawned. A handle this\n // instance never spawned falls back to `handle.id` — the re-attach path for\n // durable hosts that journal the handle and hand it back via\n // `AgentOptions.handle` on a replay where `spawn` never re-ran. An\n // EXPLICITLY-destroyed handle still throws (a use-after-free guard, distinct\n // from never-spawned).\n function getSandboxId(handle: ExecutionHandle): string {\n if (destroyed.has(handle.id))\n throw new Error(`Sandbox ${handle.id} not found`)\n return sandboxes.get(handle.id) ?? handle.id\n }\n\n return {\n type: 'sandbox',\n\n capabilities: {\n shell: true,\n filesystem: true,\n glob: true,\n network: true,\n gpu: false,\n } satisfies ContextCapabilities,\n\n async spawn(config?: SpawnConfig): Promise<ExecutionHandle> {\n const result = await provider.spawn(config ?? {})\n const handle: ExecutionHandle = { id: result.id, type: 'sandbox', cwd: result.cwd }\n sandboxes.set(handle.id, result.id)\n return handle\n },\n\n async exec(handle: ExecutionHandle, command: string, options?): Promise<ExecResult> {\n return provider.exec(getSandboxId(handle), command, options)\n },\n\n async readFile(handle: ExecutionHandle, path: string): Promise<string> {\n return provider.readFile(getSandboxId(handle), path)\n },\n\n async fileSize(handle: ExecutionHandle, path: string): Promise<number | null> {\n // Shared exec-probed capability (see exec-file-size.ts). Probe\n // through the sandbox's shell rather than the provider's file API:\n // `readFile` buffers the WHOLE file into host memory, so the\n // whole-read guard needs the size BEFORE any transfer happens. One\n // exec round-trip per whole-read tool call (read_file / edit) — the\n // price of not ballooning the host on a multi-GB sandbox file.\n return fileSizeViaExec(command => provider.exec(getSandboxId(handle), command), path)\n },\n\n async writeFile(handle: ExecutionHandle, path: string, content: string): Promise<void> {\n return provider.writeFile(getSandboxId(handle), path, content)\n },\n\n async listFiles(handle: ExecutionHandle, path: string, options?: ListFilesOptions): Promise<string[]> {\n const sandboxId = getSandboxId(handle)\n if (options?.glob) {\n return globFilesViaShell(\n path,\n command => provider.exec(sandboxId, command, { cwd: handle.cwd }),\n options?.limit ?? 1000,\n )\n }\n return provider.listFiles(sandboxId, path, options)\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n const id = sandboxes.get(handle.id)\n if (!id)\n return\n await provider.destroy(id)\n sandboxes.delete(handle.id)\n // Mark as torn down so a later op on this exact handle fails loudly\n // (use-after-free) instead of silently re-attaching by id.\n destroyed.add(handle.id)\n },\n }\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"sandbox.js","names":[],"sources":["../../src/contexts/sandbox.ts"],"sourcesContent":["/**\n * Remote sandbox execution context.\n *\n * Offloads execution to a remote sandbox API (e.g. Rivet, E2B).\n * Specific providers implement the SandboxProvider interface.\n */\n\nimport type { ContextCapabilities, ExecResult, ExecutionContext, ExecutionHandle, ListFilesOptions, SpawnConfig, StreamHandle } from './types'\nimport { fileSizeViaExec } from './exec-file-size'\nimport { globFilesViaShell } from './glob-shell'\n\n// ---------------------------------------------------------------------------\n// Sandbox provider interface\n// ---------------------------------------------------------------------------\n\nexport interface SandboxProvider {\n name: string\n spawn: (config: SpawnConfig) => Promise<{ id: string, cwd: string }>\n exec: (sandboxId: string, command: string, options?: { cwd?: string, env?: Record<string, string>, timeout?: number }) => Promise<ExecResult>\n readFile: (sandboxId: string, path: string) => Promise<string>\n writeFile: (sandboxId: string, path: string, content: string) => Promise<void>\n listFiles: (sandboxId: string, path: string, options?: ListFilesOptions) => Promise<string[]>\n destroy: (sandboxId: string) => Promise<void>\n /**\n * Optional live bidirectional stream — see {@link ExecutionContext.execStream}\n * for the implementor contract. Providers whose backing API has no stdin\n * channel omit it; `createSandboxContext` then leaves `execStream` off the\n * context entirely so `capabilities.stream` stays false and callers get a\n * clear \"unsupported\" error instead of a silent host spawn.\n */\n execStream?: (\n sandboxId: string,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, signal?: AbortSignal },\n ) => Promise<StreamHandle>\n}\n\n// ---------------------------------------------------------------------------\n// Sandbox execution context\n// ---------------------------------------------------------------------------\n\nexport function createSandboxContext(provider: SandboxProvider): ExecutionContext {\n const sandboxes = new Map<string, string>()\n const destroyed = new Set<string>()\n\n // The handle id IS the sandbox id (set in `spawn` below), so the map is an\n // identity record of sandboxes this context instance spawned. A handle this\n // instance never spawned falls back to `handle.id` — the re-attach path for\n // durable hosts that journal the handle and hand it back via\n // `AgentOptions.handle` on a replay where `spawn` never re-ran. An\n // EXPLICITLY-destroyed handle still throws (a use-after-free guard, distinct\n // from never-spawned).\n function getSandboxId(handle: ExecutionHandle): string {\n if (destroyed.has(handle.id))\n throw new Error(`Sandbox ${handle.id} not found`)\n return sandboxes.get(handle.id) ?? handle.id\n }\n\n return {\n type: 'sandbox',\n\n capabilities: {\n shell: true,\n filesystem: true,\n glob: true,\n network: true,\n gpu: false,\n stream: provider.execStream !== undefined,\n } satisfies ContextCapabilities,\n\n async spawn(config?: SpawnConfig): Promise<ExecutionHandle> {\n const result = await provider.spawn(config ?? {})\n const handle: ExecutionHandle = { id: result.id, type: 'sandbox', cwd: result.cwd }\n sandboxes.set(handle.id, result.id)\n return handle\n },\n\n async exec(handle: ExecutionHandle, command: string, options?): Promise<ExecResult> {\n return provider.exec(getSandboxId(handle), command, options)\n },\n\n // Conditionally present: `capabilities.stream` is derived from the\n // provider, so the method must disappear too when the provider has no\n // stdin channel. A stub that always throws would make the two disagree.\n ...(provider.execStream\n ? {\n execStream: async (\n handle: ExecutionHandle,\n command: string,\n options?: { cwd?: string, env?: Record<string, string>, signal?: AbortSignal },\n ): Promise<StreamHandle> => provider.execStream!(getSandboxId(handle), command, options),\n }\n : {}),\n\n async readFile(handle: ExecutionHandle, path: string): Promise<string> {\n return provider.readFile(getSandboxId(handle), path)\n },\n\n async fileSize(handle: ExecutionHandle, path: string): Promise<number | null> {\n // Shared exec-probed capability (see exec-file-size.ts). Probe\n // through the sandbox's shell rather than the provider's file API:\n // `readFile` buffers the WHOLE file into host memory, so the\n // whole-read guard needs the size BEFORE any transfer happens. One\n // exec round-trip per whole-read tool call (read_file / edit) — the\n // price of not ballooning the host on a multi-GB sandbox file.\n return fileSizeViaExec(command => provider.exec(getSandboxId(handle), command), path)\n },\n\n async writeFile(handle: ExecutionHandle, path: string, content: string): Promise<void> {\n return provider.writeFile(getSandboxId(handle), path, content)\n },\n\n async listFiles(handle: ExecutionHandle, path: string, options?: ListFilesOptions): Promise<string[]> {\n const sandboxId = getSandboxId(handle)\n if (options?.glob) {\n return globFilesViaShell(\n path,\n command => provider.exec(sandboxId, command, { cwd: handle.cwd }),\n options?.limit ?? 1000,\n )\n }\n return provider.listFiles(sandboxId, path, options)\n },\n\n async destroy(handle: ExecutionHandle): Promise<void> {\n const id = sandboxes.get(handle.id)\n if (!id)\n return\n await provider.destroy(id)\n sandboxes.delete(handle.id)\n // Mark as torn down so a later op on this exact handle fails loudly\n // (use-after-free) instead of silently re-attaching by id.\n destroyed.add(handle.id)\n },\n }\n}\n"],"mappings":";;AAyCA,SAAgB,qBAAqB,UAA6C;CAChF,MAAM,4BAAY,IAAI,IAAoB;CAC1C,MAAM,4BAAY,IAAI,IAAY;CASlC,SAAS,aAAa,QAAiC;EACrD,IAAI,UAAU,IAAI,OAAO,EAAE,GACzB,MAAM,IAAI,MAAM,WAAW,OAAO,GAAG,WAAW;EAClD,OAAO,UAAU,IAAI,OAAO,EAAE,KAAK,OAAO;CAC5C;CAEA,OAAO;EACL,MAAM;EAEN,cAAc;GACZ,OAAO;GACP,YAAY;GACZ,MAAM;GACN,SAAS;GACT,KAAK;GACL,QAAQ,SAAS,eAAe,KAAA;EAClC;EAEA,MAAM,MAAM,QAAgD;GAC1D,MAAM,SAAS,MAAM,SAAS,MAAM,UAAU,CAAC,CAAC;GAChD,MAAM,SAA0B;IAAE,IAAI,OAAO;IAAI,MAAM;IAAW,KAAK,OAAO;GAAI;GAClF,UAAU,IAAI,OAAO,IAAI,OAAO,EAAE;GAClC,OAAO;EACT;EAEA,MAAM,KAAK,QAAyB,SAAiB,SAA+B;GAClF,OAAO,SAAS,KAAK,aAAa,MAAM,GAAG,SAAS,OAAO;EAC7D;EAKA,GAAI,SAAS,aACT,EACE,YAAY,OACV,QACA,SACA,YAC0B,SAAS,WAAY,aAAa,MAAM,GAAG,SAAS,OAAO,EACzF,IACA,CAAC;EAEL,MAAM,SAAS,QAAyB,MAA+B;GACrE,OAAO,SAAS,SAAS,aAAa,MAAM,GAAG,IAAI;EACrD;EAEA,MAAM,SAAS,QAAyB,MAAsC;GAO5E,OAAO,iBAAgB,YAAW,SAAS,KAAK,aAAa,MAAM,GAAG,OAAO,GAAG,IAAI;EACtF;EAEA,MAAM,UAAU,QAAyB,MAAc,SAAgC;GACrF,OAAO,SAAS,UAAU,aAAa,MAAM,GAAG,MAAM,OAAO;EAC/D;EAEA,MAAM,UAAU,QAAyB,MAAc,SAA+C;GACpG,MAAM,YAAY,aAAa,MAAM;GACrC,IAAI,SAAS,MACX,OAAO,kBACL,OACA,YAAW,SAAS,KAAK,WAAW,SAAS,EAAE,KAAK,OAAO,IAAI,CAAC,GAChE,SAAS,SAAS,GACpB;GAEF,OAAO,SAAS,UAAU,WAAW,MAAM,OAAO;EACpD;EAEA,MAAM,QAAQ,QAAwC;GACpD,MAAM,KAAK,UAAU,IAAI,OAAO,EAAE;GAClC,IAAI,CAAC,IACH;GACF,MAAM,SAAS,QAAQ,EAAE;GACzB,UAAU,OAAO,OAAO,EAAE;GAG1B,UAAU,IAAI,OAAO,EAAE;EACzB;CACF;AACF"}
|
|
@@ -194,6 +194,8 @@ function createProcessContext(config) {
|
|
|
194
194
|
* (Node warns past 10 — a long-running session that switches sessions
|
|
195
195
|
* frequently would otherwise hit that).
|
|
196
196
|
*/
|
|
197
|
+
/** Live `execStream` children → owning handle id. See `exitHandler`. */
|
|
198
|
+
const streamChildren = /* @__PURE__ */ new Map();
|
|
197
199
|
let exitHandlerRegistered = false;
|
|
198
200
|
const exitHandler = () => {
|
|
199
201
|
for (const task of tasks.values()) {
|
|
@@ -205,6 +207,13 @@ function createProcessContext(config) {
|
|
|
205
207
|
else process.kill(pid, "SIGTERM");
|
|
206
208
|
} catch {}
|
|
207
209
|
}
|
|
210
|
+
for (const child of streamChildren.keys()) killProcessGroup(child, "SIGTERM");
|
|
211
|
+
};
|
|
212
|
+
const registerExitHandler = () => {
|
|
213
|
+
if (!exitHandlerRegistered) {
|
|
214
|
+
process.on("exit", exitHandler);
|
|
215
|
+
exitHandlerRegistered = true;
|
|
216
|
+
}
|
|
208
217
|
};
|
|
209
218
|
return {
|
|
210
219
|
type: "process",
|
|
@@ -214,6 +223,7 @@ function createProcessContext(config) {
|
|
|
214
223
|
glob: true,
|
|
215
224
|
network: true,
|
|
216
225
|
gpu: false,
|
|
226
|
+
stream: true,
|
|
217
227
|
detachedTasks: "process-lifetime"
|
|
218
228
|
},
|
|
219
229
|
async spawn(overrides) {
|
|
@@ -382,6 +392,82 @@ function createProcessContext(config) {
|
|
|
382
392
|
}
|
|
383
393
|
return (await readdir(await resolveContained(handle, path), { withFileTypes: true })).map((d) => d.isDirectory() ? `${d.name}/` : d.name);
|
|
384
394
|
},
|
|
395
|
+
/**
|
|
396
|
+
* Live bidirectional stream to a child process — see
|
|
397
|
+
* `ExecutionContext.execStream` for the seam contract.
|
|
398
|
+
*
|
|
399
|
+
* The reference implementation: an ordinary piped `spawn`, so both
|
|
400
|
+
* streams stay separate and no terminal line discipline sits between
|
|
401
|
+
* the caller and the child. Sandbox-backed contexts have to work
|
|
402
|
+
* harder to reach the same shape.
|
|
403
|
+
*
|
|
404
|
+
* Unlike `execBackground`, nothing here lands in the `tasks` registry:
|
|
405
|
+
* this handle is owned by its caller (an MCP transport's `close()`),
|
|
406
|
+
* not by the model-facing background-task list that `shell_kill` /
|
|
407
|
+
* `wait_task` walk. It IS tracked in `streamChildren` so the shared
|
|
408
|
+
* orphan reaper and `destroy()` can sweep it — detached process-group
|
|
409
|
+
* semantics (needed so `kill()` takes the child's own children with
|
|
410
|
+
* it) are exactly what would otherwise leak a live server per crash.
|
|
411
|
+
*/
|
|
412
|
+
async execStream(handle, command, options) {
|
|
413
|
+
const cwd = options?.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
|
|
414
|
+
const spawnChild = await loadSpawn();
|
|
415
|
+
options?.signal?.throwIfAborted();
|
|
416
|
+
const child = spawnChild("/bin/sh", ["-c", command], {
|
|
417
|
+
cwd,
|
|
418
|
+
env: {
|
|
419
|
+
...baseEnv,
|
|
420
|
+
...defaultEnv,
|
|
421
|
+
...options?.env
|
|
422
|
+
},
|
|
423
|
+
stdio: [
|
|
424
|
+
"pipe",
|
|
425
|
+
"pipe",
|
|
426
|
+
"pipe"
|
|
427
|
+
],
|
|
428
|
+
detached: SUPPORTS_PROCESS_GROUPS
|
|
429
|
+
});
|
|
430
|
+
registerExitHandler();
|
|
431
|
+
streamChildren.set(child, handle.id);
|
|
432
|
+
let spawnError = null;
|
|
433
|
+
child.on("error", (err) => {
|
|
434
|
+
spawnError = err;
|
|
435
|
+
});
|
|
436
|
+
const exited = new Promise((resolveExit) => {
|
|
437
|
+
child.once("close", (code, signalName) => {
|
|
438
|
+
streamChildren.delete(child);
|
|
439
|
+
resolveExit(code !== null ? code : signalName === "SIGTERM" ? 143 : signalName ? 128 : 1);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
const kill = async () => {
|
|
443
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
444
|
+
const pid = child.pid;
|
|
445
|
+
if (pid === void 0) return;
|
|
446
|
+
try {
|
|
447
|
+
if (SUPPORTS_PROCESS_GROUPS) process.kill(-pid, "SIGTERM");
|
|
448
|
+
else process.kill(pid, "SIGTERM");
|
|
449
|
+
} catch {}
|
|
450
|
+
};
|
|
451
|
+
if (options?.signal) if (options.signal.aborted) kill();
|
|
452
|
+
else options.signal.addEventListener("abort", () => {
|
|
453
|
+
kill();
|
|
454
|
+
}, { once: true });
|
|
455
|
+
return {
|
|
456
|
+
pid: child.pid ?? -1,
|
|
457
|
+
stdout: child.stdout,
|
|
458
|
+
stderr: child.stderr,
|
|
459
|
+
write: async (data) => {
|
|
460
|
+
if (spawnError) throw spawnError;
|
|
461
|
+
const stdin = child.stdin;
|
|
462
|
+
if (!stdin || stdin.destroyed) throw new Error(`execStream: stdin is closed (pid ${child.pid ?? -1})`);
|
|
463
|
+
await new Promise((resolveWrite, rejectWrite) => {
|
|
464
|
+
stdin.write(data, (err) => err ? rejectWrite(err) : resolveWrite());
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
kill,
|
|
468
|
+
exited
|
|
469
|
+
};
|
|
470
|
+
},
|
|
385
471
|
async execBackground(handle, command, options) {
|
|
386
472
|
const cwd = options.cwd ? resolve(handle.cwd, options.cwd) : handle.cwd;
|
|
387
473
|
const spawnChild = await loadSpawn();
|
|
@@ -389,10 +475,7 @@ function createProcessContext(config) {
|
|
|
389
475
|
const taskId = `bash_${++taskCounter}`;
|
|
390
476
|
assertSafeTaskId(taskId);
|
|
391
477
|
const outputPath = resolve(options.outputDir, `${taskId}.${contextTimestamp}.log`);
|
|
392
|
-
|
|
393
|
-
process.on("exit", exitHandler);
|
|
394
|
-
exitHandlerRegistered = true;
|
|
395
|
-
}
|
|
478
|
+
registerExitHandler();
|
|
396
479
|
const outputStream = createWriteStream(outputPath, { flags: "a" });
|
|
397
480
|
outputStream.on("error", (err) => {
|
|
398
481
|
if (process.env.ZIDANE_DEBUG) process.stderr.write(`[zidane/contexts] task ${taskId} log stream error: ${err.message}\n`);
|
|
@@ -621,6 +704,10 @@ function createProcessContext(config) {
|
|
|
621
704
|
});
|
|
622
705
|
}));
|
|
623
706
|
for (const [taskId, state] of tasks) if (state.handleId === handle.id) tasks.delete(taskId);
|
|
707
|
+
for (const [child, ownerId] of streamChildren) if (ownerId === handle.id) {
|
|
708
|
+
killProcessGroup(child, "SIGTERM");
|
|
709
|
+
streamChildren.delete(child);
|
|
710
|
+
}
|
|
624
711
|
handles.delete(handle.id);
|
|
625
712
|
if (exitHandlerRegistered && handles.size === 0) {
|
|
626
713
|
process.off("exit", exitHandler);
|
|
@@ -694,4 +781,4 @@ function resolveDetachedTasksCapability(context) {
|
|
|
694
781
|
//#endregion
|
|
695
782
|
export { killProcessGroup as i, SUPPORTS_PROCESS_GROUPS as n, createProcessContext as r, resolveDetachedTasksCapability as t };
|
|
696
783
|
|
|
697
|
-
//# sourceMappingURL=contexts-
|
|
784
|
+
//# sourceMappingURL=contexts-DbMuUPjS.js.map
|