@agentproto/sandbox-e2b 0.3.7 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,19 +29,53 @@ try {
29
29
 
30
30
  Requires `E2B_API_KEY` in the host process's environment.
31
31
 
32
+ ## Port exposure
33
+
34
+ Each booted sandbox exposes an `expose(port)` method that returns the public
35
+ URL for any port the sandboxed process listens on:
36
+
37
+ ```ts
38
+ const host = await createSandboxAgentSessionHost({ provider: e2bSandboxProvider, spec, secrets })
39
+ // After booting, expose any app port the sandboxed process is listening on:
40
+ const { url } = await host.expose!(3210)
41
+ // url → "https://3210-<sandboxId>.e2b.app"
42
+ ```
43
+
44
+ To pre-expose ports at boot time, declare `extraPorts` in the spec. The
45
+ returned `BootedSandbox.ports` map (and the session descriptor's `sandboxPorts`
46
+ field) are populated immediately so orchestrators can read the URL without a
47
+ separate `expose()` call:
48
+
49
+ ```ts
50
+ const host = await createSandboxAgentSessionHost({
51
+ provider: e2bSandboxProvider,
52
+ spec: { provider: "e2b", config: {}, extraPorts: [3210] },
53
+ secrets: { slugs: ["E2B_API_KEY"] },
54
+ })
55
+ console.log(host.ports) // { 3210: "https://3210-<sandboxId>.e2b.app" }
56
+ ```
57
+
58
+ Loopback bind is enough inside the VM — e2b's edge handles forwarding. See
59
+ `SandboxPortExposureUnsupportedError` in `@agentproto/sandbox` for the error
60
+ thrown when calling `exposePort()` on a provider that does not support exposure.
61
+
32
62
  ## Notes
33
63
 
34
64
  - The daemon's own origin allowlist defaults to `localhost:*`; this provider
35
65
  opens it for the sandbox's own public host (`--allow-origin
36
66
  https://<getHost>`) so the host process can reach it over
37
67
  `https://<getHost>/mcp`.
38
- - The pre-built `agentproto-workstation` template can lag behind the latest
39
- `@agentproto/cli` release. This provider runs `npm i -g
40
- @agentproto/cli@<cliVersion>` before starting the daemon (`cliVersion` defaults
41
- to `@latest`; pin it in `spec.config` for reproducible boots). Set
42
- `updateCliOnBoot: false` in `spec.config` to skip the install entirely e.g.
43
- once the template is rebuilt against a current release, which is the cleaner
44
- long-term fix.
68
+ <!-- sync-templates:start -->
69
+ - The default `agentproto-workstation` template is declared in
70
+ `templates/workstation/versions.json`: `@agentproto/cli@0.17.0`, `@agentproto/adapter-hermes@0.4.10`, `@agentproto/adapter-mastra-agent@0.6.0`, `@agentproto/adapter-opencode@1.1.10`, `opencode-ai@1.18.28`. The on-boot
71
+ `npm i -g` is skipped by default only once the template's recorded `baked`
72
+ block PROVES the image already carries the requested pin; until then the
73
+ legacy boot install stays on.
74
+ <!-- sync-templates:end -->
75
+ - Custom (non-baked) templates can lag behind: the provider runs `npm i -g
76
+ @agentproto/cli@<cliVersion>` before starting the daemon on them (`cliVersion`
77
+ defaults to `@latest`; pin it in `spec.config` for reproducible boots). Set
78
+ `updateCliOnBoot: false` in `spec.config` to skip the install entirely.
45
79
 
46
80
  ## License
47
81
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { SandboxProvider } from '@agentproto/sandbox';
2
2
 
3
+ /**
4
+ * GENERATED by scripts/sync-templates.mjs from templates/workstation/versions.json.
5
+ * DO NOT EDIT — edit versions.json and re-run the sync script instead.
6
+ */
7
+ /** Published stable e2b template id baked with the pins below. */
8
+ declare const DEFAULT_TEMPLATE = "53ybr99wdfgoebi9nee8";
9
+
3
10
  /**
4
11
  * e2b `SandboxProvider` — boots the pre-built `agentproto-workstation`
5
12
  * template, ensures the agentproto daemon is listening on it, and exposes
@@ -19,14 +26,16 @@ import { SandboxProvider } from '@agentproto/sandbox';
19
26
  * The workstation template MAY already autostart the daemon; since that
20
27
  * can't be verified without a live template, this provider checks health
21
28
  * first and only issues the start command when the daemon isn't already
22
- * responding — correct either way. When it does start the daemon, it first
23
- * updates the baked `@agentproto/cli` (the pre-built template can lag
24
- * behind verified stale against a live template) so callers aren't stuck
25
- * on whatever agentproto version the template was last baked with.
29
+ * responding — correct either way. When it does start the daemon on a
30
+ * non-proven template, it first updates the CLI (custom templates can lag
31
+ * behind). The boot install is skipped by default ONLY when the template's
32
+ * recorded `baked` block (templates/workstation/versions.json, via the
33
+ * generated module) proves the image already carries the requested pin —
34
+ * an unproven bake (`baked: null` fields, e.g. the current out-of-band
35
+ * stable image) keeps the legacy install. An explicit `updateCliOnBoot`
36
+ * overrides either way.
26
37
  */
27
38
 
28
- /** Pre-built template that bakes @agentproto/cli + adapters + node + git. */
29
- declare const DEFAULT_TEMPLATE = "53ybr99wdfgoebi9nee8";
30
39
  /** `e2bSandboxProvider.boot`/`.connect` — see module docs. */
31
40
  declare const e2bSandboxProvider: SandboxProvider;
32
41
 
package/dist/index.mjs CHANGED
@@ -5,7 +5,39 @@ import { Sandbox } from 'e2b';
5
5
  * e2b SandboxProvider for @agentproto/sandbox.
6
6
  */
7
7
 
8
+
9
+ // src/template-versions.generated.ts
8
10
  var DEFAULT_TEMPLATE = "53ybr99wdfgoebi9nee8";
11
+ var TEMPLATES = {
12
+ stable: {
13
+ id: "53ybr99wdfgoebi9nee8",
14
+ alias: "agentproto-workstation",
15
+ baked: {
16
+ "cli": "0.17.0",
17
+ "adapters": {
18
+ "@agentproto/adapter-hermes": "0.4.10",
19
+ "@agentproto/adapter-mastra-agent": "0.6.0",
20
+ "@agentproto/adapter-opencode": "1.1.10"
21
+ },
22
+ "builtAt": "2026-09-05T20:39:40Z"
23
+ }
24
+ },
25
+ dev: {
26
+ id: "tnqtmeims5q9ex7j9k06",
27
+ alias: "agentproto-workstation-dev",
28
+ baked: {
29
+ "cli": "0.17.0",
30
+ "adapters": {
31
+ "@agentproto/adapter-hermes": "0.4.10",
32
+ "@agentproto/adapter-mastra-agent": "0.6.0",
33
+ "@agentproto/adapter-opencode": "1.1.10"
34
+ },
35
+ "builtAt": "2026-09-05T16:08:13Z"
36
+ }
37
+ }
38
+ };
39
+
40
+ // src/provider.ts
9
41
  var DEFAULT_PORT = 18790;
10
42
  var DEFAULT_SANDBOX_TIMEOUT_MS = 45 * 6e4;
11
43
  var DEFAULT_WORKSPACE = "/home/user";
@@ -30,13 +62,20 @@ function readE2bConfig(spec) {
30
62
  setupCommands: Array.isArray(config.setupCommands) ? config.setupCommands.filter((c) => typeof c === "string" && c.length > 0) : void 0
31
63
  };
32
64
  }
