@agentproto/sandbox 0.4.0 → 0.5.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/dist/index.d.ts +52 -1
- package/dist/index.mjs +36 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -26,6 +26,25 @@ declare function defineSandbox<TFactory = unknown, TCapabilities extends Record<
|
|
|
26
26
|
|
|
27
27
|
/** AIP-36 sandbox manifest handle — provider id, config, env passthrough, limits. */
|
|
28
28
|
type SandboxSpec = SandboxHandle;
|
|
29
|
+
/**
|
|
30
|
+
* Thrown when the box booted (or reconnected to) but the daemon MCP
|
|
31
|
+
* connection on top of it failed. The box itself already existed at that
|
|
32
|
+
* point, so `createSandboxAgentSessionHost` reaped it (paused when the
|
|
33
|
+
* provider supports it, killed otherwise) before throwing — `cleanedUp`
|
|
34
|
+
* records which. Callers use this to stamp the sandbox ledger instead of
|
|
35
|
+
* leaving a live box with no owner.
|
|
36
|
+
*/
|
|
37
|
+
declare class SandboxHostBootFailedError extends Error {
|
|
38
|
+
readonly sandboxId: string;
|
|
39
|
+
/** What the cleanup did to the box: "paused" or "stopped". Always set —
|
|
40
|
+
* the cleanup itself is best-effort, so a failed cleanup still records
|
|
41
|
+
* the attempt. */
|
|
42
|
+
readonly cleanedUp: "paused" | "stopped";
|
|
43
|
+
constructor(message: string, info: {
|
|
44
|
+
sandboxId: string;
|
|
45
|
+
cleanedUp: "paused" | "stopped";
|
|
46
|
+
});
|
|
47
|
+
}
|
|
29
48
|
/**
|
|
30
49
|
* Thrown when a caller requests port exposure on a `BootedSandbox` whose
|
|
31
50
|
* provider does not support it — i.e. the sandbox handle has no `expose()`
|
|
@@ -118,6 +137,27 @@ interface SandboxBootOpts {
|
|
|
118
137
|
*/
|
|
119
138
|
keepAlive?: boolean;
|
|
120
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Thrown by a provider's `connect()` when the provider answers that the
|
|
142
|
+
* sandbox id no longer exists (e2b's `"Sandbox Not Found"` 404) — the box
|
|
143
|
+
* died out from under the session while the local ledger still shows it
|
|
144
|
+
* paused/connected. Distinct from a generic connect failure so callers
|
|
145
|
+
* (session-spawn's reconnect path, the ledger) can mark the row GONE
|
|
146
|
+
* instead of leaving a phantom paused box on the books.
|
|
147
|
+
*/
|
|
148
|
+
declare class SandboxBoxGoneError extends Error {
|
|
149
|
+
constructor(sandboxId: string, cause?: unknown);
|
|
150
|
+
}
|
|
151
|
+
/** Structural check (name-based, so it works across package boundaries and
|
|
152
|
+
* with mocked errors) — true when the error means "the box is gone". */
|
|
153
|
+
declare function isSandboxBoxGoneError(err: unknown): boolean;
|
|
154
|
+
/** What a `SandboxProvider.probe` liveness check reports about a box. */
|
|
155
|
+
interface SandboxProbeResult {
|
|
156
|
+
/** False means the provider answers the id is gone (404 / not-found). */
|
|
157
|
+
alive: boolean;
|
|
158
|
+
/** Provider-reported box state (e.g. "running", "paused"), when available. */
|
|
159
|
+
state?: string;
|
|
160
|
+
}
|
|
121
161
|
/**
|
|
122
162
|
* Backend-agnostic sandbox lifecycle. Concrete implementations (e2b, modal,
|
|
123
163
|
* daytona, blaxel, …) live in their own packages so this one stays free of
|
|
@@ -131,6 +171,17 @@ interface SandboxProvider {
|
|
|
131
171
|
* which tears down its temp workspace on `stop()`) omit it; the runtime
|
|
132
172
|
* errors clearly when reuse is requested against such a provider. */
|
|
133
173
|
connect?(sandboxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>;
|
|
174
|
+
/**
|
|
175
|
+
* Liveness probe against the PROVIDER (not the box's daemon): answers
|
|
176
|
+
* whether the sandbox id still exists at all — the signal a box death
|
|
177
|
+
* needs, distinct from a session dying. Polling the provider's control
|
|
178
|
+
* plane (`GET /sandboxes/:id`, 404 ⇒ gone) is deliberately cheap.
|
|
179
|
+
* `{ alive: false }` means the provider answered the id is gone; a THROWN
|
|
180
|
+
* error means the check itself failed (callers treat a throw as
|
|
181
|
+
* "unknown", never as death). Optional: providers with no such API omit
|
|
182
|
+
* it; a caller reading `sandboxAlive` then reports "unknown".
|
|
183
|
+
*/
|
|
184
|
+
probe?(sandboxId: string): Promise<SandboxProbeResult>;
|
|
134
185
|
}
|
|
135
186
|
/** Which secrets to resolve into the sandbox's env, and how. */
|
|
136
187
|
interface SandboxSecretsConfig {
|
|
@@ -222,4 +273,4 @@ declare function resolveLifecyclePolicy(spec: SandboxHandle, reuse: boolean): Sa
|
|
|
222
273
|
declare const SPEC_NAME: "agentsandbox/v1";
|
|
223
274
|
declare const SPEC_VERSION: "1.0.0-alpha";
|
|
224
275
|
|
|
225
|
-
export { type BootedSandbox, type CreateSandboxAgentSessionHostOpts, SPEC_NAME, SPEC_VERSION, type SandboxAgentSessionHost, type SandboxBootOpts, SandboxHandle, type SandboxLifecyclePolicy, SandboxPortExposureUnsupportedError, type SandboxProvider, SandboxRuntimeHandle, SandboxRuntimeInput, type SandboxSecretsConfig, type SandboxSpec, createSandboxAgentSessionHost, defineSandbox, exposePort, resolveLifecyclePolicy };
|
|
276
|
+
export { type BootedSandbox, type CreateSandboxAgentSessionHostOpts, SPEC_NAME, SPEC_VERSION, type SandboxAgentSessionHost, type SandboxBootOpts, SandboxBoxGoneError, SandboxHandle, SandboxHostBootFailedError, type SandboxLifecyclePolicy, SandboxPortExposureUnsupportedError, type SandboxProbeResult, type SandboxProvider, SandboxRuntimeHandle, SandboxRuntimeInput, type SandboxSecretsConfig, type SandboxSpec, createSandboxAgentSessionHost, defineSandbox, exposePort, isSandboxBoxGoneError, resolveLifecyclePolicy };
|
package/dist/index.mjs
CHANGED
|
@@ -6,6 +6,19 @@ import { connectDaemonAgentSessionHost } from '@agentproto/worktree';
|
|
|
6
6
|
* @agentproto/sandbox v0.1.0-alpha
|
|
7
7
|
* AIP-36 SANDBOX.md `defineSandbox` reference implementation.
|
|
8
8
|
*/
|
|
9
|
+
var SandboxHostBootFailedError = class extends Error {
|
|
10
|
+
sandboxId;
|
|
11
|
+
/** What the cleanup did to the box: "paused" or "stopped". Always set —
|
|
12
|
+
* the cleanup itself is best-effort, so a failed cleanup still records
|
|
13
|
+
* the attempt. */
|
|
14
|
+
cleanedUp;
|
|
15
|
+
constructor(message, info) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "SandboxHostBootFailedError";
|
|
18
|
+
this.sandboxId = info.sandboxId;
|
|
19
|
+
this.cleanedUp = info.cleanedUp;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
9
22
|
var SandboxPortExposureUnsupportedError = class extends Error {
|
|
10
23
|
constructor(message) {
|
|
11
24
|
super(message ?? "This sandbox provider does not support port exposure.");
|
|
@@ -20,6 +33,18 @@ async function exposePort(booted, port) {
|
|
|
20
33
|
}
|
|
21
34
|
return booted.expose(port);
|
|
22
35
|
}
|
|
36
|
+
var SandboxBoxGoneError = class extends Error {
|
|
37
|
+
constructor(sandboxId, cause) {
|
|
38
|
+
super(
|
|
39
|
+
`sandbox "${sandboxId}" no longer exists on its provider \u2014 the box is gone (the session descriptor and ledger can outlive a provider-reaped box).`
|
|
40
|
+
);
|
|
41
|
+
this.name = "SandboxBoxGoneError";
|
|
42
|
+
this.cause = cause;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
function isSandboxBoxGoneError(err) {
|
|
46
|
+
return err instanceof Error && err.name === "SandboxBoxGoneError";
|
|
47
|
+
}
|
|
23
48
|
async function createSandboxAgentSessionHost(opts) {
|
|
24
49
|
const env = await resolveSandboxSecretsEnv(opts.secrets);
|
|
25
50
|
let booted;
|
|
@@ -37,8 +62,16 @@ async function createSandboxAgentSessionHost(opts) {
|
|
|
37
62
|
try {
|
|
38
63
|
host = await connectDaemonAgentSessionHost({ url: booted.mcpUrl });
|
|
39
64
|
} catch (err) {
|
|
40
|
-
|
|
41
|
-
|
|
65
|
+
const cleanedUp = booted.pause ? "paused" : "stopped";
|
|
66
|
+
if (booted.pause) {
|
|
67
|
+
await booted.pause().catch(() => void 0);
|
|
68
|
+
} else {
|
|
69
|
+
await booted.stop().catch(() => void 0);
|
|
70
|
+
}
|
|
71
|
+
throw new SandboxHostBootFailedError(
|
|
72
|
+
err instanceof Error ? err.message : String(err),
|
|
73
|
+
{ sandboxId: booted.sandboxId, cleanedUp }
|
|
74
|
+
);
|
|
42
75
|
}
|
|
43
76
|
return {
|
|
44
77
|
...host,
|
|
@@ -94,6 +127,6 @@ function parseIdleAfterMs(event) {
|
|
|
94
127
|
var SPEC_NAME = "agentsandbox/v1";
|
|
95
128
|
var SPEC_VERSION = "1.0.0-alpha";
|
|
96
129
|
|
|
97
|
-
export { SPEC_NAME, SPEC_VERSION, SandboxPortExposureUnsupportedError, createSandboxAgentSessionHost, exposePort, resolveLifecyclePolicy };
|
|
130
|
+
export { SPEC_NAME, SPEC_VERSION, SandboxBoxGoneError, SandboxHostBootFailedError, SandboxPortExposureUnsupportedError, createSandboxAgentSessionHost, exposePort, isSandboxBoxGoneError, resolveLifecyclePolicy };
|
|
98
131
|
//# sourceMappingURL=index.mjs.map
|
|
99
132
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/agent-session-host.ts","../src/lifecycle.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * AIP-36 sandbox-backed `AgentSessionHost`.\n *\n * The seam an `AgentStep` binds against (`AgentSessionHost`,\n * `@agentproto/workflow-runtime`) is already satisfiable by a *remote*\n * daemon via `connectDaemonAgentSessionHost` (`@agentproto/worktree`) —\n * it just needs a reachable MCP URL. So running a coding-agent step\n * inside a sandbox is: boot a provider-specific box that exposes an\n * agentproto daemon's MCP endpoint as a URL, then hand that URL to the\n * daemon host unchanged. No new session-host implementation, no\n * bespoke spawn/prompt plumbing — this module only wires secrets → env\n * → `provider.boot` → `connectDaemonAgentSessionHost`.\n */\n\nimport { assertSafeSecretValue, type SecretResolver } from \"@agentproto/secrets/exposure\"\nimport { connectDaemonAgentSessionHost, type DaemonAgentSessionHost } from \"@agentproto/worktree\"\nimport type { SandboxHandle } from \"./types.js\"\n\n/** AIP-36 sandbox manifest handle — provider id, config, env passthrough, limits. */\nexport type SandboxSpec = SandboxHandle\n\n/**\n * Thrown when a caller requests port exposure on a `BootedSandbox` whose\n * provider does not support it — i.e. the sandbox handle has no `expose()`\n * method. Callers should check for `expose` before calling it, or catch\n * this error and fall back gracefully.\n */\nexport class SandboxPortExposureUnsupportedError extends Error {\n constructor(message?: string) {\n super(message ?? \"This sandbox provider does not support port exposure.\")\n this.name = \"SandboxPortExposureUnsupportedError\"\n }\n}\n\n/** What a `SandboxProvider` hands back once the box is up and reachable. */\nexport interface BootedSandbox {\n /** The booted agentproto daemon's MCP endpoint, reachable from this process. */\n mcpUrl: string\n /** Provider-assigned sandbox id, for logging / lookup. */\n sandboxId: string\n /** Opaque secret gating `mcpUrl`, present when `opts.expose === \"private\"`\n * was honoured (see `SandboxBootOpts.expose`). Absent for the default\n * public-exposure path (boot-and-drive) and for providers/paths that\n * can't gate the port at all — a caller that needs a gated URL (e.g.\n * `attachSandbox`) MUST treat a missing token as \"not gated\", not as\n * \"no auth needed\". The token is the raw secret; how a client must\n * PRESENT it (bearer header, cookie, …) is provider-specific — see\n * `authHeaders`. */\n token?: string\n /** Exact HTTP header(s) a client must send to authenticate against the\n * gated `mcpUrl` — the provider's own answer to \"how do I present the\n * token\". Box, for instance, gates its private hostname with a\n * `Cookie: _port_auth=<token>` (verified live: bearer/query are ignored,\n * the port edge only honours the cookie), so it returns that here rather\n * than leaving the caller to guess a scheme. Present iff `token` is; a\n * token-only provider that omits this is treated by `buildMcpConfigSnippet`\n * as `Authorization: Bearer <token>`. */\n authHeaders?: Record<string, string>\n /**\n * Expose an app port on the sandbox and return its public URL. E2B returns\n * `https://<port>-<sandboxId>.e2b.app`. Loopback bind is enough inside the\n * VM — the provider's edge handles the forwarding.\n *\n * Optional: providers that cannot expose arbitrary ports omit this method.\n * Callers should check for presence before calling, or catch\n * `SandboxPortExposureUnsupportedError` when using `exposePort()`.\n */\n expose?(port: number): Promise<{ url: string }>\n /**\n * Ports resolved at boot time from `SandboxSpec.extraPorts` — a map of\n * port number to public URL. Only present when the spec declared\n * `extraPorts` AND the provider supports exposure. Callers that need a\n * port URL at runtime should use `expose()` directly when this map is\n * absent or doesn't include the target port.\n */\n ports?: Record<number, string>\n /** Tear down the sandbox. */\n stop(): Promise<void>\n /** Pause the sandbox instead of killing it — keeps it reconnectable via\n * `SandboxProvider.connect(sandboxId, ...)` later. Optional: providers\n * that can't pause (or don't support reconnect at all) omit it; callers\n * that want to pause fall back to `stop()` when it's absent. */\n pause?(): Promise<void>\n}\n\n/**\n * Expose a port on a booted sandbox. Throws `SandboxPortExposureUnsupportedError`\n * when the provider's sandbox handle has no `expose()` method.\n */\nexport async function exposePort(booted: BootedSandbox, port: number): Promise<{ url: string }> {\n if (!booted.expose) {\n throw new SandboxPortExposureUnsupportedError(\n `sandbox \"${booted.sandboxId}\" does not support port exposure — ` +\n \"the provider has no expose() implementation.\",\n )\n }\n return booted.expose(port)\n}\n\n/** Env resolved from secrets, handed to `provider.boot`. */\nexport interface SandboxBootOpts {\n env: Record<string, string>\n /**\n * How the provider should expose the daemon's port. `\"public\"` (the\n * default when omitted) is boot-and-drive's ephemeral, provider-owned,\n * ungated URL. `\"private\"` asks the provider for a PERSISTENT,\n * token-gated URL instead — set by `attachSandbox`, which produces a\n * durable connection descriptor and must never emit an ungated one.\n * Providers that don't support gating simply ignore this and omit\n * `BootedSandbox.token`; the caller is responsible for treating that as\n * a failure when it needed a gated URL.\n */\n expose?: \"public\" | \"private\"\n /**\n * Keep the sandbox awake indefinitely for the always-on rendezvous model\n * — set by `attachSandbox` when its own `keepAlive` opt is true. A\n * provider that supports an explicit no-auto-stop/no-expiry assertion\n * (e.g. Box's `ttlSeconds: null`) should (re-)apply it as part of\n * `connect()`, defensively, even if the sandbox already defaults to it.\n * Providers with no such concept simply ignore this.\n */\n keepAlive?: boolean\n}\n\n/**\n * Backend-agnostic sandbox lifecycle. Concrete implementations (e2b, modal,\n * daytona, blaxel, …) live in their own packages so this one stays free of\n * vendor SDK dependencies — see `@agentproto/sandbox-e2b`.\n */\nexport interface SandboxProvider {\n boot(spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>\n /** Reconnect to an already-booted (possibly paused) sandbox instead of\n * booting a fresh one — the reuse path (`agent_start.sandbox.reuse`).\n * Optional: providers that can't reconnect (e.g. the `local` passthrough,\n * which tears down its temp workspace on `stop()`) omit it; the runtime\n * errors clearly when reuse is requested against such a provider. */\n connect?(sandboxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>\n}\n\n/** Which secrets to resolve into the sandbox's env, and how. */\nexport interface SandboxSecretsConfig {\n /** Secret slugs to resolve (e.g. `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`). */\n slugs: readonly string[]\n /** Resolves a slug to its value. Defaults to reading `process.env[slug]`. */\n resolver?: SecretResolver\n}\n\nexport interface CreateSandboxAgentSessionHostOpts {\n provider: SandboxProvider\n spec: SandboxSpec\n secrets: SandboxSecretsConfig\n /** Reconnect to this existing sandbox id instead of booting a fresh box —\n * requires `provider.connect`; throws a clear error otherwise. */\n sandboxId?: string\n}\n\nexport type SandboxAgentSessionHost = DaemonAgentSessionHost & {\n /** The booted sandbox daemon's MCP endpoint (`BootedSandbox.mcpUrl`) —\n * surfaced so a caller can drive the box's OTHER daemon tools (app_install,\n * command_execute, …) the same way the session host drives agent_start. */\n mcpUrl: string\n /** Provider-assigned sandbox id (`BootedSandbox.sandboxId`) — surfaced so a\n * caller can record it (there's no local PID for a sandboxed session). */\n sandboxId: string\n /** Ports resolved at boot from `SandboxSpec.extraPorts` — forwarded from\n * `BootedSandbox.ports` so the runtime can record them on the session\n * descriptor without reaching into the booted handle after the fact. */\n ports?: Record<number, string>\n /** Expose an app port and return its public URL — forwarded from\n * `BootedSandbox.expose`. Absent when the provider doesn't support it. */\n expose?: BootedSandbox[\"expose\"]\n /** Close the daemon connection AND tear down the sandbox. */\n stop(): Promise<void>\n /** Close the daemon connection and PAUSE the sandbox instead of killing\n * it — only present when the booted sandbox supports `pause()`. */\n pause?(): Promise<void>\n}\n\n/**\n * Resolve `secrets` into an env map, boot (or, when `opts.sandboxId` is set,\n * reconnect to) the sandbox with it, then connect the #202 daemon host to\n * the sandbox's exposed MCP URL. `stop()` closes the daemon connection\n * before tearing down the sandbox (never leaks the box on a client-side\n * error); `pause()` does the same but pauses rather than kills.\n */\nexport async function createSandboxAgentSessionHost(\n opts: CreateSandboxAgentSessionHostOpts,\n): Promise<SandboxAgentSessionHost> {\n const env = await resolveSandboxSecretsEnv(opts.secrets)\n let booted: BootedSandbox\n if (opts.sandboxId !== undefined) {\n if (!opts.provider.connect) {\n throw new Error(\n `createSandboxAgentSessionHost: reuse requested for sandbox \"${opts.sandboxId}\", ` +\n \"but this provider has no connect() — it can only boot fresh sandboxes.\",\n )\n }\n booted = await opts.provider.connect(opts.sandboxId, opts.spec, { env })\n } else {\n booted = await opts.provider.boot(opts.spec, { env })\n }\n let host: DaemonAgentSessionHost\n try {\n host = await connectDaemonAgentSessionHost({ url: booted.mcpUrl })\n } catch (err) {\n await booted.stop()\n throw err\n }\n return {\n ...host,\n mcpUrl: booted.mcpUrl,\n sandboxId: booted.sandboxId,\n ...(booted.ports ? { ports: booted.ports } : {}),\n ...(booted.expose ? { expose: booted.expose.bind(booted) } : {}),\n async stop(): Promise<void> {\n await host.close()\n await booted.stop()\n },\n ...(booted.pause\n ? {\n async pause(): Promise<void> {\n await host.close()\n await booted.pause!()\n },\n }\n : {}),\n }\n}\n\nconst defaultProcessEnvResolver: SecretResolver = name => process.env[name] ?? null\n\n/** Resolve every configured slug, failing loudly (no silent gaps in the sandbox env). */\nasync function resolveSandboxSecretsEnv(\n config: SandboxSecretsConfig,\n): Promise<Record<string, string>> {\n const resolver = config.resolver ?? defaultProcessEnvResolver\n const env: Record<string, string> = {}\n for (const slug of config.slugs) {\n const value = await resolver(slug)\n if (value === null || value === undefined) {\n throw new Error(\n `createSandboxAgentSessionHost: missing secret \"${slug}\" — set it in the ` +\n \"host process's environment, or pass a resolver that can supply it.\",\n )\n }\n assertSafeSecretValue(slug, value)\n env[slug] = value\n }\n return env\n}\n","/**\n * AIP-36 `lifecycle` policy resolution — maps a `SandboxHandle`'s\n * `lifecycle.pause_after_idle` / `lifecycle.destroy_on` (plus whether this\n * boot is a request to reconnect to an existing box) to a concrete\n * teardown decision. Pure and host-agnostic: the actual pause-vs-kill call\n * happens in `@agentproto/runtime`'s sandbox proxy, which just reads this\n * policy back off.\n */\n\nimport type { SandboxHandle } from \"./types.js\"\n\nexport interface SandboxLifecyclePolicy {\n /** What session close should do to the box: pause it (keeps it\n * reconnectable via `SandboxProvider.connect`) or kill it (ephemeral).\n * Pause is the default: absent any explicit lifecycle declaration the\n * box is paused on close, and dies at its own `timeoutMs` anyway. */\n teardown: \"kill\" | \"pause\"\n /** Idle window in milliseconds, parsed from the AIP-37 `idle-<seconds>`\n * event name. Undefined when the spec doesn't declare\n * `lifecycle.pause_after_idle`. */\n pauseAfterIdleMs?: number\n}\n\nconst IDLE_EVENT_PATTERN = /^idle-(\\d+)$/\n\n/**\n * Pause is the default teardown: absent `destroy_on`, `pause_after_idle`\n * AND `reuse`, a closed box is paused (`SandboxProvider.connect`-able)\n * rather than killed — it still dies at its own `timeoutMs`, so pausing\n * never accumulates boxes indefinitely. The explicit declarations stay\n * authoritative: an `destroy_on` always kills (the spec states outright\n * the box must not survive session close), and `pause_after_idle` /\n * `reuse` pause (which the default now agrees with).\n */\nexport function resolveLifecyclePolicy(spec: SandboxHandle, reuse: boolean): SandboxLifecyclePolicy {\n if (spec.lifecycle?.destroy_on) return { teardown: \"kill\" }\n\n const pauseAfterIdleMs = parseIdleAfterMs(spec.lifecycle?.pause_after_idle)\n const teardown: \"kill\" | \"pause\" = \"pause\"\n return { teardown, ...(pauseAfterIdleMs !== undefined ? { pauseAfterIdleMs } : {}) }\n}\n\nfunction parseIdleAfterMs(event: string | undefined): number | undefined {\n if (!event) return undefined\n const match = IDLE_EVENT_PATTERN.exec(event)\n if (!match) return undefined\n return Number(match[1]) * 1000\n}\n","/**\n * @agentproto/sandbox — AIP-36 SANDBOX.md `defineSandbox` reference impl.\n *\n * A composable schema block defining the `sandbox` field — provider, config, command env, network egress, resource limits — for any manifest that names a compute environment for agent-issued shell commands. Sibling primitive to STORAGE.md (AIP-35); inline or ref, mirroring AIP-17 RUNNER and AIP-19 SECRETS.\n *\n * Spec: https://agentproto.sh/docs/aip-36\n *\n * Authoring paths:\n * - TS: `defineSandbox({...})` → `SandboxHandle`\n * - MD: `parseSandboxManifest(src) → sandboxFromManifest({...})` → `SandboxHandle`\n */\n\nexport const SPEC_NAME = \"agentsandbox/v1\" as const\nexport const SPEC_VERSION = \"1.0.0-alpha\" as const\n\nexport { defineSandbox } from \"./define-sandbox.js\"\nexport type {\n SandboxDefinition,\n SandboxHandle,\n SandboxRuntimeInput,\n SandboxRuntimeHandle,\n} from \"./types.js\"\n\n/** The AIP-36 frontmatter zod schema, under the name consumers that accept\n * an inline `SandboxSpec` (e.g. `@agentproto/runtime`'s `agent_start.sandbox`)\n * validate against. Same schema `define-sandbox.ts`/`manifest/index.ts` use. */\nexport { sandboxFrontmatterSchema as SandboxSpecSchema } from \"./schema.js\"\n\nexport {\n createSandboxAgentSessionHost,\n exposePort,\n SandboxPortExposureUnsupportedError,\n type SandboxSpec,\n type BootedSandbox,\n type SandboxBootOpts,\n type SandboxProvider,\n type SandboxSecretsConfig,\n type CreateSandboxAgentSessionHostOpts,\n type SandboxAgentSessionHost,\n} from \"./agent-session-host.js\"\n\nexport { resolveLifecyclePolicy, type SandboxLifecyclePolicy } from \"./lifecycle.js\"\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/agent-session-host.ts","../src/lifecycle.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * AIP-36 sandbox-backed `AgentSessionHost`.\n *\n * The seam an `AgentStep` binds against (`AgentSessionHost`,\n * `@agentproto/workflow-runtime`) is already satisfiable by a *remote*\n * daemon via `connectDaemonAgentSessionHost` (`@agentproto/worktree`) —\n * it just needs a reachable MCP URL. So running a coding-agent step\n * inside a sandbox is: boot a provider-specific box that exposes an\n * agentproto daemon's MCP endpoint as a URL, then hand that URL to the\n * daemon host unchanged. No new session-host implementation, no\n * bespoke spawn/prompt plumbing — this module only wires secrets → env\n * → `provider.boot` → `connectDaemonAgentSessionHost`.\n */\n\nimport { assertSafeSecretValue, type SecretResolver } from \"@agentproto/secrets/exposure\"\nimport { connectDaemonAgentSessionHost, type DaemonAgentSessionHost } from \"@agentproto/worktree\"\nimport type { SandboxHandle } from \"./types.js\"\n\n/** AIP-36 sandbox manifest handle — provider id, config, env passthrough, limits. */\nexport type SandboxSpec = SandboxHandle\n\n/**\n * Thrown when the box booted (or reconnected to) but the daemon MCP\n * connection on top of it failed. The box itself already existed at that\n * point, so `createSandboxAgentSessionHost` reaped it (paused when the\n * provider supports it, killed otherwise) before throwing — `cleanedUp`\n * records which. Callers use this to stamp the sandbox ledger instead of\n * leaving a live box with no owner.\n */\nexport class SandboxHostBootFailedError extends Error {\n readonly sandboxId: string\n /** What the cleanup did to the box: \"paused\" or \"stopped\". Always set —\n * the cleanup itself is best-effort, so a failed cleanup still records\n * the attempt. */\n readonly cleanedUp: \"paused\" | \"stopped\"\n\n constructor(message: string, info: { sandboxId: string; cleanedUp: \"paused\" | \"stopped\" }) {\n super(message)\n this.name = \"SandboxHostBootFailedError\"\n this.sandboxId = info.sandboxId\n this.cleanedUp = info.cleanedUp\n }\n}\n\n/**\n * Thrown when a caller requests port exposure on a `BootedSandbox` whose\n * provider does not support it — i.e. the sandbox handle has no `expose()`\n * method. Callers should check for `expose` before calling it, or catch\n * this error and fall back gracefully.\n */\nexport class SandboxPortExposureUnsupportedError extends Error {\n constructor(message?: string) {\n super(message ?? \"This sandbox provider does not support port exposure.\")\n this.name = \"SandboxPortExposureUnsupportedError\"\n }\n}\n\n/** What a `SandboxProvider` hands back once the box is up and reachable. */\nexport interface BootedSandbox {\n /** The booted agentproto daemon's MCP endpoint, reachable from this process. */\n mcpUrl: string\n /** Provider-assigned sandbox id, for logging / lookup. */\n sandboxId: string\n /** Opaque secret gating `mcpUrl`, present when `opts.expose === \"private\"`\n * was honoured (see `SandboxBootOpts.expose`). Absent for the default\n * public-exposure path (boot-and-drive) and for providers/paths that\n * can't gate the port at all — a caller that needs a gated URL (e.g.\n * `attachSandbox`) MUST treat a missing token as \"not gated\", not as\n * \"no auth needed\". The token is the raw secret; how a client must\n * PRESENT it (bearer header, cookie, …) is provider-specific — see\n * `authHeaders`. */\n token?: string\n /** Exact HTTP header(s) a client must send to authenticate against the\n * gated `mcpUrl` — the provider's own answer to \"how do I present the\n * token\". Box, for instance, gates its private hostname with a\n * `Cookie: _port_auth=<token>` (verified live: bearer/query are ignored,\n * the port edge only honours the cookie), so it returns that here rather\n * than leaving the caller to guess a scheme. Present iff `token` is; a\n * token-only provider that omits this is treated by `buildMcpConfigSnippet`\n * as `Authorization: Bearer <token>`. */\n authHeaders?: Record<string, string>\n /**\n * Expose an app port on the sandbox and return its public URL. E2B returns\n * `https://<port>-<sandboxId>.e2b.app`. Loopback bind is enough inside the\n * VM — the provider's edge handles the forwarding.\n *\n * Optional: providers that cannot expose arbitrary ports omit this method.\n * Callers should check for presence before calling, or catch\n * `SandboxPortExposureUnsupportedError` when using `exposePort()`.\n */\n expose?(port: number): Promise<{ url: string }>\n /**\n * Ports resolved at boot time from `SandboxSpec.extraPorts` — a map of\n * port number to public URL. Only present when the spec declared\n * `extraPorts` AND the provider supports exposure. Callers that need a\n * port URL at runtime should use `expose()` directly when this map is\n * absent or doesn't include the target port.\n */\n ports?: Record<number, string>\n /** Tear down the sandbox. */\n stop(): Promise<void>\n /** Pause the sandbox instead of killing it — keeps it reconnectable via\n * `SandboxProvider.connect(sandboxId, ...)` later. Optional: providers\n * that can't pause (or don't support reconnect at all) omit it; callers\n * that want to pause fall back to `stop()` when it's absent. */\n pause?(): Promise<void>\n}\n\n/**\n * Expose a port on a booted sandbox. Throws `SandboxPortExposureUnsupportedError`\n * when the provider's sandbox handle has no `expose()` method.\n */\nexport async function exposePort(booted: BootedSandbox, port: number): Promise<{ url: string }> {\n if (!booted.expose) {\n throw new SandboxPortExposureUnsupportedError(\n `sandbox \"${booted.sandboxId}\" does not support port exposure — ` +\n \"the provider has no expose() implementation.\",\n )\n }\n return booted.expose(port)\n}\n\n/** Env resolved from secrets, handed to `provider.boot`. */\nexport interface SandboxBootOpts {\n env: Record<string, string>\n /**\n * How the provider should expose the daemon's port. `\"public\"` (the\n * default when omitted) is boot-and-drive's ephemeral, provider-owned,\n * ungated URL. `\"private\"` asks the provider for a PERSISTENT,\n * token-gated URL instead — set by `attachSandbox`, which produces a\n * durable connection descriptor and must never emit an ungated one.\n * Providers that don't support gating simply ignore this and omit\n * `BootedSandbox.token`; the caller is responsible for treating that as\n * a failure when it needed a gated URL.\n */\n expose?: \"public\" | \"private\"\n /**\n * Keep the sandbox awake indefinitely for the always-on rendezvous model\n * — set by `attachSandbox` when its own `keepAlive` opt is true. A\n * provider that supports an explicit no-auto-stop/no-expiry assertion\n * (e.g. Box's `ttlSeconds: null`) should (re-)apply it as part of\n * `connect()`, defensively, even if the sandbox already defaults to it.\n * Providers with no such concept simply ignore this.\n */\n keepAlive?: boolean\n}\n\n/**\n * Thrown by a provider's `connect()` when the provider answers that the\n * sandbox id no longer exists (e2b's `\"Sandbox Not Found\"` 404) — the box\n * died out from under the session while the local ledger still shows it\n * paused/connected. Distinct from a generic connect failure so callers\n * (session-spawn's reconnect path, the ledger) can mark the row GONE\n * instead of leaving a phantom paused box on the books.\n */\nexport class SandboxBoxGoneError extends Error {\n constructor(sandboxId: string, cause?: unknown) {\n super(\n `sandbox \"${sandboxId}\" no longer exists on its provider — the box is gone ` +\n \"(the session descriptor and ledger can outlive a provider-reaped box).\",\n )\n this.name = \"SandboxBoxGoneError\"\n this.cause = cause\n }\n}\n\n/** Structural check (name-based, so it works across package boundaries and\n * with mocked errors) — true when the error means \"the box is gone\". */\nexport function isSandboxBoxGoneError(err: unknown): boolean {\n return err instanceof Error && err.name === \"SandboxBoxGoneError\"\n}\n\n/** What a `SandboxProvider.probe` liveness check reports about a box. */\nexport interface SandboxProbeResult {\n /** False means the provider answers the id is gone (404 / not-found). */\n alive: boolean\n /** Provider-reported box state (e.g. \"running\", \"paused\"), when available. */\n state?: string\n}\n\n/**\n * Backend-agnostic sandbox lifecycle. Concrete implementations (e2b, modal,\n * daytona, blaxel, …) live in their own packages so this one stays free of\n * vendor SDK dependencies — see `@agentproto/sandbox-e2b`.\n */\nexport interface SandboxProvider {\n boot(spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>\n /** Reconnect to an already-booted (possibly paused) sandbox instead of\n * booting a fresh one — the reuse path (`agent_start.sandbox.reuse`).\n * Optional: providers that can't reconnect (e.g. the `local` passthrough,\n * which tears down its temp workspace on `stop()`) omit it; the runtime\n * errors clearly when reuse is requested against such a provider. */\n connect?(sandboxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>\n /**\n * Liveness probe against the PROVIDER (not the box's daemon): answers\n * whether the sandbox id still exists at all — the signal a box death\n * needs, distinct from a session dying. Polling the provider's control\n * plane (`GET /sandboxes/:id`, 404 ⇒ gone) is deliberately cheap.\n * `{ alive: false }` means the provider answered the id is gone; a THROWN\n * error means the check itself failed (callers treat a throw as\n * \"unknown\", never as death). Optional: providers with no such API omit\n * it; a caller reading `sandboxAlive` then reports \"unknown\".\n */\n probe?(sandboxId: string): Promise<SandboxProbeResult>\n}\n\n/** Which secrets to resolve into the sandbox's env, and how. */\nexport interface SandboxSecretsConfig {\n /** Secret slugs to resolve (e.g. `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`). */\n slugs: readonly string[]\n /** Resolves a slug to its value. Defaults to reading `process.env[slug]`. */\n resolver?: SecretResolver\n}\n\nexport interface CreateSandboxAgentSessionHostOpts {\n provider: SandboxProvider\n spec: SandboxSpec\n secrets: SandboxSecretsConfig\n /** Reconnect to this existing sandbox id instead of booting a fresh box —\n * requires `provider.connect`; throws a clear error otherwise. */\n sandboxId?: string\n}\n\nexport type SandboxAgentSessionHost = DaemonAgentSessionHost & {\n /** The booted sandbox daemon's MCP endpoint (`BootedSandbox.mcpUrl`) —\n * surfaced so a caller can drive the box's OTHER daemon tools (app_install,\n * command_execute, …) the same way the session host drives agent_start. */\n mcpUrl: string\n /** Provider-assigned sandbox id (`BootedSandbox.sandboxId`) — surfaced so a\n * caller can record it (there's no local PID for a sandboxed session). */\n sandboxId: string\n /** Ports resolved at boot from `SandboxSpec.extraPorts` — forwarded from\n * `BootedSandbox.ports` so the runtime can record them on the session\n * descriptor without reaching into the booted handle after the fact. */\n ports?: Record<number, string>\n /** Expose an app port and return its public URL — forwarded from\n * `BootedSandbox.expose`. Absent when the provider doesn't support it. */\n expose?: BootedSandbox[\"expose\"]\n /** Close the daemon connection AND tear down the sandbox. */\n stop(): Promise<void>\n /** Close the daemon connection and PAUSE the sandbox instead of killing\n * it — only present when the booted sandbox supports `pause()`. */\n pause?(): Promise<void>\n}\n\n/**\n * Resolve `secrets` into an env map, boot (or, when `opts.sandboxId` is set,\n * reconnect to) the sandbox with it, then connect the #202 daemon host to\n * the sandbox's exposed MCP URL. `stop()` closes the daemon connection\n * before tearing down the sandbox (never leaks the box on a client-side\n * error); `pause()` does the same but pauses rather than kills.\n */\nexport async function createSandboxAgentSessionHost(\n opts: CreateSandboxAgentSessionHostOpts,\n): Promise<SandboxAgentSessionHost> {\n const env = await resolveSandboxSecretsEnv(opts.secrets)\n let booted: BootedSandbox\n if (opts.sandboxId !== undefined) {\n if (!opts.provider.connect) {\n throw new Error(\n `createSandboxAgentSessionHost: reuse requested for sandbox \"${opts.sandboxId}\", ` +\n \"but this provider has no connect() — it can only boot fresh sandboxes.\",\n )\n }\n booted = await opts.provider.connect(opts.sandboxId, opts.spec, { env })\n } else {\n booted = await opts.provider.boot(opts.spec, { env })\n }\n let host: DaemonAgentSessionHost\n try {\n host = await connectDaemonAgentSessionHost({ url: booted.mcpUrl })\n } catch (err) {\n // The box EXISTS at this point — the daemon connection on top of it\n // failed. Reap it so a failed boot never leaves a live box behind:\n // pause when the provider supports it (keeps a reconnected box\n // reconnectable — it was already there before this spawn), kill\n // otherwise. Then rethrow annotated so the caller can stamp the\n // ledger with the actual outcome.\n const cleanedUp = booted.pause ? \"paused\" : \"stopped\"\n if (booted.pause) {\n await booted.pause().catch(() => undefined)\n } else {\n await booted.stop().catch(() => undefined)\n }\n throw new SandboxHostBootFailedError(\n err instanceof Error ? err.message : String(err),\n { sandboxId: booted.sandboxId, cleanedUp },\n )\n }\n return {\n ...host,\n mcpUrl: booted.mcpUrl,\n sandboxId: booted.sandboxId,\n ...(booted.ports ? { ports: booted.ports } : {}),\n ...(booted.expose ? { expose: booted.expose.bind(booted) } : {}),\n async stop(): Promise<void> {\n await host.close()\n await booted.stop()\n },\n ...(booted.pause\n ? {\n async pause(): Promise<void> {\n await host.close()\n await booted.pause!()\n },\n }\n : {}),\n }\n}\n\nconst defaultProcessEnvResolver: SecretResolver = name => process.env[name] ?? null\n\n/** Resolve every configured slug, failing loudly (no silent gaps in the sandbox env). */\nasync function resolveSandboxSecretsEnv(\n config: SandboxSecretsConfig,\n): Promise<Record<string, string>> {\n const resolver = config.resolver ?? defaultProcessEnvResolver\n const env: Record<string, string> = {}\n for (const slug of config.slugs) {\n const value = await resolver(slug)\n if (value === null || value === undefined) {\n throw new Error(\n `createSandboxAgentSessionHost: missing secret \"${slug}\" — set it in the ` +\n \"host process's environment, or pass a resolver that can supply it.\",\n )\n }\n assertSafeSecretValue(slug, value)\n env[slug] = value\n }\n return env\n}\n","/**\n * AIP-36 `lifecycle` policy resolution — maps a `SandboxHandle`'s\n * `lifecycle.pause_after_idle` / `lifecycle.destroy_on` (plus whether this\n * boot is a request to reconnect to an existing box) to a concrete\n * teardown decision. Pure and host-agnostic: the actual pause-vs-kill call\n * happens in `@agentproto/runtime`'s sandbox proxy, which just reads this\n * policy back off.\n */\n\nimport type { SandboxHandle } from \"./types.js\"\n\nexport interface SandboxLifecyclePolicy {\n /** What session close should do to the box: pause it (keeps it\n * reconnectable via `SandboxProvider.connect`) or kill it (ephemeral).\n * Pause is the default: absent any explicit lifecycle declaration the\n * box is paused on close, and dies at its own `timeoutMs` anyway. */\n teardown: \"kill\" | \"pause\"\n /** Idle window in milliseconds, parsed from the AIP-37 `idle-<seconds>`\n * event name. Undefined when the spec doesn't declare\n * `lifecycle.pause_after_idle`. */\n pauseAfterIdleMs?: number\n}\n\nconst IDLE_EVENT_PATTERN = /^idle-(\\d+)$/\n\n/**\n * Pause is the default teardown: absent `destroy_on`, `pause_after_idle`\n * AND `reuse`, a closed box is paused (`SandboxProvider.connect`-able)\n * rather than killed — it still dies at its own `timeoutMs`, so pausing\n * never accumulates boxes indefinitely. The explicit declarations stay\n * authoritative: an `destroy_on` always kills (the spec states outright\n * the box must not survive session close), and `pause_after_idle` /\n * `reuse` pause (which the default now agrees with).\n */\nexport function resolveLifecyclePolicy(spec: SandboxHandle, reuse: boolean): SandboxLifecyclePolicy {\n if (spec.lifecycle?.destroy_on) return { teardown: \"kill\" }\n\n const pauseAfterIdleMs = parseIdleAfterMs(spec.lifecycle?.pause_after_idle)\n const teardown: \"kill\" | \"pause\" = \"pause\"\n return { teardown, ...(pauseAfterIdleMs !== undefined ? { pauseAfterIdleMs } : {}) }\n}\n\nfunction parseIdleAfterMs(event: string | undefined): number | undefined {\n if (!event) return undefined\n const match = IDLE_EVENT_PATTERN.exec(event)\n if (!match) return undefined\n return Number(match[1]) * 1000\n}\n","/**\n * @agentproto/sandbox — AIP-36 SANDBOX.md `defineSandbox` reference impl.\n *\n * A composable schema block defining the `sandbox` field — provider, config, command env, network egress, resource limits — for any manifest that names a compute environment for agent-issued shell commands. Sibling primitive to STORAGE.md (AIP-35); inline or ref, mirroring AIP-17 RUNNER and AIP-19 SECRETS.\n *\n * Spec: https://agentproto.sh/docs/aip-36\n *\n * Authoring paths:\n * - TS: `defineSandbox({...})` → `SandboxHandle`\n * - MD: `parseSandboxManifest(src) → sandboxFromManifest({...})` → `SandboxHandle`\n */\n\nexport const SPEC_NAME = \"agentsandbox/v1\" as const\nexport const SPEC_VERSION = \"1.0.0-alpha\" as const\n\nexport { defineSandbox } from \"./define-sandbox.js\"\nexport type {\n SandboxDefinition,\n SandboxHandle,\n SandboxRuntimeInput,\n SandboxRuntimeHandle,\n} from \"./types.js\"\n\n/** The AIP-36 frontmatter zod schema, under the name consumers that accept\n * an inline `SandboxSpec` (e.g. `@agentproto/runtime`'s `agent_start.sandbox`)\n * validate against. Same schema `define-sandbox.ts`/`manifest/index.ts` use. */\nexport { sandboxFrontmatterSchema as SandboxSpecSchema } from \"./schema.js\"\n\nexport {\n createSandboxAgentSessionHost,\n exposePort,\n SandboxPortExposureUnsupportedError,\n SandboxBoxGoneError,\n isSandboxBoxGoneError,\n SandboxHostBootFailedError,\n type SandboxSpec,\n type BootedSandbox,\n type SandboxBootOpts,\n type SandboxProvider,\n type SandboxProbeResult,\n type SandboxSecretsConfig,\n type CreateSandboxAgentSessionHostOpts,\n type SandboxAgentSessionHost,\n} from \"./agent-session-host.js\"\n\nexport { resolveLifecyclePolicy, type SandboxLifecyclePolicy } from \"./lifecycle.js\"\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentproto/sandbox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "@agentproto/sandbox — AIP-36 SANDBOX.md reference implementation. A composable schema block defining the `sandbox` field — provider, config, command env, network egress, resource limits — for any manifest that names a compute environment for agent-issued shell commands. Sibling primitive to STORAGE.md (AIP-35); inline or ref, mirroring AIP-17 RUNNER and AIP-19 SECRETS. Also ships createSandboxAgentSessionHost — the provider-agnostic seam that runs an AgentStep's coding-agent turn inside a booted sandbox by pointing the existing daemon-backed AgentSessionHost at its MCP URL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentproto",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"gray-matter": "^4.0.3",
|
|
50
50
|
"zod": "^4.5.4",
|
|
51
|
-
"@agentproto/secrets": "0.2.5",
|
|
52
|
-
"@agentproto/workflow-runtime": "0.11.0",
|
|
53
51
|
"@agentproto/define-doctype": "0.1.2",
|
|
52
|
+
"@agentproto/workflow-runtime": "0.11.0",
|
|
53
|
+
"@agentproto/secrets": "0.2.5",
|
|
54
54
|
"@agentproto/worktree": "0.6.2"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|