@agentproto/sandbox 0.14.0 → 0.15.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 +8 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -341,6 +341,9 @@ interface SandboxProviderBox {
|
|
|
341
341
|
id: string;
|
|
342
342
|
/** Provider-reported state ("running", "stopped", …), when available. */
|
|
343
343
|
state?: string;
|
|
344
|
+
/** The agentproto session that booted the box, when the provider recorded
|
|
345
|
+
* it (e2b: the `owner` metadata stamped at boot). */
|
|
346
|
+
owner?: string;
|
|
344
347
|
/** Session slots the provider still counts for this box. */
|
|
345
348
|
sessions?: number;
|
|
346
349
|
/** Memory the box holds, in MiB - when the provider reports it. */
|
|
@@ -369,6 +372,11 @@ interface SandboxBox {
|
|
|
369
372
|
tenantId?: string;
|
|
370
373
|
/** End user inside the tenant, when the provider reports it. */
|
|
371
374
|
endUser?: string;
|
|
375
|
+
/** The agentproto session that booted the box, when the provider recorded
|
|
376
|
+
* it (e2b: the `owner` metadata stamped at boot). */
|
|
377
|
+
owner?: string;
|
|
378
|
+
/** That session's label, when the provider recorded it. */
|
|
379
|
+
label?: string;
|
|
372
380
|
/** Provider host identifier (the computer controller's `hostId`), when
|
|
373
381
|
* reported. */
|
|
374
382
|
host?: string;
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/agent-session-host.ts","../src/exec-errors.ts","../src/quota-error.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 it was a\n * reconnect and the 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 /** Concrete provider that owned the box, when the boot came through a\n * composite provider (`auto`). Absent for a single-provider boot, where\n * the caller's requested slug already IS the provider. */\n readonly provider?: string\n\n constructor(\n message: string,\n info: { sandboxId: string; cleanedUp: \"paused\" | \"stopped\"; provider?: string },\n ) {\n super(message)\n this.name = \"SandboxHostBootFailedError\"\n this.sandboxId = info.sandboxId\n this.cleanedUp = info.cleanedUp\n if (info.provider !== undefined) this.provider = info.provider\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/**\n * How a composite provider (e.g. `auto`) chose the concrete backend for one\n * boot. `requested` is the slug the caller asked for, `provider` the one that\n * actually produced the box; `from` names the provider a fallback moved away\n * from (when there was one), and `reason` is a short, secret-free explanation.\n */\nexport interface SandboxProviderChoice {\n /** Slug the caller requested (e.g. `\"auto\"`). */\n requested: string\n /** Concrete provider that actually produced the box. */\n provider: string\n /** Provider a fallback moved away from, when the choice was not the first\n * candidate (e.g. `\"computer\"` on an overflow to e2b). */\n from?: string\n /** Short, secret-free explanation for the choice. */\n reason?: string\n /** ISO instant the choice was made. */\n at: string\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 /** Concrete provider that produced this box, for a composite provider\n * (`auto`). Absent for a single-provider boot, where the caller's own\n * requested slug already IS the provider. */\n provider?: string\n /** How a composite provider chose this backend. Absent for a\n * single-provider boot. */\n choice?: SandboxProviderChoice\n /** The egress address the provider brokered for this box, when it supports\n * selecting one (AIP-36 `network.egressIp` / provider config `egressIp`).\n * `null` means the controller explicitly reports no dedicated address\n * (for example `\"shared\"`); absent means the provider/controller has no\n * egress-IP concept. Optional so existing providers are unaffected. */\n egressIp?: { id: string; publicIp: string } | null\n /** Opaque secret gating `mcpUrl`. Present either when `opts.expose ===\n * \"private\"` was honoured (see `SandboxBootOpts.expose`), OR when the\n * provider's own daemon is gated unconditionally regardless of `expose`\n * (e.g. `computer`, whose in-box daemon always requires its own\n * per-box bearer because the controller's port publish / daemon-proxy\n * route is never a loopback hop inside the guest). Absent for the\n * default public-exposure path on providers that don't gate it, and for\n * providers/paths that can't gate the port at all — a caller that needs\n * a gated URL (e.g. `attachSandbox`) MUST treat a missing token as \"not\n * gated\", not as \"no auth needed\". The token is the raw secret; how a\n * client must PRESENT it (bearer header, cookie, …) is provider-specific\n * — see `authHeaders`. NEVER a tenant-wide or provider-infrastructure\n * credential: a provider that needs one of those to reach the box (e.g.\n * through an authenticated proxy route) MUST keep it internal and\n * inject it only on its own outgoing requests, never here — this field\n * is handed to callers and may be persisted (session descriptor,\n * `buildMcpConfigSnippet`'s paste-ready `.mcp.json`). */\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 /**\n * Run ONE command inside the box and return its exit code + captured\n * stdout/stderr — the seam behind the runtime's `sandbox_exec` MCP tool\n * (\"run my tests in the box without moving my whole session there\").\n * Optional: providers that cannot shell into the box omit this, and the\n * tool reports a clear unsupported error. Non-zero exits are RESULTS here\n * (the method resolves with their exitCode + streams), not throws — only\n * transport/timeout/setup failures reject.\n */\n exec?(opts: SandboxExecOpts): Promise<SandboxExecResult>\n /**\n * Read ONE file out of the box as raw bytes — how the runtime pulls a\n * `git bundle` back to the host at teardown. `path` is absolute INSIDE the\n * box. Optional: providers that cannot download files omit it. Rejects when\n * the file is missing or the transport fails.\n */\n readFile?(path: string): Promise<Uint8Array>\n /**\n * Write ONE file into the box (creating parent directories) — how the\n * runtime's `sandbox_exec` ships a script/payload too large to inline in the\n * command. `path` is absolute INSIDE the box. Optional: providers that\n * cannot upload files omit it; callers report a clear unsupported error.\n */\n writeFile?(path: string, data: Uint8Array): Promise<void>\n /**\n * The paired host's identity, present ONLY for the `device` sandbox\n * provider (`device:<fingerprint-or-name>` targets — the provider can\n * never guarantee it for a generic vendor sandbox). `fingerprint` is the\n * resolved REAL fingerprint of the registered host the sandbox\n * bridged onto, even when the caller addressed the device by its\n * user-given name — see `createDeviceSandboxProvider` in the runtime.\n */\n device?: { fingerprint: 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 * Release THIS session's provider-side claim on the box WITHOUT stopping\n * or pausing it — the shared-box survivor path. A session that leaves a box\n * other sessions still drive must still give back the slot it took at\n * attach (e.g. the computer controller's holder refcount); `stop()` would\n * kill the box under the survivors and `pause()` would suspend it.\n * Optional: providers with no per-session attach bookkeeping omit it.\n * Idempotent: releasing an already-released slot is a no-op.\n */\n detach?(): Promise<void>\n /**\n * Re-arm the box's remaining lifetime to `ms` from now — the same\n * mechanism a provider's own `connect()` already uses to re-arm a resumed\n * box's deadline (e.g. e2b's `sandbox.setTimeout`), exposed here so a host\n * can extend it again on detach WITHOUT a full reconnect, so a long-lived\n * session survives a host daemon restart without the box expiring out\n * from under it. Optional: providers with no fixed lifetime cap omit it.\n */\n extendTimeout?(ms: number): Promise<void>\n /** The lifetime (ms) this box was booted/re-armed with — what a host should\n * pass to `extendTimeout` when renewing during activity. Absent when the\n * provider has no fixed lifetime. */\n lifetimeMs?: number\n}\n\n/** One-shot command-execution request, as passed to `BootedSandbox.exec` by\n * the runtime's `sandbox_exec` MCP tool. */\nexport interface SandboxExecOpts {\n /** Shell command to run inside the box, passed verbatim to the provider's\n * SDK (e.g. e2b's `sandbox.commands.run`, which shells it itself). */\n command: string\n /** Absolute path INSIDE the box to run the command from (e.g. e2b's\n * `/home/user`). Provider-specific path semantics — callers must pass a\n * path that exists in the box, never a HOST path. */\n cwd?: string\n /** Extra env vars for THIS command only (merged onto the box's own\n * sandbox-level env by the provider's SDK; not persisted). */\n env?: Record<string, string>\n /** Per-command timeout in ms — what the box kills the process after when\n * exceeded. Provider-dependent error shape on expiry (e.g. e2b throws a\n * TimeoutError); the caller treats any throw as a failed exec. */\n timeoutMs?: number\n}\n\n/** What a successful `BootedSandbox.exec` reports. The caller truncates the\n * streams for display — the provider returns them whole. */\nexport interface SandboxExecResult {\n /** Process exit code (0 = success). Non-zero exits are a NORMAL result\n * here, not an error: callers decide what to do with them. */\n exitCode: number\n stdout: string\n stderr: string\n /** Wall-clock duration of the exec call, ms. */\n durationMs: number\n /** True when the provider's captured output was clipped BY ITS OWN STREAM\n * CAP (not the command's exit) — surfaced so a caller pulling a large file\n * out through a stream-capped exec API knows the data is incomplete.\n * Absent for providers whose capture is uncapped. */\n truncated?: boolean\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 * Optional progress logger for the daemon's own log. Providers that have\n * long, multi-phase boots call this once per phase (e.g. \"create\n * requested\", \"running\", \"daemon healthy\") so an operator can see where a\n * slow boot is stuck. Absent means the provider must stay silent; its\n * package is not a logger.\n */\n log?: (line: string) => void\n /**\n * Aborts a boot whose session was killed while it was still `starting`.\n * Providers with a multi-phase boot should check it between phases and\n * throw `SandboxBootCancelledError`; the caller then tears the box down\n * through its normal failed-boot path instead of adopting the session.\n */\n signal?: AbortSignal\n}\n\n/** Thrown by a provider's boot/connect when the caller cancelled a spawn\n * while the box was still booting. The caller must not register a session\n * for it; the box is expected to be cleaned up by the failed-boot path. */\nexport class SandboxBootCancelledError extends Error {\n /** The provider box id, when it was known before the cancel was observed. */\n readonly sandboxId?: string\n /** Concrete provider that was booting, when a composite provider (`auto`)\n * tagged the error — so the failed-boot ledger row names a resolvable\n * provider instead of the composite slug. */\n provider?: string\n\n constructor(message = \"sandbox boot cancelled\", sandboxId?: string) {\n super(message)\n this.name = \"SandboxBootCancelledError\"\n if (sandboxId !== undefined) this.sandboxId = sandboxId\n }\n}\n\n/** Structural check (name-based, so it works across package boundaries and\n * with mocked errors). */\nexport function isSandboxBootCancelledError(err: unknown): err is SandboxBootCancelledError {\n return err instanceof Error && err.name === \"SandboxBootCancelledError\"\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 * One box as the PROVIDER's control plane reports it - the shape\n * `SandboxProvider.listBoxes` returns. It is deliberately thin: the gc only\n * needs the identity, the state, how many session slots the provider still\n * counts for the box, and (when the provider knows it) the memory the box\n * holds and when it was created.\n */\nexport interface SandboxProviderBox {\n /** Provider-assigned box id - the same id `boot()` returned. */\n id: string\n /** Provider-reported state (\"running\", \"stopped\", …), when available. */\n state?: string\n /** Session slots the provider still counts for this box. */\n sessions?: number\n /** Memory the box holds, in MiB - when the provider reports it. */\n memoryMiB?: number\n /** Creation timestamp (ISO-8601) - when the provider reports it. */\n createdAt?: string\n}\n\n/**\n * One box as the unified inventory surface reports it\n * (`SandboxProvider.list`). Wider than `SandboxProviderBox`: identity,\n * ownership, sizing and activity, as far as the provider's control plane\n * knows them.\n *\n * Missing means UNKNOWN: a field the provider does not report stays\n * `undefined` and is never inferred from another field (in particular a\n * missing `tenantId`/`endUser` is not \"the default tenant\").\n */\nexport interface SandboxBox {\n /** Provider-assigned box id - the same id `boot()` returned. */\n id: string\n /** Concrete provider that owns the box. */\n provider: \"computer\" | \"e2b\"\n /** Provider-reported state (\"running\", \"stopped\", …), when available. */\n state?: string\n /** Owning product tenant, when the provider reports it. */\n tenantId?: string\n /** End user inside the tenant, when the provider reports it. */\n endUser?: string\n /** Provider host identifier (the computer controller's `hostId`), when\n * reported. */\n host?: string\n /** Virtual CPUs allocated to the box, when reported. */\n cpus?: number\n /** Memory the box holds, in MiB - when reported. */\n memoryMiB?: number\n /** Creation timestamp (ISO-8601) - when reported. */\n createdAt?: string\n /** Start of the current active interval (ISO-8601) - when reported. */\n activeSince?: string\n /** Last activity timestamp (ISO-8601) - when reported. */\n lastActivityAt?: string\n /** Public egress address, `null` when the provider says the box has none,\n * `undefined` when the provider does not report egress. */\n egressIp?: string | null\n /** Session slots the provider still counts for this box, when reported. */\n sessions?: number\n /** Accumulated cost in EUR, when the provider reports it; `null` when it\n * knows the box but has no cost model. */\n costEur?: number | null\n}\n\n/**\n * One usage window, the exact W0 `UsageWindow` shape from\n * `projects/computer/CONTRACT-SCALE.md` (camelCase JSON, ISO-8601 UTC\n * times, integer seconds, EUR). Providers map their own ledger onto this\n * without adding or renaming fields.\n */\nexport interface SandboxUsageWindow {\n /** Box id the window belongs to. */\n computer: string\n /** Owning product tenant. */\n tenantId: string\n /** End user inside the tenant, when the ledger knows it. */\n endUser?: string\n /** Concrete provider that produced the window. */\n provider: \"computer\" | \"e2b\"\n /** Provider host identifier, when the provider reports it. */\n host?: string\n /** Window start (ISO-8601). */\n from: string\n /** Window end (ISO-8601). */\n to: string\n /** Seconds the box was active in the window. */\n activeSec: number\n /** `activeSec * cpus`. */\n vcpuSec: number\n /** `activeSec * memoryMiB / 1024`. */\n gibSec: number\n /** Cost for the window in EUR, from the provider's cost model; `null`\n * when the host has no price model, never `0`. */\n costEur: number | null\n /** Opening ledger event id, when the provider exposes one (the fleet\n * dedupes windows on it). */\n eventId?: 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 * Open an existing box for a ONE-SHOT command (`sandbox_exec`) without\n * joining a session. Optional: providers that can expose `exec` on an\n * already-running box without taking a session slot (and without probing,\n * restarting or re-tokening its daemon) implement it; `sandbox_exec` uses\n * it when present and falls back to `connect()` otherwise. The returned\n * handle is not an ownership claim: its `stop()` must never detach a\n * session or tear down the box.\n */\n connectForExec?(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 * Destroy the box on the provider control plane WITHOUT connecting to it.\n * The teardown/gc counterpart of `connect()` + `stop()`: connecting to an\n * existing box implies RESUMING a suspended one first, on providers whose\n * resume allocates RAM/CPU (the `computer` controller) — capacity a\n * crowded host does not have, and exactly what made `sandbox rm --box`\n * fail with \"host capacity exhausted\" even though deleting a box frees\n * resources. `remove` needs no capacity: a suspended box is deleted as-is\n * (its RAM snapshot is freed), releasing MORE than it ever costs.\n * Must treat \"the id is already gone\" as SUCCESS (idempotent) so gc passes\n * converge; only a provider-side rejection propagates.\n * SHARED-BOX SAFETY (AIP-36): by DEFAULT `remove` must NOT force the\n * destroy past a controller that still counts attached sessions — a\n * shared box under live sessions must not be destroyed just because one\n * caller asked. Providers that accept `opts.force` treat `409` asked\n * without force as a soft \"still in use\" (non-fatal), and the ONLY\n * caller that may force the gc path, after its attaché candidate check.\n * Optional: providers that cannot destroy without a live control-plane\n * route omit it; callers fall back to `connect()` + `stop()`.\n */\n remove?(sandboxId: string, opts?: { force?: boolean }): Promise<void>\n /**\n * List the boxes this provider's control plane still holds - the surface\n * the orphan-box gc reads. A box the provider still lists (and still\n * charges RAM for) that no live agentproto session references is an\n * ORPHAN: the provider keeps it (and its memory) while nobody can use it.\n * Optional: providers whose control plane cannot enumerate their boxes\n * omit it, and the gc simply has no candidate for that provider.\n */\n listBoxes?(): Promise<SandboxProviderBox[]>\n /**\n * List the boxes for the unified inventory surface. Same control-plane\n * read as `listBoxes`, with the ownership/sizing/activity fields the\n * fleet and ops views need. Providers that cannot enumerate boxes omit\n * it. `from`/`to` optionally scope a provider that can filter its view by\n * time; providers with no such filter ignore them.\n */\n list?(opts?: { from?: string; to?: string }): Promise<SandboxBox[]>\n /**\n * Usage windows the provider's ledger covers for `[from, to]`, one per\n * active interval, optionally restricted to one `endUser`. Providers\n * without a usage ledger omit it. Windows are mapped exactly as\n * `SandboxUsageWindow`; unknown fields stay absent rather than being\n * filled in.\n */\n usage?(opts: { from: string; to: string; endUser?: string }): Promise<SandboxUsageWindow[]>\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 /** Optional per-phase boot logger forwarded to the provider; see\n * `SandboxBootOpts.log`. Absent keeps the provider silent. */\n log?: (line: string) => void\n /** Abort a boot whose session was killed mid-provision; see\n * `SandboxBootOpts.signal`. */\n signal?: AbortSignal\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 /** Concrete provider that produced the box (`BootedSandbox.provider`) —\n * forwarded so a caller records the ACTUAL backend, not the requested\n * composite slug. Absent for a single-provider boot. */\n provider?: BootedSandbox[\"provider\"]\n /** How a composite provider chose the backend (`BootedSandbox.choice`) —\n * forwarded for the descriptor + operator log. Absent for a\n * single-provider boot. */\n choice?: BootedSandbox[\"choice\"]\n /** Headers the box daemon's gated HTTP routes require — the same ones the\n * MCP connection presents. Absent for an ungated daemon. */\n authHeaders?: Record<string, 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 /** The egress address brokered for the box — forwarded from\n * `BootedSandbox.egressIp` so a caller can record it without reaching\n * into the booted handle after the fact. Absent when the provider has no\n * egress-IP concept. */\n egressIp?: BootedSandbox[\"egressIp\"]\n /** The paired host's resolved fingerprint — forwarded from\n * `BootedSandbox.device`, present only for the `device` provider\n * (BOOTSTRAP P7a's `hostFingerprint` source). */\n device?: BootedSandbox[\"device\"]\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 /** Run one command inside the box — forwarded from `BootedSandbox.exec`.\n * Absent when the provider can't shell into the box. */\n exec?: BootedSandbox[\"exec\"]\n /** Download one file from the box — forwarded from `BootedSandbox.readFile`.\n * Absent when the provider can't. */\n readFile?: BootedSandbox[\"readFile\"]\n /** Upload one file into the box — forwarded from `BootedSandbox.writeFile`.\n * Absent when the provider can't. */\n writeFile?: BootedSandbox[\"writeFile\"]\n /** Re-arm the box's remaining lifetime — forwarded from\n * `BootedSandbox.extendTimeout`. Absent when the provider has no fixed\n * lifetime cap to re-arm. */\n extendTimeout?: BootedSandbox[\"extendTimeout\"]\n /** The box's configured lifetime — forwarded from `BootedSandbox.lifetimeMs`. */\n lifetimeMs?: BootedSandbox[\"lifetimeMs\"]\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 /** Close the daemon connection and release THIS session's provider-side\n * claim on the box, leaving the box itself running for other sessions —\n * only present when the booted sandbox supports `detach()`. */\n detach?(): 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, {\n env,\n ...(opts.log ? { log: opts.log } : {}),\n ...(opts.signal ? { signal: opts.signal } : {}),\n })\n } else {\n booted = await opts.provider.boot(opts.spec, {\n env,\n ...(opts.log ? { log: opts.log } : {}),\n ...(opts.signal ? { signal: opts.signal } : {}),\n })\n }\n // A box whose public edge has NO provider-side auth of its own (InstaBox)\n // gates its daemon with a per-box bearer; Box's private path gates with a\n // `_port_auth` cookie. Surface whatever `booted.authHeaders`/`token` the\n // provider returned so the immediate host connection authenticates too —\n // without this the boot succeeds but the daemon MCP connect 401s, the box\n // gets reaped, and `agent_start` always fails. Absent ⇒ unchanged.\n const headers =\n booted.authHeaders ??\n (booted.token ? { Authorization: `Bearer ${booted.token}` } : undefined)\n let host: DaemonAgentSessionHost\n try {\n host = await connectDaemonAgentSessionHost({\n url: booted.mcpUrl,\n ...(headers ? { headers } : {}),\n })\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 a REconnected box when the provider supports it (it was already\n // there before this spawn, so keep it reconnectable), kill a box this\n // spawn just created (nothing earlier lives in it). Then rethrow\n // annotated so the caller can stamp the ledger with the actual outcome.\n const pauseIt = booted.pause !== undefined && opts.sandboxId !== undefined\n const cleanedUp = pauseIt ? \"paused\" : \"stopped\"\n if (pauseIt && 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 {\n sandboxId: booted.sandboxId,\n cleanedUp,\n ...(booted.provider !== undefined ? { provider: booted.provider } : {}),\n },\n )\n }\n return {\n ...host,\n mcpUrl: booted.mcpUrl,\n sandboxId: booted.sandboxId,\n ...(booted.provider !== undefined ? { provider: booted.provider } : {}),\n ...(booted.choice !== undefined ? { choice: booted.choice } : {}),\n ...(headers ? { authHeaders: headers } : {}),\n ...(booted.ports ? { ports: booted.ports } : {}),\n ...(booted.egressIp !== undefined ? { egressIp: booted.egressIp } : {}),\n ...(booted.expose ? { expose: booted.expose.bind(booted) } : {}),\n ...(booted.exec ? { exec: booted.exec.bind(booted) } : {}),\n ...(booted.readFile ? { readFile: booted.readFile.bind(booted) } : {}),\n ...(booted.writeFile ? { writeFile: booted.writeFile.bind(booted) } : {}),\n ...(booted.extendTimeout ? { extendTimeout: booted.extendTimeout.bind(booted) } : {}),\n ...(booted.lifetimeMs !== undefined ? { lifetimeMs: booted.lifetimeMs } : {}),\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 ...(booted.detach\n ? {\n async detach(): Promise<void> {\n await host.close()\n await booted.detach!()\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 * Error-text hygiene for provider `exec` failures. SDKs (e2b) echo the whole\n * shelled command into their error message — for a 9 KB inline script that is\n * unreadable, and any secret placed in the command leaks into logs/transcripts.\n */\n\n/** Characters of a command kept in an error message. */\nexport const EXEC_ERROR_COMMAND_MAX_CHARS = 200\n\n/** `command` cut to {@link EXEC_ERROR_COMMAND_MAX_CHARS} with a `…(N chars)` marker. */\nexport function shortenCommand(command: string, max = EXEC_ERROR_COMMAND_MAX_CHARS): string {\n if (command.length <= max) return command\n return `${command.slice(0, max)}…(${command.length} chars)`\n}\n\n/** Replace every verbatim occurrence of `command` in `message` with its\n * shortened form. A short command is left alone (nothing to gain). */\nexport function scrubCommandFromMessage(message: string, command: string): string {\n if (command.length <= EXEC_ERROR_COMMAND_MAX_CHARS || command.length === 0) return message\n return message.split(command).join(shortenCommand(command))\n}\n","/**\n * Thrown by a provider's `boot()` when the VENDOR ACCOUNT cannot host another\n * box: plan caps (max concurrent/total machines) surfaced as 402/403/429 or\n * an explicit quota code/message. Distinct from a generic boot failure so a\n * caller (the session spawn path) can fall back to ANOTHER provider instead\n * of surfacing the failure to the user as dead-end noise.\n */\nexport class SandboxQuotaError extends Error {\n /** The provider slug (e.g. \"e2b\", \"box\", \"instabox\") that reported the cap. */\n readonly provider: string\n\n constructor(provider: string, message?: string, cause?: unknown) {\n super(\n message ??\n `sandbox provider \"${provider}\" has no capacity left for another box ` +\n \"(account limit reached): retrying on the same provider will not help; \" +\n \"fall back to a different sandbox provider.\",\n )\n this.name = \"SandboxQuotaError\"\n this.provider = provider\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 provider account is\n * out of room\". */\nexport function isSandboxQuotaError(err: unknown): boolean {\n return err instanceof Error && err.name === \"SandboxQuotaError\"\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 * Kill is the default: absent any explicit lifecycle declaration (or a\n * reconnect target), the box is destroyed on close rather than left\n * paused-and-billed. Pause is an explicit opt-in — see\n * `resolveLifecyclePolicy`'s doc. */\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 * Kill is the default teardown: absent `destroy_on`, `pause_after_idle`,\n * AND `reuse`, a closed box is destroyed rather than left paused (and\n * billed) with nothing pointed at it — a fleet of daemons defaulting to\n * \"leave it running\" is exactly how a provider account accumulates\n * dozens of forgotten paused boxes (see the 2026-09 sandbox-ledger\n * reconcile finding). Pause is the explicit opt-in: `pause_after_idle`\n * declares the box should idle out on its own schedule rather than die\n * immediately, and `reuse` means THIS boot is itself a reconnect to an\n * existing box — killing it on close would undo the very reason it was\n * reused. `destroy_on` stays authoritative over both (the spec states\n * outright the box must not survive session close).\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\" = pauseAfterIdleMs !== undefined || reuse ? \"pause\" : \"kill\"\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 = \"sandbox/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 SandboxBootCancelledError,\n isSandboxBootCancelledError,\n SandboxHostBootFailedError,\n type SandboxSpec,\n type BootedSandbox,\n type SandboxBootOpts,\n type SandboxExecOpts,\n type SandboxExecResult,\n type SandboxProvider,\n type SandboxProbeResult,\n type SandboxProviderChoice,\n type SandboxProviderBox,\n type SandboxBox,\n type SandboxUsageWindow,\n type SandboxSecretsConfig,\n type CreateSandboxAgentSessionHostOpts,\n type SandboxAgentSessionHost,\n} from \"./agent-session-host.js\"\n\nexport {\n EXEC_ERROR_COMMAND_MAX_CHARS,\n shortenCommand,\n scrubCommandFromMessage,\n} from \"./exec-errors.js\"\n\nexport { SandboxQuotaError, isSandboxQuotaError } from \"./quota-error.js\"\n\nexport { resolveLifecyclePolicy, type SandboxLifecyclePolicy } from \"./lifecycle.js\"\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/agent-session-host.ts","../src/exec-errors.ts","../src/quota-error.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 it was a\n * reconnect and the 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 /** Concrete provider that owned the box, when the boot came through a\n * composite provider (`auto`). Absent for a single-provider boot, where\n * the caller's requested slug already IS the provider. */\n readonly provider?: string\n\n constructor(\n message: string,\n info: { sandboxId: string; cleanedUp: \"paused\" | \"stopped\"; provider?: string },\n ) {\n super(message)\n this.name = \"SandboxHostBootFailedError\"\n this.sandboxId = info.sandboxId\n this.cleanedUp = info.cleanedUp\n if (info.provider !== undefined) this.provider = info.provider\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/**\n * How a composite provider (e.g. `auto`) chose the concrete backend for one\n * boot. `requested` is the slug the caller asked for, `provider` the one that\n * actually produced the box; `from` names the provider a fallback moved away\n * from (when there was one), and `reason` is a short, secret-free explanation.\n */\nexport interface SandboxProviderChoice {\n /** Slug the caller requested (e.g. `\"auto\"`). */\n requested: string\n /** Concrete provider that actually produced the box. */\n provider: string\n /** Provider a fallback moved away from, when the choice was not the first\n * candidate (e.g. `\"computer\"` on an overflow to e2b). */\n from?: string\n /** Short, secret-free explanation for the choice. */\n reason?: string\n /** ISO instant the choice was made. */\n at: string\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 /** Concrete provider that produced this box, for a composite provider\n * (`auto`). Absent for a single-provider boot, where the caller's own\n * requested slug already IS the provider. */\n provider?: string\n /** How a composite provider chose this backend. Absent for a\n * single-provider boot. */\n choice?: SandboxProviderChoice\n /** The egress address the provider brokered for this box, when it supports\n * selecting one (AIP-36 `network.egressIp` / provider config `egressIp`).\n * `null` means the controller explicitly reports no dedicated address\n * (for example `\"shared\"`); absent means the provider/controller has no\n * egress-IP concept. Optional so existing providers are unaffected. */\n egressIp?: { id: string; publicIp: string } | null\n /** Opaque secret gating `mcpUrl`. Present either when `opts.expose ===\n * \"private\"` was honoured (see `SandboxBootOpts.expose`), OR when the\n * provider's own daemon is gated unconditionally regardless of `expose`\n * (e.g. `computer`, whose in-box daemon always requires its own\n * per-box bearer because the controller's port publish / daemon-proxy\n * route is never a loopback hop inside the guest). Absent for the\n * default public-exposure path on providers that don't gate it, and for\n * providers/paths that can't gate the port at all — a caller that needs\n * a gated URL (e.g. `attachSandbox`) MUST treat a missing token as \"not\n * gated\", not as \"no auth needed\". The token is the raw secret; how a\n * client must PRESENT it (bearer header, cookie, …) is provider-specific\n * — see `authHeaders`. NEVER a tenant-wide or provider-infrastructure\n * credential: a provider that needs one of those to reach the box (e.g.\n * through an authenticated proxy route) MUST keep it internal and\n * inject it only on its own outgoing requests, never here — this field\n * is handed to callers and may be persisted (session descriptor,\n * `buildMcpConfigSnippet`'s paste-ready `.mcp.json`). */\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 /**\n * Run ONE command inside the box and return its exit code + captured\n * stdout/stderr — the seam behind the runtime's `sandbox_exec` MCP tool\n * (\"run my tests in the box without moving my whole session there\").\n * Optional: providers that cannot shell into the box omit this, and the\n * tool reports a clear unsupported error. Non-zero exits are RESULTS here\n * (the method resolves with their exitCode + streams), not throws — only\n * transport/timeout/setup failures reject.\n */\n exec?(opts: SandboxExecOpts): Promise<SandboxExecResult>\n /**\n * Read ONE file out of the box as raw bytes — how the runtime pulls a\n * `git bundle` back to the host at teardown. `path` is absolute INSIDE the\n * box. Optional: providers that cannot download files omit it. Rejects when\n * the file is missing or the transport fails.\n */\n readFile?(path: string): Promise<Uint8Array>\n /**\n * Write ONE file into the box (creating parent directories) — how the\n * runtime's `sandbox_exec` ships a script/payload too large to inline in the\n * command. `path` is absolute INSIDE the box. Optional: providers that\n * cannot upload files omit it; callers report a clear unsupported error.\n */\n writeFile?(path: string, data: Uint8Array): Promise<void>\n /**\n * The paired host's identity, present ONLY for the `device` sandbox\n * provider (`device:<fingerprint-or-name>` targets — the provider can\n * never guarantee it for a generic vendor sandbox). `fingerprint` is the\n * resolved REAL fingerprint of the registered host the sandbox\n * bridged onto, even when the caller addressed the device by its\n * user-given name — see `createDeviceSandboxProvider` in the runtime.\n */\n device?: { fingerprint: 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 * Release THIS session's provider-side claim on the box WITHOUT stopping\n * or pausing it — the shared-box survivor path. A session that leaves a box\n * other sessions still drive must still give back the slot it took at\n * attach (e.g. the computer controller's holder refcount); `stop()` would\n * kill the box under the survivors and `pause()` would suspend it.\n * Optional: providers with no per-session attach bookkeeping omit it.\n * Idempotent: releasing an already-released slot is a no-op.\n */\n detach?(): Promise<void>\n /**\n * Re-arm the box's remaining lifetime to `ms` from now — the same\n * mechanism a provider's own `connect()` already uses to re-arm a resumed\n * box's deadline (e.g. e2b's `sandbox.setTimeout`), exposed here so a host\n * can extend it again on detach WITHOUT a full reconnect, so a long-lived\n * session survives a host daemon restart without the box expiring out\n * from under it. Optional: providers with no fixed lifetime cap omit it.\n */\n extendTimeout?(ms: number): Promise<void>\n /** The lifetime (ms) this box was booted/re-armed with — what a host should\n * pass to `extendTimeout` when renewing during activity. Absent when the\n * provider has no fixed lifetime. */\n lifetimeMs?: number\n}\n\n/** One-shot command-execution request, as passed to `BootedSandbox.exec` by\n * the runtime's `sandbox_exec` MCP tool. */\nexport interface SandboxExecOpts {\n /** Shell command to run inside the box, passed verbatim to the provider's\n * SDK (e.g. e2b's `sandbox.commands.run`, which shells it itself). */\n command: string\n /** Absolute path INSIDE the box to run the command from (e.g. e2b's\n * `/home/user`). Provider-specific path semantics — callers must pass a\n * path that exists in the box, never a HOST path. */\n cwd?: string\n /** Extra env vars for THIS command only (merged onto the box's own\n * sandbox-level env by the provider's SDK; not persisted). */\n env?: Record<string, string>\n /** Per-command timeout in ms — what the box kills the process after when\n * exceeded. Provider-dependent error shape on expiry (e.g. e2b throws a\n * TimeoutError); the caller treats any throw as a failed exec. */\n timeoutMs?: number\n}\n\n/** What a successful `BootedSandbox.exec` reports. The caller truncates the\n * streams for display — the provider returns them whole. */\nexport interface SandboxExecResult {\n /** Process exit code (0 = success). Non-zero exits are a NORMAL result\n * here, not an error: callers decide what to do with them. */\n exitCode: number\n stdout: string\n stderr: string\n /** Wall-clock duration of the exec call, ms. */\n durationMs: number\n /** True when the provider's captured output was clipped BY ITS OWN STREAM\n * CAP (not the command's exit) — surfaced so a caller pulling a large file\n * out through a stream-capped exec API knows the data is incomplete.\n * Absent for providers whose capture is uncapped. */\n truncated?: boolean\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 * Optional progress logger for the daemon's own log. Providers that have\n * long, multi-phase boots call this once per phase (e.g. \"create\n * requested\", \"running\", \"daemon healthy\") so an operator can see where a\n * slow boot is stuck. Absent means the provider must stay silent; its\n * package is not a logger.\n */\n log?: (line: string) => void\n /**\n * Aborts a boot whose session was killed while it was still `starting`.\n * Providers with a multi-phase boot should check it between phases and\n * throw `SandboxBootCancelledError`; the caller then tears the box down\n * through its normal failed-boot path instead of adopting the session.\n */\n signal?: AbortSignal\n}\n\n/** Thrown by a provider's boot/connect when the caller cancelled a spawn\n * while the box was still booting. The caller must not register a session\n * for it; the box is expected to be cleaned up by the failed-boot path. */\nexport class SandboxBootCancelledError extends Error {\n /** The provider box id, when it was known before the cancel was observed. */\n readonly sandboxId?: string\n /** Concrete provider that was booting, when a composite provider (`auto`)\n * tagged the error — so the failed-boot ledger row names a resolvable\n * provider instead of the composite slug. */\n provider?: string\n\n constructor(message = \"sandbox boot cancelled\", sandboxId?: string) {\n super(message)\n this.name = \"SandboxBootCancelledError\"\n if (sandboxId !== undefined) this.sandboxId = sandboxId\n }\n}\n\n/** Structural check (name-based, so it works across package boundaries and\n * with mocked errors). */\nexport function isSandboxBootCancelledError(err: unknown): err is SandboxBootCancelledError {\n return err instanceof Error && err.name === \"SandboxBootCancelledError\"\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 * One box as the PROVIDER's control plane reports it - the shape\n * `SandboxProvider.listBoxes` returns. It is deliberately thin: the gc only\n * needs the identity, the state, how many session slots the provider still\n * counts for the box, and (when the provider knows it) the memory the box\n * holds and when it was created.\n */\nexport interface SandboxProviderBox {\n /** Provider-assigned box id - the same id `boot()` returned. */\n id: string\n /** Provider-reported state (\"running\", \"stopped\", …), when available. */\n state?: string\n /** The agentproto session that booted the box, when the provider recorded\n * it (e2b: the `owner` metadata stamped at boot). */\n owner?: string\n /** Session slots the provider still counts for this box. */\n sessions?: number\n /** Memory the box holds, in MiB - when the provider reports it. */\n memoryMiB?: number\n /** Creation timestamp (ISO-8601) - when the provider reports it. */\n createdAt?: string\n}\n\n/**\n * One box as the unified inventory surface reports it\n * (`SandboxProvider.list`). Wider than `SandboxProviderBox`: identity,\n * ownership, sizing and activity, as far as the provider's control plane\n * knows them.\n *\n * Missing means UNKNOWN: a field the provider does not report stays\n * `undefined` and is never inferred from another field (in particular a\n * missing `tenantId`/`endUser` is not \"the default tenant\").\n */\nexport interface SandboxBox {\n /** Provider-assigned box id - the same id `boot()` returned. */\n id: string\n /** Concrete provider that owns the box. */\n provider: \"computer\" | \"e2b\"\n /** Provider-reported state (\"running\", \"stopped\", …), when available. */\n state?: string\n /** Owning product tenant, when the provider reports it. */\n tenantId?: string\n /** End user inside the tenant, when the provider reports it. */\n endUser?: string\n /** The agentproto session that booted the box, when the provider recorded\n * it (e2b: the `owner` metadata stamped at boot). */\n owner?: string\n /** That session's label, when the provider recorded it. */\n label?: string\n /** Provider host identifier (the computer controller's `hostId`), when\n * reported. */\n host?: string\n /** Virtual CPUs allocated to the box, when reported. */\n cpus?: number\n /** Memory the box holds, in MiB - when reported. */\n memoryMiB?: number\n /** Creation timestamp (ISO-8601) - when reported. */\n createdAt?: string\n /** Start of the current active interval (ISO-8601) - when reported. */\n activeSince?: string\n /** Last activity timestamp (ISO-8601) - when reported. */\n lastActivityAt?: string\n /** Public egress address, `null` when the provider says the box has none,\n * `undefined` when the provider does not report egress. */\n egressIp?: string | null\n /** Session slots the provider still counts for this box, when reported. */\n sessions?: number\n /** Accumulated cost in EUR, when the provider reports it; `null` when it\n * knows the box but has no cost model. */\n costEur?: number | null\n}\n\n/**\n * One usage window, the exact W0 `UsageWindow` shape from\n * `projects/computer/CONTRACT-SCALE.md` (camelCase JSON, ISO-8601 UTC\n * times, integer seconds, EUR). Providers map their own ledger onto this\n * without adding or renaming fields.\n */\nexport interface SandboxUsageWindow {\n /** Box id the window belongs to. */\n computer: string\n /** Owning product tenant. */\n tenantId: string\n /** End user inside the tenant, when the ledger knows it. */\n endUser?: string\n /** Concrete provider that produced the window. */\n provider: \"computer\" | \"e2b\"\n /** Provider host identifier, when the provider reports it. */\n host?: string\n /** Window start (ISO-8601). */\n from: string\n /** Window end (ISO-8601). */\n to: string\n /** Seconds the box was active in the window. */\n activeSec: number\n /** `activeSec * cpus`. */\n vcpuSec: number\n /** `activeSec * memoryMiB / 1024`. */\n gibSec: number\n /** Cost for the window in EUR, from the provider's cost model; `null`\n * when the host has no price model, never `0`. */\n costEur: number | null\n /** Opening ledger event id, when the provider exposes one (the fleet\n * dedupes windows on it). */\n eventId?: 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 * Open an existing box for a ONE-SHOT command (`sandbox_exec`) without\n * joining a session. Optional: providers that can expose `exec` on an\n * already-running box without taking a session slot (and without probing,\n * restarting or re-tokening its daemon) implement it; `sandbox_exec` uses\n * it when present and falls back to `connect()` otherwise. The returned\n * handle is not an ownership claim: its `stop()` must never detach a\n * session or tear down the box.\n */\n connectForExec?(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 * Destroy the box on the provider control plane WITHOUT connecting to it.\n * The teardown/gc counterpart of `connect()` + `stop()`: connecting to an\n * existing box implies RESUMING a suspended one first, on providers whose\n * resume allocates RAM/CPU (the `computer` controller) — capacity a\n * crowded host does not have, and exactly what made `sandbox rm --box`\n * fail with \"host capacity exhausted\" even though deleting a box frees\n * resources. `remove` needs no capacity: a suspended box is deleted as-is\n * (its RAM snapshot is freed), releasing MORE than it ever costs.\n * Must treat \"the id is already gone\" as SUCCESS (idempotent) so gc passes\n * converge; only a provider-side rejection propagates.\n * SHARED-BOX SAFETY (AIP-36): by DEFAULT `remove` must NOT force the\n * destroy past a controller that still counts attached sessions — a\n * shared box under live sessions must not be destroyed just because one\n * caller asked. Providers that accept `opts.force` treat `409` asked\n * without force as a soft \"still in use\" (non-fatal), and the ONLY\n * caller that may force the gc path, after its attaché candidate check.\n * Optional: providers that cannot destroy without a live control-plane\n * route omit it; callers fall back to `connect()` + `stop()`.\n */\n remove?(sandboxId: string, opts?: { force?: boolean }): Promise<void>\n /**\n * List the boxes this provider's control plane still holds - the surface\n * the orphan-box gc reads. A box the provider still lists (and still\n * charges RAM for) that no live agentproto session references is an\n * ORPHAN: the provider keeps it (and its memory) while nobody can use it.\n * Optional: providers whose control plane cannot enumerate their boxes\n * omit it, and the gc simply has no candidate for that provider.\n */\n listBoxes?(): Promise<SandboxProviderBox[]>\n /**\n * List the boxes for the unified inventory surface. Same control-plane\n * read as `listBoxes`, with the ownership/sizing/activity fields the\n * fleet and ops views need. Providers that cannot enumerate boxes omit\n * it. `from`/`to` optionally scope a provider that can filter its view by\n * time; providers with no such filter ignore them.\n */\n list?(opts?: { from?: string; to?: string }): Promise<SandboxBox[]>\n /**\n * Usage windows the provider's ledger covers for `[from, to]`, one per\n * active interval, optionally restricted to one `endUser`. Providers\n * without a usage ledger omit it. Windows are mapped exactly as\n * `SandboxUsageWindow`; unknown fields stay absent rather than being\n * filled in.\n */\n usage?(opts: { from: string; to: string; endUser?: string }): Promise<SandboxUsageWindow[]>\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 /** Optional per-phase boot logger forwarded to the provider; see\n * `SandboxBootOpts.log`. Absent keeps the provider silent. */\n log?: (line: string) => void\n /** Abort a boot whose session was killed mid-provision; see\n * `SandboxBootOpts.signal`. */\n signal?: AbortSignal\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 /** Concrete provider that produced the box (`BootedSandbox.provider`) —\n * forwarded so a caller records the ACTUAL backend, not the requested\n * composite slug. Absent for a single-provider boot. */\n provider?: BootedSandbox[\"provider\"]\n /** How a composite provider chose the backend (`BootedSandbox.choice`) —\n * forwarded for the descriptor + operator log. Absent for a\n * single-provider boot. */\n choice?: BootedSandbox[\"choice\"]\n /** Headers the box daemon's gated HTTP routes require — the same ones the\n * MCP connection presents. Absent for an ungated daemon. */\n authHeaders?: Record<string, 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 /** The egress address brokered for the box — forwarded from\n * `BootedSandbox.egressIp` so a caller can record it without reaching\n * into the booted handle after the fact. Absent when the provider has no\n * egress-IP concept. */\n egressIp?: BootedSandbox[\"egressIp\"]\n /** The paired host's resolved fingerprint — forwarded from\n * `BootedSandbox.device`, present only for the `device` provider\n * (BOOTSTRAP P7a's `hostFingerprint` source). */\n device?: BootedSandbox[\"device\"]\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 /** Run one command inside the box — forwarded from `BootedSandbox.exec`.\n * Absent when the provider can't shell into the box. */\n exec?: BootedSandbox[\"exec\"]\n /** Download one file from the box — forwarded from `BootedSandbox.readFile`.\n * Absent when the provider can't. */\n readFile?: BootedSandbox[\"readFile\"]\n /** Upload one file into the box — forwarded from `BootedSandbox.writeFile`.\n * Absent when the provider can't. */\n writeFile?: BootedSandbox[\"writeFile\"]\n /** Re-arm the box's remaining lifetime — forwarded from\n * `BootedSandbox.extendTimeout`. Absent when the provider has no fixed\n * lifetime cap to re-arm. */\n extendTimeout?: BootedSandbox[\"extendTimeout\"]\n /** The box's configured lifetime — forwarded from `BootedSandbox.lifetimeMs`. */\n lifetimeMs?: BootedSandbox[\"lifetimeMs\"]\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 /** Close the daemon connection and release THIS session's provider-side\n * claim on the box, leaving the box itself running for other sessions —\n * only present when the booted sandbox supports `detach()`. */\n detach?(): 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, {\n env,\n ...(opts.log ? { log: opts.log } : {}),\n ...(opts.signal ? { signal: opts.signal } : {}),\n })\n } else {\n booted = await opts.provider.boot(opts.spec, {\n env,\n ...(opts.log ? { log: opts.log } : {}),\n ...(opts.signal ? { signal: opts.signal } : {}),\n })\n }\n // A box whose public edge has NO provider-side auth of its own (InstaBox)\n // gates its daemon with a per-box bearer; Box's private path gates with a\n // `_port_auth` cookie. Surface whatever `booted.authHeaders`/`token` the\n // provider returned so the immediate host connection authenticates too —\n // without this the boot succeeds but the daemon MCP connect 401s, the box\n // gets reaped, and `agent_start` always fails. Absent ⇒ unchanged.\n const headers =\n booted.authHeaders ??\n (booted.token ? { Authorization: `Bearer ${booted.token}` } : undefined)\n let host: DaemonAgentSessionHost\n try {\n host = await connectDaemonAgentSessionHost({\n url: booted.mcpUrl,\n ...(headers ? { headers } : {}),\n })\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 a REconnected box when the provider supports it (it was already\n // there before this spawn, so keep it reconnectable), kill a box this\n // spawn just created (nothing earlier lives in it). Then rethrow\n // annotated so the caller can stamp the ledger with the actual outcome.\n const pauseIt = booted.pause !== undefined && opts.sandboxId !== undefined\n const cleanedUp = pauseIt ? \"paused\" : \"stopped\"\n if (pauseIt && 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 {\n sandboxId: booted.sandboxId,\n cleanedUp,\n ...(booted.provider !== undefined ? { provider: booted.provider } : {}),\n },\n )\n }\n return {\n ...host,\n mcpUrl: booted.mcpUrl,\n sandboxId: booted.sandboxId,\n ...(booted.provider !== undefined ? { provider: booted.provider } : {}),\n ...(booted.choice !== undefined ? { choice: booted.choice } : {}),\n ...(headers ? { authHeaders: headers } : {}),\n ...(booted.ports ? { ports: booted.ports } : {}),\n ...(booted.egressIp !== undefined ? { egressIp: booted.egressIp } : {}),\n ...(booted.expose ? { expose: booted.expose.bind(booted) } : {}),\n ...(booted.exec ? { exec: booted.exec.bind(booted) } : {}),\n ...(booted.readFile ? { readFile: booted.readFile.bind(booted) } : {}),\n ...(booted.writeFile ? { writeFile: booted.writeFile.bind(booted) } : {}),\n ...(booted.extendTimeout ? { extendTimeout: booted.extendTimeout.bind(booted) } : {}),\n ...(booted.lifetimeMs !== undefined ? { lifetimeMs: booted.lifetimeMs } : {}),\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 ...(booted.detach\n ? {\n async detach(): Promise<void> {\n await host.close()\n await booted.detach!()\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 * Error-text hygiene for provider `exec` failures. SDKs (e2b) echo the whole\n * shelled command into their error message — for a 9 KB inline script that is\n * unreadable, and any secret placed in the command leaks into logs/transcripts.\n */\n\n/** Characters of a command kept in an error message. */\nexport const EXEC_ERROR_COMMAND_MAX_CHARS = 200\n\n/** `command` cut to {@link EXEC_ERROR_COMMAND_MAX_CHARS} with a `…(N chars)` marker. */\nexport function shortenCommand(command: string, max = EXEC_ERROR_COMMAND_MAX_CHARS): string {\n if (command.length <= max) return command\n return `${command.slice(0, max)}…(${command.length} chars)`\n}\n\n/** Replace every verbatim occurrence of `command` in `message` with its\n * shortened form. A short command is left alone (nothing to gain). */\nexport function scrubCommandFromMessage(message: string, command: string): string {\n if (command.length <= EXEC_ERROR_COMMAND_MAX_CHARS || command.length === 0) return message\n return message.split(command).join(shortenCommand(command))\n}\n","/**\n * Thrown by a provider's `boot()` when the VENDOR ACCOUNT cannot host another\n * box: plan caps (max concurrent/total machines) surfaced as 402/403/429 or\n * an explicit quota code/message. Distinct from a generic boot failure so a\n * caller (the session spawn path) can fall back to ANOTHER provider instead\n * of surfacing the failure to the user as dead-end noise.\n */\nexport class SandboxQuotaError extends Error {\n /** The provider slug (e.g. \"e2b\", \"box\", \"instabox\") that reported the cap. */\n readonly provider: string\n\n constructor(provider: string, message?: string, cause?: unknown) {\n super(\n message ??\n `sandbox provider \"${provider}\" has no capacity left for another box ` +\n \"(account limit reached): retrying on the same provider will not help; \" +\n \"fall back to a different sandbox provider.\",\n )\n this.name = \"SandboxQuotaError\"\n this.provider = provider\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 provider account is\n * out of room\". */\nexport function isSandboxQuotaError(err: unknown): boolean {\n return err instanceof Error && err.name === \"SandboxQuotaError\"\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 * Kill is the default: absent any explicit lifecycle declaration (or a\n * reconnect target), the box is destroyed on close rather than left\n * paused-and-billed. Pause is an explicit opt-in — see\n * `resolveLifecyclePolicy`'s doc. */\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 * Kill is the default teardown: absent `destroy_on`, `pause_after_idle`,\n * AND `reuse`, a closed box is destroyed rather than left paused (and\n * billed) with nothing pointed at it — a fleet of daemons defaulting to\n * \"leave it running\" is exactly how a provider account accumulates\n * dozens of forgotten paused boxes (see the 2026-09 sandbox-ledger\n * reconcile finding). Pause is the explicit opt-in: `pause_after_idle`\n * declares the box should idle out on its own schedule rather than die\n * immediately, and `reuse` means THIS boot is itself a reconnect to an\n * existing box — killing it on close would undo the very reason it was\n * reused. `destroy_on` stays authoritative over both (the spec states\n * outright the box must not survive session close).\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\" = pauseAfterIdleMs !== undefined || reuse ? \"pause\" : \"kill\"\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 = \"sandbox/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 SandboxBootCancelledError,\n isSandboxBootCancelledError,\n SandboxHostBootFailedError,\n type SandboxSpec,\n type BootedSandbox,\n type SandboxBootOpts,\n type SandboxExecOpts,\n type SandboxExecResult,\n type SandboxProvider,\n type SandboxProbeResult,\n type SandboxProviderChoice,\n type SandboxProviderBox,\n type SandboxBox,\n type SandboxUsageWindow,\n type SandboxSecretsConfig,\n type CreateSandboxAgentSessionHostOpts,\n type SandboxAgentSessionHost,\n} from \"./agent-session-host.js\"\n\nexport {\n EXEC_ERROR_COMMAND_MAX_CHARS,\n shortenCommand,\n scrubCommandFromMessage,\n} from \"./exec-errors.js\"\n\nexport { SandboxQuotaError, isSandboxQuotaError } from \"./quota-error.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.15.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",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"zod": "^4.6.7",
|
|
51
51
|
"@agentproto/doctype": "0.2.0",
|
|
52
52
|
"@agentproto/secrets": "2.2.0",
|
|
53
|
-
"@agentproto/workflow-runtime": "0.20.
|
|
54
|
-
"@agentproto/worktree": "0.
|
|
53
|
+
"@agentproto/workflow-runtime": "0.20.3",
|
|
54
|
+
"@agentproto/worktree": "0.20.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^25.9.9",
|