33
- async function ensureDaemonHealthy(sandbox, host, port, workspace, config, env) {
65
+ function resolveUpdateCli(config, template, templates = TEMPLATES) {
66
+ if (config.updateCliOnBoot !== void 0) return config.updateCliOnBoot;
67
+ if ((config.installPackages?.length ?? 0) > 0) return true;
68
+ const baked = Object.values(templates).find((entry) => entry.id !== null && entry.id === template)?.baked;
69
+ if (!baked || baked.cli === null) return true;
70
+ return config.cliVersion !== void 0 && config.cliVersion !== baked.cli;
71
+ }
72
+ async function ensureDaemonHealthy(sandbox, host, port, workspace, config, env, updateCli) {
34
73
  const healthUrl = `https://${host}/health`;
35
74
  const healthProbeTimeoutMs = config.healthProbeTimeoutMs ?? HEALTH_PROBE_TIMEOUT_MS;
36
75
  const daemonReadyTimeoutMs = config.daemonReadyTimeoutMs ?? DAEMON_READY_TIMEOUT_MS;
37
76
  const pollIntervalMs = config.pollIntervalMs ?? POLL_INTERVAL_MS;
38
77
  const alreadyUp = await probeHealth(healthUrl, healthProbeTimeoutMs, pollIntervalMs);
39
- if (!alreadyUp && (config.updateCliOnBoot ?? true)) {
78
+ if (!alreadyUp && updateCli) {
40
79
  const extras = (config.installPackages ?? []).map((spec) => ` '${spec.replace(/'/g, "")}'`).join("");
41
80
  const cli = `@agentproto/cli@${config.cliVersion ?? "latest"}`;
42
81
  await sandbox.commands.run(`sudo npm i -g ${cli}${extras}`, {
@@ -48,7 +87,7 @@ async function ensureDaemonHealthy(sandbox, host, port, workspace, config, env)
48
87
  await sandbox.commands.run(command, { envs: env });
49
88
  }
50
89
  if (alreadyUp) return;
51
- await sandbox.commands.run(
90
+ const serve = await sandbox.commands.run(
52
91
  `agentproto serve --port ${port} --bind 0.0.0.0 --workspace ${workspace} --allow-origin https://${host}`,
53
92
  // timeoutMs: 0 disables e2b's per-command timeout — which DEFAULTS TO 60
54
93
  // SECONDS and applies to `background: true` commands too, silently
@@ -60,19 +99,29 @@ async function ensureDaemonHealthy(sandbox, host, port, workspace, config, env)
60
99
  // SANDBOX (lifetime cap above), not as long as a shell command.
61
100
  { background: true, envs: env, timeoutMs: 0 }
62
101
  );
63
- const ready = await probeHealth(healthUrl, daemonReadyTimeoutMs, pollIntervalMs);
64
- if (!ready) {
65
- await sandbox.kill();
102
+ const outcome = await probeHealthOrExit(healthUrl, serve, daemonReadyTimeoutMs, pollIntervalMs);
103
+ if (!outcome.healthy) {
104
+ const detail = boxDaemonOutput(serve);
66
105
  throw new Error(
67
- `@agentproto/sandbox-e2b: agentproto daemon did not become healthy at ${healthUrl} within ${daemonReadyTimeoutMs}ms (sandbox ${sandbox.sandboxId}).`
106
+ `@agentproto/sandbox-e2b: agentproto daemon did not become healthy at ${healthUrl} within ${daemonReadyTimeoutMs}ms (sandbox ${sandbox.sandboxId})${outcome.exited ? " \u2014 the box daemon process exited during boot" : ""}.${detail ? `
107
+ --- box daemon output ---
108
+ ${detail}` : ""}`
68
109
  );
69
110
  }
70
111
  }
71
- function toBootedSandbox(sandbox, host, token) {
112
+ function toBootedSandbox(sandbox, host, token, extraPorts) {
113
+ const ports = {};
114
+ for (const port of extraPorts ?? []) {
115
+ ports[port] = `https://${sandbox.getHost(port)}`;
116
+ }
72
117
  return {
73
118
  mcpUrl: `https://${host}/mcp`,
74
119
  sandboxId: sandbox.sandboxId,
75
120
  ...token ? { token } : {},
121
+ ...Object.keys(ports).length > 0 ? { ports } : {},
122
+ expose(port) {
123
+ return Promise.resolve({ url: `https://${sandbox.getHost(port)}` });
124
+ },
76
125
  async stop() {
77
126
  await sandbox.kill();
78
127
  },
@@ -92,9 +141,14 @@ var e2bSandboxProvider = {
92
141
  envs: opts.env,
93
142
  timeoutMs: config.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS
94
143
  });
95
- const host = sandbox.getHost(port);
96
- await ensureDaemonHealthy(sandbox, host, port, workspace, config, opts.env);
97
- return toBootedSandbox(sandbox, host);
144
+ try {
145
+ const host = sandbox.getHost(port);
146
+ await ensureDaemonHealthy(sandbox, host, port, workspace, config, opts.env, resolveUpdateCli(config, template));
147
+ return toBootedSandbox(sandbox, host, void 0, spec.extraPorts);
148
+ } catch (err) {
149
+ await sandbox.kill().catch(() => void 0);
150
+ throw err;
151
+ }
98
152
  },
99
153
  async connect(sandboxId, spec, opts) {
100
154
  const config = readE2bConfig(spec);
@@ -102,12 +156,20 @@ var e2bSandboxProvider = {
102
156
  const workspace = config.workspace ?? DEFAULT_WORKSPACE;
103
157
  const sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY });
104
158
  await sandbox.setTimeout(config.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS);
105
- const host = sandbox.getHost(port);
106
- await ensureDaemonHealthy(sandbox, host, port, workspace, config, opts.env);
107
- const token = opts.expose === "private" ? sandbox.trafficAccessToken : void 0;
108
- return toBootedSandbox(sandbox, host, token);
159
+ try {
160
+ const host = sandbox.getHost(port);
161
+ await ensureDaemonHealthy(sandbox, host, port, workspace, config, opts.env, resolveUpdateCli(config, config.template ?? DEFAULT_TEMPLATE));
162
+ return finishConnect(sandbox, host, opts, spec);
163
+ } catch (err) {
164
+ await sandbox.kill().catch(() => void 0);
165
+ throw err;
166
+ }
109
167
  }
110
168
  };
169
+ function finishConnect(sandbox, host, opts, spec) {
170
+ const token = opts.expose === "private" ? sandbox.trafficAccessToken : void 0;
171
+ return toBootedSandbox(sandbox, host, token, spec.extraPorts);
172
+ }
111
173
  async function probeHealth(url, timeoutMs, pollIntervalMs) {
112
174
  const deadline = Date.now() + timeoutMs;
113
175
  for (; ; ) {
@@ -120,6 +182,35 @@ async function probeHealth(url, timeoutMs, pollIntervalMs) {
120
182
  await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
121
183
  }
122
184
  }
185
+ async function probeHealthOrExit(url, handle, timeoutMs, pollIntervalMs) {
186
+ const deadline = Date.now() + timeoutMs;
187
+ let exited = false;
188
+ if (typeof handle.wait === "function") {
189
+ void handle.wait().then(
190
+ () => {
191
+ exited = true;
192
+ },
193
+ () => {
194
+ exited = true;
195
+ }
196
+ );
197
+ }
198
+ for (; ; ) {
199
+ try {
200
+ const res = await fetch(url, { signal: AbortSignal.timeout(pollIntervalMs) });
201
+ if (res.ok) return { healthy: true, exited: false };
202
+ } catch {
203
+ }
204
+ if (exited || handle.exitCode !== void 0) return { healthy: false, exited: true };
205
+ if (Date.now() >= deadline) return { healthy: false, exited: false };
206
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
207
+ }
208
+ }
209
+ function boxDaemonOutput(handle) {
210
+ const joined = [handle.stderr ?? "", handle.stdout ?? ""].map((s) => s.trim()).filter(Boolean).join("\n");
211
+ const MAX = 2e3;
212
+ return joined.length > MAX ? `\u2026${joined.slice(-MAX)}` : joined;
213
+ }
123
214
 
124
215
  export { DEFAULT_TEMPLATE, e2bSandboxProvider };
125
216
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/provider.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * e2b `SandboxProvider` — boots the pre-built `agentproto-workstation`\n * template, ensures the agentproto daemon is listening on it, and exposes\n * the daemon's `/mcp` endpoint via `sandbox.getHost()` so\n * `createSandboxAgentSessionHost` (`@agentproto/sandbox`) can hand it\n * straight to `connectDaemonAgentSessionHost`.\n *\n * `getHost(port)` returns a publicly reachable hostname for that port\n * directly — e2b's per-sandbox auth token (`SandboxOpts.secure`) gates the\n * SDK's own control-plane (envd) traffic, not arbitrary listening ports the\n * sandbox process opens, so no extra auth header is threaded through here.\n * The daemon's OWN origin allowlist is a separate gate, though: it defaults\n * to `localhost:*`, which rejects a connection from this host process\n * against the sandbox's public `https://<getHost>` origin. `--allow-origin`\n * is passed the sandbox's own getHost origin to open that gate.\n *\n * The workstation template MAY already autostart the daemon; since that\n * can't be verified without a live template, this provider checks health\n * first and only issues the start command when the daemon isn't already\n * responding — correct either way. When it does start the daemon, it first\n * updates the baked `@agentproto/cli` (the pre-built template can lag\n * behind — verified stale against a live template) so callers aren't stuck\n * on whatever agentproto version the template was last baked with.\n */\n\nimport { Sandbox } from \"e2b\"\nimport type { BootedSandbox, SandboxBootOpts, SandboxProvider, SandboxSpec } from \"@agentproto/sandbox\"\n\n/** Pre-built template that bakes @agentproto/cli + adapters + node + git. */\nexport const DEFAULT_TEMPLATE = \"53ybr99wdfgoebi9nee8\"\n\n/** `serve.ts`'s default port (`DEFAULT_MCP_URL` in `@agentproto/harness`). */\nconst DEFAULT_PORT = 18790\n/**\n * Default sandbox LIFETIME. e2b's own default is 5 minutes (`SandboxOpts.\n * timeoutMs @default 300_000`) — lethal for real agent work: boot (~30s) +\n * `npm i -g` (~90s) + a multi-minute agent turn crosses 5:00 mid-turn, e2b\n * reaps the VM, and the next request cold-boots it WITHOUT the daemon —\n * surfacing as the baffling `\"The sandbox is running but port is not open\"`\n * 502 (observed live killing a PR-review turn). 45 minutes covers any\n * plausible session; `config.timeoutMs` still overrides in either direction.\n */\nconst DEFAULT_SANDBOX_TIMEOUT_MS = 45 * 60_000\nconst DEFAULT_WORKSPACE = \"/home/user\"\nconst HEALTH_PROBE_TIMEOUT_MS = 3_000\nconst DAEMON_READY_TIMEOUT_MS = 30_000\nconst POLL_INTERVAL_MS = 500\nconst UPDATE_CLI_TIMEOUT_MS = 120_000\n\ninterface E2bSandboxConfig {\n /** Template name or id. Defaults to the `agentproto-workstation` template. */\n template?: string\n /** Port the agentproto daemon listens on inside the sandbox. Default 18790. */\n port?: number\n /** `--workspace` passed to `agentproto serve`. Default `/home/user`. */\n workspace?: string\n /** Sandbox lifetime cap, forwarded to `Sandbox.create`. */\n timeoutMs?: number\n /** How long to wait for the health probe before assuming the daemon isn't autostarted. */\n healthProbeTimeoutMs?: number\n /** How long to wait for the daemon to become healthy after starting it. */\n daemonReadyTimeoutMs?: number\n /** Delay between health-probe attempts. */\n pollIntervalMs?: number\n /** Run `npm i -g @agentproto/cli@latest` before starting the daemon, so a\n * stale template bake doesn't pin callers to an old agentproto version.\n * Default true. Only runs when this provider is the one starting the\n * daemon (skipped when the health probe finds it already autostarted). */\n updateCliOnBoot?: boolean\n /** CLI version to install on boot, pinned instead of a floating `@latest`.\n * A broken `@agentproto/cli@latest` publish otherwise silently kills the\n * box on boot; pinning a known-good version (sourced from config, defaulting\n * to the repo's declared `cliVersion`) makes the boot install reproducible.\n * Produces `@agentproto/cli@<version>`; when unset, falls back to `@latest`. */\n cliVersion?: string\n /** Extra npm package specs installed globally ALONGSIDE the CLI update\n * (single `npm i -g` invocation). Load-bearing for adapters: the CLI\n * update replaces the global install, LOSING any template-baked\n * `@agentproto/adapter-*` packages (verified against a live template) —\n * so the adapter the caller intends to spawn must be (re)installed in the\n * same breath, e.g. `[\"@agentproto/adapter-claude-sdk@latest\"]` (plus\n * `\"@anthropic-ai/claude-code@latest\"` for the claude-code adapter's\n * underlying CLI). Ignored when `updateCliOnBoot` is false or the daemon\n * was already autostarted healthy. */\n installPackages?: string[]\n /** Timeout for the `npm i -g` update command. Default 120s. */\n updateCliTimeoutMs?: number\n /** Extra shell commands host-executed inside the box AFTER the boot `npm i -g`\n * and BEFORE `agentproto serve` hands control to the agent. Generic\n * provision-time hook — no policy baked in here; callers decide what to run\n * (e.g. installing a git hook). Each entry is passed verbatim to\n * `sandbox.commands.run`. Runs on EVERY boot/connect, even when the health\n * probe finds the daemon already autostarted — entries must be idempotent. */\n setupCommands?: string[]\n}\n\nfunction readE2bConfig(spec: SandboxSpec): E2bSandboxConfig {\n const config = (spec.config ?? {}) as Record<string, unknown>\n return {\n template: typeof config.template === \"string\" ? config.template : undefined,\n port: typeof config.port === \"number\" ? config.port : undefined,\n workspace: typeof config.workspace === \"string\" ? config.workspace : undefined,\n timeoutMs: typeof config.timeoutMs === \"number\" ? config.timeoutMs : undefined,\n healthProbeTimeoutMs:\n typeof config.healthProbeTimeoutMs === \"number\" ? config.healthProbeTimeoutMs : undefined,\n daemonReadyTimeoutMs:\n typeof config.daemonReadyTimeoutMs === \"number\" ? config.daemonReadyTimeoutMs : undefined,\n pollIntervalMs: typeof config.pollIntervalMs === \"number\" ? config.pollIntervalMs : undefined,\n updateCliOnBoot: typeof config.updateCliOnBoot === \"boolean\" ? config.updateCliOnBoot : undefined,\n cliVersion: typeof config.cliVersion === \"string\" ? config.cliVersion : undefined,\n installPackages: Array.isArray(config.installPackages)\n ? config.installPackages.filter((p): p is string => typeof p === \"string\" && p.length > 0)\n : undefined,\n updateCliTimeoutMs:\n typeof config.updateCliTimeoutMs === \"number\" ? config.updateCliTimeoutMs : undefined,\n setupCommands: Array.isArray(config.setupCommands)\n ? config.setupCommands.filter((c): c is string => typeof c === \"string\" && c.length > 0)\n : undefined,\n }\n}\n\n/**\n * Probe health, and — if the daemon isn't already up — update the (possibly\n * stale) baked CLI and start it, then re-probe. Shared by `boot` (a fresh\n * template's autostart may not have fired yet) and `connect` (a resumed box\n * may wake with a dead daemon process — see PR3 risk \"stale daemon on\n * reconnect\"). Throws (after killing the sandbox) if the daemon never comes\n * up.\n */\nasync function ensureDaemonHealthy(\n sandbox: Sandbox,\n host: string,\n port: number,\n workspace: string,\n config: E2bSandboxConfig,\n env: Record<string, string>,\n): Promise<void> {\n const healthUrl = `https://${host}/health`\n const healthProbeTimeoutMs = config.healthProbeTimeoutMs ?? HEALTH_PROBE_TIMEOUT_MS\n const daemonReadyTimeoutMs = config.daemonReadyTimeoutMs ?? DAEMON_READY_TIMEOUT_MS\n const pollIntervalMs = config.pollIntervalMs ?? POLL_INTERVAL_MS\n\n const alreadyUp = await probeHealth(healthUrl, healthProbeTimeoutMs, pollIntervalMs)\n\n if (!alreadyUp && (config.updateCliOnBoot ?? true)) {\n // One `npm i -g` for the CLI update AND any caller-declared extras\n // (`installPackages` — typically the adapter about to be spawned). The\n // CLI update alone replaces the global install and LOSES template-baked\n // adapters, so the two must land in the same invocation. Extras are\n // single-quoted (with any quote chars stripped) since they arrive from\n // caller-authored spec config.\n const extras = (config.installPackages ?? [])\n .map(spec => ` '${spec.replace(/'/g, \"\")}'`)\n .join(\"\")\n const cli = `@agentproto/cli@${config.cliVersion ?? \"latest\"}`\n await sandbox.commands.run(`sudo npm i -g ${cli}${extras}`, {\n envs: env,\n timeoutMs: config.updateCliTimeoutMs ?? UPDATE_CLI_TIMEOUT_MS,\n })\n }\n // Caller-declared provision hooks — host-executed after the npm install and\n // before the agent gets control. Kept generic (no policy here); the ts layer\n // uses this to install a commit-msg hook that strips AI-attribution trailers.\n // Runs regardless of `alreadyUp`: the workstation template MAY autostart the\n // daemon (module doc above), and a caller-declared hook install must still\n // land on that path — callers rely on `setupCommands` entries being\n // idempotent so re-running them against an already-provisioned box is safe.\n for (const command of config.setupCommands ?? []) {\n await sandbox.commands.run(command, { envs: env })\n }\n\n if (alreadyUp) return\n\n await sandbox.commands.run(\n `agentproto serve --port ${port} --bind 0.0.0.0 --workspace ${workspace} --allow-origin https://${host}`,\n // timeoutMs: 0 disables e2b's per-command timeout — which DEFAULTS TO 60\n // SECONDS and applies to `background: true` commands too, silently\n // SIGKILLing the daemon one minute after boot. That was the root cause of\n // every long agent turn dying with \"sandbox is running but port is not\n // open\" (502) / `MCP error -32001` while short (<60s) probe turns passed\n // — verified live: the daemon's port went dead at exactly serve+60s with\n // the VM healthy and memory flat. The daemon must live as long as the\n // SANDBOX (lifetime cap above), not as long as a shell command.\n { background: true, envs: env, timeoutMs: 0 },\n )\n const ready = await probeHealth(healthUrl, daemonReadyTimeoutMs, pollIntervalMs)\n if (!ready) {\n await sandbox.kill()\n throw new Error(\n `@agentproto/sandbox-e2b: agentproto daemon did not become healthy at ${healthUrl} ` +\n `within ${daemonReadyTimeoutMs}ms (sandbox ${sandbox.sandboxId}).`,\n )\n }\n}\n\n/** Wrap a healthy sandbox handle into the `BootedSandbox` shape common to\n * `boot` and `connect`. `pause()` always keeps the full memory snapshot\n * (`keepMemory: true`, the SDK default) — a filesystem-only pause would\n * cold-boot the box on resume, dropping the running agentproto daemon and\n * any open connections (PR3 risk \"e2b pause loses in-memory state\"). */\nfunction toBootedSandbox(\n sandbox: Awaited<ReturnType<typeof Sandbox.create>>,\n host: string,\n token?: string,\n): BootedSandbox {\n return {\n mcpUrl: `https://${host}/mcp`,\n sandboxId: sandbox.sandboxId,\n ...(token ? { token } : {}),\n async stop(): Promise<void> {\n await sandbox.kill()\n },\n async pause(): Promise<void> {\n await sandbox.pause({ keepMemory: true })\n },\n }\n}\n\n/** `e2bSandboxProvider.boot`/`.connect` — see module docs. */\nexport const e2bSandboxProvider: SandboxProvider = {\n async boot(spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox> {\n const config = readE2bConfig(spec)\n const template = config.template ?? DEFAULT_TEMPLATE\n const port = config.port ?? DEFAULT_PORT\n const workspace = config.workspace ?? DEFAULT_WORKSPACE\n\n const sandbox = await Sandbox.create(template, {\n apiKey: process.env.E2B_API_KEY,\n envs: opts.env,\n timeoutMs: config.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS,\n })\n\n const host = sandbox.getHost(port)\n await ensureDaemonHealthy(sandbox, host, port, workspace, config, opts.env)\n return toBootedSandbox(sandbox, host)\n },\n\n async connect(sandboxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox> {\n const config = readE2bConfig(spec)\n const port = config.port ?? DEFAULT_PORT\n const workspace = config.workspace ?? DEFAULT_WORKSPACE\n\n const sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY })\n // A resumed box keeps its ORIGINAL lifetime deadline — re-arm it so the\n // reconnected session gets the same runway a fresh boot gets (same\n // 5-minute-default trap as `boot`, see DEFAULT_SANDBOX_TIMEOUT_MS).\n await sandbox.setTimeout(config.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS)\n\n const host = sandbox.getHost(port)\n await ensureDaemonHealthy(sandbox, host, port, workspace, config, opts.env)\n // `opts.expose === \"private\"` (only ever set by `attachSandbox`, see\n // @agentproto/runtime) surfaces e2b's own \"restricted public traffic\"\n // token, when the sandbox has one — it gates access to EVERY exposed\n // port, this one included, at e2b's edge (distinct from `SandboxOpts.\n // secure`, which only covers envd's own control-plane traffic — see\n // module docs). Best-effort: e2b's SDK only exposes the token as a\n // readonly property with no documented guarantee it's populated on a\n // reconnect for a sandbox that wasn't created with traffic restriction\n // enabled. Unverified against a live sandbox.\n const token = opts.expose === \"private\" ? sandbox.trafficAccessToken : undefined\n return toBootedSandbox(sandbox, host, token)\n },\n}\n\n/** Poll `url` until it responds OK, or return false once `timeoutMs` elapses. */\nasync function probeHealth(url: string, timeoutMs: number, pollIntervalMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs\n for (;;) {\n try {\n const res = await fetch(url, { signal: AbortSignal.timeout(pollIntervalMs) })\n if (res.ok) return true\n } catch {\n // not up yet — fall through to the deadline check\n }\n if (Date.now() >= deadline) return false\n await new Promise(resolve => setTimeout(resolve, pollIntervalMs))\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/template-versions.generated.ts","../src/provider.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * GENERATED by scripts/sync-templates.mjs from templates/workstation/versions.json.\n * DO NOT EDIT — edit versions.json and re-run the sync script instead.\n */\n\n/** Published stable e2b template id baked with the pins below. */\nexport const DEFAULT_TEMPLATE = \"53ybr99wdfgoebi9nee8\"\nexport const TEMPLATE_ALIASES = {\n stable: \"agentproto-workstation\",\n dev: \"agentproto-workstation-dev\",\n} as const\n/**\n * What each published image was PROVEN to contain at its last real e2b\n * build. A null field means unknown (e.g. out-of-band bake) — consumers\n * must assume the image does NOT match the declared pins.\n */\nexport const TEMPLATES = {\n stable: {\n id: \"53ybr99wdfgoebi9nee8\",\n alias: \"agentproto-workstation\",\n baked: {\n \"cli\": \"0.17.0\",\n \"adapters\": {\n \"@agentproto/adapter-hermes\": \"0.4.10\",\n \"@agentproto/adapter-mastra-agent\": \"0.6.0\",\n \"@agentproto/adapter-opencode\": \"1.1.10\"\n },\n \"builtAt\": \"2026-09-05T20:39:40Z\"\n },\n },\n dev: {\n id: \"tnqtmeims5q9ex7j9k06\",\n alias: \"agentproto-workstation-dev\",\n baked: {\n \"cli\": \"0.17.0\",\n \"adapters\": {\n \"@agentproto/adapter-hermes\": \"0.4.10\",\n \"@agentproto/adapter-mastra-agent\": \"0.6.0\",\n \"@agentproto/adapter-opencode\": \"1.1.10\"\n },\n \"builtAt\": \"2026-09-05T16:08:13Z\"\n },\n },\n} as const\nexport const BAKED_CLI_VERSION = \"0.17.0\"\nexport const BAKED_ADAPTERS = {\n \"@agentproto/adapter-hermes\": \"0.4.10\",\n \"@agentproto/adapter-mastra-agent\": \"0.6.0\",\n \"@agentproto/adapter-opencode\": \"1.1.10\",\n} as const\nexport const BAKED_RUNTIME = {\n \"opencode-ai\": \"1.18.28\",\n} as const\nexport const BAKED_BASE_IMAGE = \"e2bdev/code-interpreter:latest\"\n","/**\n * e2b `SandboxProvider` — boots the pre-built `agentproto-workstation`\n * template, ensures the agentproto daemon is listening on it, and exposes\n * the daemon's `/mcp` endpoint via `sandbox.getHost()` so\n * `createSandboxAgentSessionHost` (`@agentproto/sandbox`) can hand it\n * straight to `connectDaemonAgentSessionHost`.\n *\n * `getHost(port)` returns a publicly reachable hostname for that port\n * directly — e2b's per-sandbox auth token (`SandboxOpts.secure`) gates the\n * SDK's own control-plane (envd) traffic, not arbitrary listening ports the\n * sandbox process opens, so no extra auth header is threaded through here.\n * The daemon's OWN origin allowlist is a separate gate, though: it defaults\n * to `localhost:*`, which rejects a connection from this host process\n * against the sandbox's public `https://<getHost>` origin. `--allow-origin`\n * is passed the sandbox's own getHost origin to open that gate.\n *\n * The workstation template MAY already autostart the daemon; since that\n * can't be verified without a live template, this provider checks health\n * first and only issues the start command when the daemon isn't already\n * responding — correct either way. When it does start the daemon on a\n * non-proven template, it first updates the CLI (custom templates can lag\n * behind). The boot install is skipped by default ONLY when the template's\n * recorded `baked` block (templates/workstation/versions.json, via the\n * generated module) proves the image already carries the requested pin —\n * an unproven bake (`baked: null` fields, e.g. the current out-of-band\n * stable image) keeps the legacy install. An explicit `updateCliOnBoot`\n * overrides either way.\n */\n\nimport { Sandbox } from \"e2b\"\nimport type { BootedSandbox, SandboxBootOpts, SandboxProvider, SandboxSpec } from \"@agentproto/sandbox\"\nimport { DEFAULT_TEMPLATE, TEMPLATES } from \"./template-versions.generated.js\"\n\n/** Re-exported from the generated module (single source:\n * `templates/workstation/versions.json` via scripts/sync-templates.mjs). */\nexport { DEFAULT_TEMPLATE, TEMPLATES }\n\n/** `serve.ts`'s default port (`DEFAULT_MCP_URL` in `@agentproto/harness`). */\nconst DEFAULT_PORT = 18790\n/**\n * Default sandbox LIFETIME. e2b's own default is 5 minutes (`SandboxOpts.\n * timeoutMs @default 300_000`) — lethal for real agent work: boot (~30s) +\n * `npm i -g` (~90s) + a multi-minute agent turn crosses 5:00 mid-turn, e2b\n * reaps the VM, and the next request cold-boots it WITHOUT the daemon —\n * surfacing as the baffling `\"The sandbox is running but port is not open\"`\n * 502 (observed live killing a PR-review turn). 45 minutes covers any\n * plausible session; `config.timeoutMs` still overrides in either direction.\n */\nconst DEFAULT_SANDBOX_TIMEOUT_MS = 45 * 60_000\nconst DEFAULT_WORKSPACE = \"/home/user\"\nconst HEALTH_PROBE_TIMEOUT_MS = 3_000\nconst DAEMON_READY_TIMEOUT_MS = 30_000\nconst POLL_INTERVAL_MS = 500\nconst UPDATE_CLI_TIMEOUT_MS = 120_000\n\ninterface E2bSandboxConfig {\n /** Template name or id. Defaults to the `agentproto-workstation` template. */\n template?: string\n /** Port the agentproto daemon listens on inside the sandbox. Default 18790. */\n port?: number\n /** `--workspace` passed to `agentproto serve`. Default `/home/user`. */\n workspace?: string\n /** Sandbox lifetime cap, forwarded to `Sandbox.create`. */\n timeoutMs?: number\n /** How long to wait for the health probe before assuming the daemon isn't autostarted. */\n healthProbeTimeoutMs?: number\n /** How long to wait for the daemon to become healthy after starting it. */\n daemonReadyTimeoutMs?: number\n /** Delay between health-probe attempts. */\n pollIntervalMs?: number\n /** Run `npm i -g @agentproto/cli@...` before starting the daemon, so a\n * stale template bake doesn't pin callers to an old agentproto version.\n * Default: false when the configured template is the baked stable\n * workstation one AND the requested cliVersion matches its baked pin\n * (the bake already carries exactly that CLI) — true otherwise (custom\n * templates can lag behind). Only runs when this provider is the one\n * starting the daemon (skipped when the health probe finds it already\n * autostarted). */\n updateCliOnBoot?: boolean\n /** CLI version to install on boot, pinned instead of a floating `@latest`.\n * A broken `@agentproto/cli@latest` publish otherwise silently kills the\n * box on boot; pinning a known-good version (sourced from config, defaulting\n * to the repo's declared `cliVersion`) makes the boot install reproducible.\n * Produces `@agentproto/cli@<version>`; when unset, falls back to `@latest`. */\n cliVersion?: string\n /** Extra npm package specs installed globally ALONGSIDE the CLI update\n * (single `npm i -g` invocation). Load-bearing for adapters: the CLI\n * update replaces the global install, LOSING any template-baked\n * `@agentproto/adapter-*` packages (verified against a live template) —\n * so the adapter the caller intends to spawn must be (re)installed in the\n * same breath, e.g. `[\"@agentproto/adapter-claude-sdk@latest\"]` (plus\n * `\"@anthropic-ai/claude-code@latest\"` for the claude-code adapter's\n * underlying CLI). Ignored when `updateCliOnBoot` is false or the daemon\n * was already autostarted healthy. Declaring a non-empty list also\n * re-enables the boot install when `updateCliOnBoot` is unset — entries\n * are never silently dropped on the baked-template skip path. */\n installPackages?: string[]\n /** Timeout for the `npm i -g` update command. Default 120s. */\n updateCliTimeoutMs?: number\n /** Extra shell commands host-executed inside the box AFTER the boot `npm i -g`\n * and BEFORE `agentproto serve` hands control to the agent. Generic\n * provision-time hook — no policy baked in here; callers decide what to run\n * (e.g. installing a git hook). Each entry is passed verbatim to\n * `sandbox.commands.run`. Runs on EVERY boot/connect, even when the health\n * probe finds the daemon already autostarted — entries must be idempotent. */\n setupCommands?: string[]\n}\n\nfunction readE2bConfig(spec: SandboxSpec): E2bSandboxConfig {\n const config = (spec.config ?? {}) as Record<string, unknown>\n return {\n template: typeof config.template === \"string\" ? config.template : undefined,\n port: typeof config.port === \"number\" ? config.port : undefined,\n workspace: typeof config.workspace === \"string\" ? config.workspace : undefined,\n timeoutMs: typeof config.timeoutMs === \"number\" ? config.timeoutMs : undefined,\n healthProbeTimeoutMs:\n typeof config.healthProbeTimeoutMs === \"number\" ? config.healthProbeTimeoutMs : undefined,\n daemonReadyTimeoutMs:\n typeof config.daemonReadyTimeoutMs === \"number\" ? config.daemonReadyTimeoutMs : undefined,\n pollIntervalMs: typeof config.pollIntervalMs === \"number\" ? config.pollIntervalMs : undefined,\n updateCliOnBoot: typeof config.updateCliOnBoot === \"boolean\" ? config.updateCliOnBoot : undefined,\n cliVersion: typeof config.cliVersion === \"string\" ? config.cliVersion : undefined,\n installPackages: Array.isArray(config.installPackages)\n ? config.installPackages.filter((p): p is string => typeof p === \"string\" && p.length > 0)\n : undefined,\n updateCliTimeoutMs:\n typeof config.updateCliTimeoutMs === \"number\" ? config.updateCliTimeoutMs : undefined,\n setupCommands: Array.isArray(config.setupCommands)\n ? config.setupCommands.filter((c): c is string => typeof c === \"string\" && c.length > 0)\n : undefined,\n }\n}\n\n/**\n * Default the on-boot CLI update OFF only for a template whose recorded\n * `baked` block PROVES the image already carries the requested CLI pin —\n * the install would just re-download the same version (90-120s per cold\n * boot, npm registry dependency, and it replaces the template's baked\n * adapters). The gate is on the RECORDED bake, not the template id: an\n * out-of-band bake nobody has verified has `baked.cli: null` and keeps the\n * legacy default of true. A requested cliVersion that differs from the\n * recorded bake also installs. An explicit `updateCliOnBoot` always wins.\n */\nexport function resolveUpdateCli(\n config: Pick<E2bSandboxConfig, \"updateCliOnBoot\" | \"cliVersion\" | \"installPackages\">,\n template: string,\n templates: { [key: string]: { id: string | null; baked: { cli: string | null } } } = TEMPLATES,\n): boolean {\n if (config.updateCliOnBoot !== undefined) return config.updateCliOnBoot\n // Runtime escape hatch: a caller declaring installPackages wants an\n // install to happen — never silently drop them on the skip path.\n if ((config.installPackages?.length ?? 0) > 0) return true\n const baked = Object.values(templates).find(entry => entry.id !== null && entry.id === template)?.baked\n if (!baked || baked.cli === null) return true\n // Recorded bake matches the requested pin (an unset cliVersion means the\n // bake itself is the pin) → nothing to install.\n return config.cliVersion !== undefined && config.cliVersion !== baked.cli\n}\n\n/**\n * Probe health, and — if the daemon isn't already up — update the (possibly\n * stale) baked CLI and start it, then re-probe. Shared by `boot` (a fresh\n * template's autostart may not have fired yet) and `connect` (a resumed box\n * may wake with a dead daemon process — see PR3 risk \"stale daemon on\n * reconnect\"). Throws (after killing the sandbox) if the daemon never comes\n * up.\n */\nasync function ensureDaemonHealthy(\n sandbox: Sandbox,\n host: string,\n port: number,\n workspace: string,\n config: E2bSandboxConfig,\n env: Record<string, string>,\n updateCli: boolean,\n): Promise<void> {\n const healthUrl = `https://${host}/health`\n const healthProbeTimeoutMs = config.healthProbeTimeoutMs ?? HEALTH_PROBE_TIMEOUT_MS\n const daemonReadyTimeoutMs = config.daemonReadyTimeoutMs ?? DAEMON_READY_TIMEOUT_MS\n const pollIntervalMs = config.pollIntervalMs ?? POLL_INTERVAL_MS\n\n const alreadyUp = await probeHealth(healthUrl, healthProbeTimeoutMs, pollIntervalMs)\n\n if (!alreadyUp && updateCli) {\n // One `npm i -g` for the CLI update AND any caller-declared extras\n // (`installPackages` — typically the adapter about to be spawned). The\n // CLI update alone replaces the global install and LOSES template-baked\n // adapters, so the two must land in the same invocation. Extras are\n // single-quoted (with any quote chars stripped) since they arrive from\n // caller-authored spec config.\n const extras = (config.installPackages ?? [])\n .map(spec => ` '${spec.replace(/'/g, \"\")}'`)\n .join(\"\")\n const cli = `@agentproto/cli@${config.cliVersion ?? \"latest\"}`\n await sandbox.commands.run(`sudo npm i -g ${cli}${extras}`, {\n envs: env,\n timeoutMs: config.updateCliTimeoutMs ?? UPDATE_CLI_TIMEOUT_MS,\n })\n }\n // Caller-declared provision hooks — host-executed after the npm install and\n // before the agent gets control. Kept generic (no policy here); the ts layer\n // uses this to install a commit-msg hook that strips AI-attribution trailers.\n // Runs regardless of `alreadyUp`: the workstation template MAY autostart the\n // daemon (module doc above), and a caller-declared hook install must still\n // land on that path — callers rely on `setupCommands` entries being\n // idempotent so re-running them against an already-provisioned box is safe.\n for (const command of config.setupCommands ?? []) {\n await sandbox.commands.run(command, { envs: env })\n }\n\n if (alreadyUp) return\n\n // Start the daemon in the background and KEEP THE HANDLE. Its stdout/stderr\n // accumulate on the handle, and `probeHealthOrExit` races the health poll\n // against the process's own exit: if the daemon dies during boot (e.g. it\n // crashes loading a baked adapter on a broken image) we fail fast with its\n // captured output instead of blocking the caller for the whole readiness\n // window. See PR fix (2026-09-05): a boot that can't become healthy must\n // fail fast with the box's daemon output, never hang the HTTP/MCP call.\n const serve = await sandbox.commands.run(\n `agentproto serve --port ${port} --bind 0.0.0.0 --workspace ${workspace} --allow-origin https://${host}`,\n // timeoutMs: 0 disables e2b's per-command timeout — which DEFAULTS TO 60\n // SECONDS and applies to `background: true` commands too, silently\n // SIGKILLing the daemon one minute after boot. That was the root cause of\n // every long agent turn dying with \"sandbox is running but port is not\n // open\" (502) / `MCP error -32001` while short (<60s) probe turns passed\n // — verified live: the daemon's port went dead at exactly serve+60s with\n // the VM healthy and memory flat. The daemon must live as long as the\n // SANDBOX (lifetime cap above), not as long as a shell command.\n { background: true, envs: env, timeoutMs: 0 },\n )\n const outcome = await probeHealthOrExit(healthUrl, serve, daemonReadyTimeoutMs, pollIntervalMs)\n if (!outcome.healthy) {\n // Deliberately do NOT kill the box here — `boot`/`connect` own that\n // cleanup so EVERY failure path (a throwing npm install, a failing setup\n // hook, or this one) reclaims the VM through a single place and can never\n // leak a running, session-less box (observed live 2026-09-05: the daemon\n // gave up but the box kept running with no session record).\n const detail = boxDaemonOutput(serve)\n throw new Error(\n `@agentproto/sandbox-e2b: agentproto daemon did not become healthy at ${healthUrl} ` +\n `within ${daemonReadyTimeoutMs}ms (sandbox ${sandbox.sandboxId})` +\n `${outcome.exited ? \" — the box daemon process exited during boot\" : \"\"}.` +\n `${detail ? `\\n--- box daemon output ---\\n${detail}` : \"\"}`,\n )\n }\n}\n\n/** Wrap a healthy sandbox handle into the `BootedSandbox` shape common to\n * `boot` and `connect`. `pause()` always keeps the full memory snapshot\n * (`keepMemory: true`, the SDK default) — a filesystem-only pause would\n * cold-boot the box on resume, dropping the running agentproto daemon and\n * any open connections (PR3 risk \"e2b pause loses in-memory state\").\n *\n * `extraPorts` (from `spec.extraPorts`) are resolved eagerly at boot time\n * into the returned `ports` map (port → `https://<getHost(port)>`).\n * `expose(port)` works for any port — not just pre-declared ones — so\n * callers can also expose ports lazily after boot. */\nfunction toBootedSandbox(\n sandbox: Awaited<ReturnType<typeof Sandbox.create>>,\n host: string,\n token?: string,\n extraPorts?: number[],\n): BootedSandbox {\n const ports: Record<number, string> = {}\n for (const port of extraPorts ?? []) {\n ports[port] = `https://${sandbox.getHost(port)}`\n }\n return {\n mcpUrl: `https://${host}/mcp`,\n sandboxId: sandbox.sandboxId,\n ...(token ? { token } : {}),\n ...(Object.keys(ports).length > 0 ? { ports } : {}),\n expose(port: number): Promise<{ url: string }> {\n return Promise.resolve({ url: `https://${sandbox.getHost(port)}` })\n },\n async stop(): Promise<void> {\n await sandbox.kill()\n },\n async pause(): Promise<void> {\n await sandbox.pause({ keepMemory: true })\n },\n }\n}\n\n/** `e2bSandboxProvider.boot`/`.connect` — see module docs. */\nexport const e2bSandboxProvider: SandboxProvider = {\n async boot(spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox> {\n const config = readE2bConfig(spec)\n const template = config.template ?? DEFAULT_TEMPLATE\n const port = config.port ?? DEFAULT_PORT\n const workspace = config.workspace ?? DEFAULT_WORKSPACE\n\n const sandbox = await Sandbox.create(template, {\n apiKey: process.env.E2B_API_KEY,\n envs: opts.env,\n timeoutMs: config.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS,\n })\n\n // Once the VM exists, EVERY failure after this point must reclaim it —\n // a slow/failed `npm i -g`, a throwing setup hook, or a daemon that never\n // becomes healthy. Otherwise the caller's request (which may have already\n // timed out and walked away) leaves a running, session-less box behind\n // (observed live 2026-09-05, six leaked boxes). This single catch is the\n // only owner of that cleanup — `ensureDaemonHealthy` never kills.\n try {\n const host = sandbox.getHost(port)\n await ensureDaemonHealthy(sandbox, host, port, workspace, config, opts.env, resolveUpdateCli(config, template))\n return toBootedSandbox(sandbox, host, undefined, spec.extraPorts)\n } catch (err) {\n await sandbox.kill().catch(() => undefined)\n throw err\n }\n },\n\n async connect(sandboxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox> {\n const config = readE2bConfig(spec)\n const port = config.port ?? DEFAULT_PORT\n const workspace = config.workspace ?? DEFAULT_WORKSPACE\n\n const sandbox = await Sandbox.connect(sandboxId, { apiKey: process.env.E2B_API_KEY })\n // A resumed box keeps its ORIGINAL lifetime deadline — re-arm it so the\n // reconnected session gets the same runway a fresh boot gets (same\n // 5-minute-default trap as `boot`, see DEFAULT_SANDBOX_TIMEOUT_MS).\n await sandbox.setTimeout(config.timeoutMs ?? DEFAULT_SANDBOX_TIMEOUT_MS)\n\n // Same never-leak contract as `boot`: a reconnect whose daemon won't come\n // back healthy must reclaim the box rather than leave it running orphaned.\n try {\n const host = sandbox.getHost(port)\n // A resumed box keeps its original template — resolve against the\n // configured template the same way `boot` does.\n await ensureDaemonHealthy(sandbox, host, port, workspace, config, opts.env, resolveUpdateCli(config, config.template ?? DEFAULT_TEMPLATE))\n return finishConnect(sandbox, host, opts, spec)\n } catch (err) {\n await sandbox.kill().catch(() => undefined)\n throw err\n }\n },\n}\n\n/** Tail of `connect` — surface e2b's private-traffic token (when asked) and\n * wrap the healthy handle. Split out so the `connect` body's try/catch stays\n * a thin never-leak guard around the boot work. */\nfunction finishConnect(\n sandbox: Sandbox,\n host: string,\n opts: SandboxBootOpts,\n spec: SandboxSpec,\n): BootedSandbox {\n // `opts.expose === \"private\"` (only ever set by `attachSandbox`, see\n // @agentproto/runtime) surfaces e2b's own \"restricted public traffic\"\n // token, when the sandbox has one — it gates access to EVERY exposed\n // port, this one included, at e2b's edge (distinct from `SandboxOpts.\n // secure`, which only covers envd's own control-plane traffic — see\n // module docs). Best-effort: e2b's SDK only exposes the token as a\n // readonly property with no documented guarantee it's populated on a\n // reconnect for a sandbox that wasn't created with traffic restriction\n // enabled. Unverified against a live sandbox.\n const token = opts.expose === \"private\" ? sandbox.trafficAccessToken : undefined\n return toBootedSandbox(sandbox, host, token, spec.extraPorts)\n}\n\n/** Poll `url` until it responds OK, or return false once `timeoutMs` elapses. */\nasync function probeHealth(url: string, timeoutMs: number, pollIntervalMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs\n for (;;) {\n try {\n const res = await fetch(url, { signal: AbortSignal.timeout(pollIntervalMs) })\n if (res.ok) return true\n } catch {\n // not up yet — fall through to the deadline check\n }\n if (Date.now() >= deadline) return false\n await new Promise(resolve => setTimeout(resolve, pollIntervalMs))\n }\n}\n\n/** A background `commands.run` handle, narrowed to just what boot-readiness\n * needs. Kept structural (not `import type { CommandHandle }`) so the health\n * race stays trivially fakeable in unit tests and tolerant of a provider/SDK\n * that returns a plainer object. */\ninterface BackgroundCommand {\n /** Resolves when the process finishes; REJECTS (`CommandExitError`) on a\n * non-zero exit. Either way the process is gone. Absent ⇒ we can't observe\n * an early exit and fall back to polling the deadline. */\n wait?: () => Promise<unknown>\n /** `undefined` while the process is still running. */\n exitCode?: number\n stdout?: string\n stderr?: string\n}\n\n/**\n * Poll `url` until the daemon is healthy, the deadline elapses, OR the daemon\n * process (`handle`) exits — whichever comes first. Returns `{ healthy,\n * exited }`. `exited: true` means the serve process died during boot, so the\n * caller can fail FAST (with the daemon's captured output) instead of waiting\n * out the full readiness window — the difference between a crashing image\n * surfacing its stderr in ~1s and hanging the HTTP/MCP call for 30s+.\n */\nasync function probeHealthOrExit(\n url: string,\n handle: BackgroundCommand,\n timeoutMs: number,\n pollIntervalMs: number,\n): Promise<{ healthy: boolean; exited: boolean }> {\n const deadline = Date.now() + timeoutMs\n let exited = false\n // `wait()` rejects with CommandExitError on a non-zero exit and resolves on\n // a zero exit — either way the daemon is no longer running, so treat both\n // identically. Only observe the exit when the handle actually supports it;\n // a plain `{}` (some tests/providers) must NOT be read as an instant exit.\n if (typeof handle.wait === \"function\") {\n void handle.wait().then(\n () => {\n exited = true\n },\n () => {\n exited = true\n },\n )\n }\n for (;;) {\n try {\n const res = await fetch(url, { signal: AbortSignal.timeout(pollIntervalMs) })\n if (res.ok) return { healthy: true, exited: false }\n } catch {\n // not up yet — fall through to the exit/deadline checks\n }\n if (exited || handle.exitCode !== undefined) return { healthy: false, exited: true }\n if (Date.now() >= deadline) return { healthy: false, exited: false }\n await new Promise(resolve => setTimeout(resolve, pollIntervalMs))\n }\n}\n\n/** Best-effort tail of a background daemon handle's captured output, for\n * embedding in a boot-failure error so a broken image is diagnosable from the\n * caller's error alone. Empty when nothing was captured — a still-running but\n * unhealthy daemon buffers its stdout until exit, so this is populated mainly\n * when the process actually died. Bounded so a chatty crash can't bloat the\n * error. */\nfunction boxDaemonOutput(handle: BackgroundCommand): string {\n const joined = [handle.stderr ?? \"\", handle.stdout ?? \"\"]\n .map(s => s.trim())\n .filter(Boolean)\n .join(\"\\n\")\n const MAX = 2_000\n return joined.length > MAX ? `…${joined.slice(-MAX)}` : joined\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentproto/sandbox-e2b",
3
- "version": "0.3.7",
4
- "description": "@agentproto/sandbox-e2b — e2b `SandboxProvider` for @agentproto/sandbox. Boots the `agentproto-workstation` e2b template, starts the agentproto daemon inside it, and exposes its MCP endpoint as a URL so any AgentSessionHost consumer (worktreeAgentWorkflow, worktree-agent CLI) can run a coding-agent step inside the sandbox.",
3
+ "version": "0.4.0",
4
+ "description": "@agentproto/sandbox-e2b — e2b `SandboxProvider` for @agentproto/sandbox. Boots the pinned `agentproto-workstation` e2b template (baked @agentproto/cli 0.17.0), starts the agentproto daemon inside it, and exposes its MCP endpoint as a URL so any AgentSessionHost consumer (worktreeAgentWorkflow, worktree-agent CLI) can run a coding-agent step inside the sandbox.",
5
5
  "keywords": [
6
6
  "agentproto",
7
7
  "aip-36",
@@ -42,14 +42,14 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "e2b": "^2.46.1",
45
- "@agentproto/sandbox": "0.2.6"
45
+ "@agentproto/sandbox": "0.3.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^25.6.2",
49
49
  "tsup": "^8.5.1",
50
50
  "typescript": "^5.9.3",
51
51
  "vitest": "^3.2.4",
52
- "@agentproto/tooling": "0.1.0-alpha.0"
52
+ "@agentproto/tooling": "0.1.0"
53
53
  },
54
54
  "scripts": {
55
55
  "dev": "tsup --watch",