@agentproto/sandbox 0.12.1 → 0.13.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.
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import { createDoctype } from '@agentproto/define-doctype';
3
+
4
+ /**
5
+ * @agentproto/sandbox v0.1.0-alpha
6
+ * AIP-36 SANDBOX.md `defineSandbox` reference implementation.
7
+ */
8
+
9
+ var sandboxFrontmatterSchema = z.object({ "schema": z.literal("sandbox/v1").describe("Standalone-only. Identifies the doctype + version. Absent when the block is inlined.").optional(), "id": z.string().regex(new RegExp("^@[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$")).describe("Standalone-only. Globally addressable id `@<owner-slug>/<sandbox-slug>`.").optional(), "version": z.string().regex(new RegExp("^\\d+\\.\\d+\\.\\d+(?:[-+][\\w.\\-]+)?$")).describe("Standalone-only. Spec version of THIS file.").optional(), "provider": z.string().min(1).describe("Backend kind. Day-1 enumerated set: local | mastra-e2b | mastra-modal | mastra-daytona | mastra-blaxel | node-permission. Hosts MAY register additional ids."), "config": z.object({ "installAdapters": z.array(z.string().min(1)).describe(`Semantic harness slugs to pre-install in the box at boot (e.g. ["hermes", "claude-code"]). Each expands to @agentproto/adapter-<slug>@latest plus that adapter's declared boot extras; a config.installPackages pin for the same package always wins. Additive \u2014 absent means no extra install.`).optional() }).catchall(z.any()).describe("Provider-specific connection fields. Shape varies per provider (see AIP-36 \xA7Provider config shapes)."), "limits": z.object({ "timeout_ms": z.number().int().gte(1).optional(), "memory_mb": z.number().int().gte(1).optional(), "cpu_ms": z.number().int().gte(1).optional(), "cpus": z.number().gte(0.1).describe("vCPUs, may be fractional (AIP-36 v1.3). Provider config-level cpus maps onto this.").optional() }).strict().describe("Resource caps per command.").optional(), "env": z.object({ "auth": z.any().optional(), "passthrough": z.array(z.string()).describe("Static host env-var names to forward into the sandbox.").default([]), "autoPassthrough": z.boolean().describe("Opt-in: forward the spawn's resolved billing-credential env-var NAME (e.g. ANTHROPIC_API_KEY) into passthrough so a fresh box inherits host auth without the caller naming vars. Only the name is injected \u2014 the value travels via the normal passthrough mechanism.").optional() }).strict().optional(), "network": z.object({ "egress": z.array(z.string()).describe("Hostnames the sandbox MAY reach. Empty / missing = no egress.").default([]), "egressIp": z.string().regex(new RegExp("^(shared|auto|dedicated|[a-z0-9]{1,8})$")).describe("Public source address for outbound traffic (AIP-36 v1.3): shared | auto | dedicated | pool slot id. A provider without an address pool MUST refuse any value other than shared with sandbox_egress_pool_unavailable.").default("shared") }).strict().optional(), "join": z.object({ "tokenEnv": z.string().min(1).describe("Name of a host env var whose value (a join-token URL) should be forwarded into the sandbox under the SAME name at boot, so the box can auto-register with the daemon that minted the token. Equivalent to adding that name to env.passthrough \u2014 provided as a distinct, self-documenting field because its purpose (auto-join) is different from an arbitrary forwarded secret. Absent/no-op when the named host env var isn't actually set (e.g. a fork's CI run with no AGENTPROTO_JOIN secret) \u2014 never fails the boot.") }).strict().describe("Auto-join wiring: forwards a host env var (holding a join-token URL) into the sandbox by the same name, so the box can register itself with the daemon that minted the token.").optional(), "mounts": z.array(z.any()).describe("Filesystems mounted inside the sandbox at declared paths. Maps to Mastra Workspace.mounts.").default([]), "identity": z.any().describe("AIP-23 identity-ref \u2014 owner of the sandbox processes.").optional(), "policy": z.any().describe("AIP-38 POLICY block \u2014 access grants on sandbox actions.").optional(), "lifecycle": z.object({ "pause_after_idle": z.string().min(1).describe("AIP-37 event name (e.g. `idle-600` for 10 min). Provider-supported only (modal, daytona).").optional(), "destroy_on": z.string().min(1).describe("AIP-37 event name (e.g. `workspace-close`).").optional() }).strict().optional(), "read_only": z.boolean().describe("Reject command execution at the sandbox layer. Read-only sandbox calls fail with `sandbox_read_only`.").default(false), "extraPorts": z.array(z.number().int().gte(1)).describe("App ports to expose at boot time. Resolved into BootedSandbox.ports (port \u2192 public URL) by providers that support port exposure.").optional(), "metadata": z.record(z.string(), z.any()).describe("Free-form, namespaced. Adapter hints under `metadata.<adapter>.*`.").optional() }).strict().describe("Validates the YAML frontmatter portion of an AIP-36 SANDBOX.md manifest, OR the inline form embedded in any other manifest's `sandbox:` block. Compute-only \u2014 durable filesystem backings live in AIP-35 STORAGE.md.");
10
+ var defineSandboxInner = createDoctype({
11
+ aip: 36,
12
+ name: "sandbox",
13
+ readDescription: false,
14
+ // AIP-36 makes `id` standalone-only — inline sandbox blocks fall
15
+ // back to `provider` for the cross-AIP id-pattern check (mirrors
16
+ // AIP-43 § Identity).
17
+ readIdentity: (def) => {
18
+ if (typeof def.id === "string" && def.id.length > 0) return def.id;
19
+ return def.provider;
20
+ },
21
+ validate(def) {
22
+ const { factory: _factory, capabilities: _capabilities, ...manifest } = def;
23
+ const result = sandboxFrontmatterSchema.safeParse(manifest);
24
+ if (!result.success) {
25
+ throw new Error(
26
+ `defineSandbox (AIP-36): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
27
+ );
28
+ }
29
+ },
30
+ build(def) {
31
+ const { factory, capabilities, ...manifest } = def;
32
+ return {
33
+ ...manifest,
34
+ ...factory !== void 0 ? { factory } : {},
35
+ ...capabilities !== void 0 ? { capabilities: Object.freeze({ ...capabilities }) } : {}
36
+ };
37
+ }
38
+ });
39
+ function defineSandbox(definition) {
40
+ return defineSandboxInner(
41
+ definition
42
+ );
43
+ }
44
+
45
+ export { defineSandbox, sandboxFrontmatterSchema };
46
+ //# sourceMappingURL=chunk-DT5F2UYM.mjs.map
47
+ //# sourceMappingURL=chunk-DT5F2UYM.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts","../src/define-sandbox.ts"],"names":[],"mappings":"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","file":"chunk-DT5F2UYM.mjs","sourcesContent":["/**\n * AIP-36 SANDBOX.md frontmatter zod schema.\n *\n * Generated from `resources/aip-36/draft/SANDBOX.schema.json` via\n * json-schema-to-zod. Imported by both `define-sandbox.ts` (TS path\n * validation) and `manifest/index.ts` (.md path validation) so every\n * field-level constraint runs in both authoring paths from a single\n * source of truth — re-run scaffold-aip to refresh after spec changes.\n *\n * Cross-field rules (if/then/allOf in JSON Schema) don't translate\n * cleanly and live in `define-sandbox.ts`'s `validate(def)` instead.\n */\n\nimport { z } from \"zod\"\n\nexport const sandboxFrontmatterSchema = z.object({ \"schema\": z.literal(\"sandbox/v1\").describe(\"Standalone-only. Identifies the doctype + version. Absent when the block is inlined.\").optional(), \"id\": z.string().regex(new RegExp(\"^@[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$\")).describe(\"Standalone-only. Globally addressable id `@<owner-slug>/<sandbox-slug>`.\").optional(), \"version\": z.string().regex(new RegExp(\"^\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:[-+][\\\\w.\\\\-]+)?$\")).describe(\"Standalone-only. Spec version of THIS file.\").optional(), \"provider\": z.string().min(1).describe(\"Backend kind. Day-1 enumerated set: local | mastra-e2b | mastra-modal | mastra-daytona | mastra-blaxel | node-permission. Hosts MAY register additional ids.\"), \"config\": z.object({ \"installAdapters\": z.array(z.string().min(1)).describe(\"Semantic harness slugs to pre-install in the box at boot (e.g. [\\\"hermes\\\", \\\"claude-code\\\"]). Each expands to @agentproto/adapter-<slug>@latest plus that adapter's declared boot extras; a config.installPackages pin for the same package always wins. Additive — absent means no extra install.\").optional() }).catchall(z.any()).describe(\"Provider-specific connection fields. Shape varies per provider (see AIP-36 §Provider config shapes).\"), \"limits\": z.object({ \"timeout_ms\": z.number().int().gte(1).optional(), \"memory_mb\": z.number().int().gte(1).optional(), \"cpu_ms\": z.number().int().gte(1).optional(), \"cpus\": z.number().gte(0.1).describe(\"vCPUs, may be fractional (AIP-36 v1.3). Provider config-level cpus maps onto this.\").optional() }).strict().describe(\"Resource caps per command.\").optional(), \"env\": z.object({ \"auth\": z.any().optional(), \"passthrough\": z.array(z.string()).describe(\"Static host env-var names to forward into the sandbox.\").default([] as never), \"autoPassthrough\": z.boolean().describe(\"Opt-in: forward the spawn's resolved billing-credential env-var NAME (e.g. ANTHROPIC_API_KEY) into passthrough so a fresh box inherits host auth without the caller naming vars. Only the name is injected — the value travels via the normal passthrough mechanism.\").optional() }).strict().optional(), \"network\": z.object({ \"egress\": z.array(z.string()).describe(\"Hostnames the sandbox MAY reach. Empty / missing = no egress.\").default([] as never), \"egressIp\": z.string().regex(new RegExp(\"^(shared|auto|dedicated|[a-z0-9]{1,8})$\")).describe(\"Public source address for outbound traffic (AIP-36 v1.3): shared | auto | dedicated | pool slot id. A provider without an address pool MUST refuse any value other than shared with sandbox_egress_pool_unavailable.\").default(\"shared\") }).strict().optional(), \"join\": z.object({ \"tokenEnv\": z.string().min(1).describe(\"Name of a host env var whose value (a join-token URL) should be forwarded into the sandbox under the SAME name at boot, so the box can auto-register with the daemon that minted the token. Equivalent to adding that name to env.passthrough — provided as a distinct, self-documenting field because its purpose (auto-join) is different from an arbitrary forwarded secret. Absent/no-op when the named host env var isn't actually set (e.g. a fork's CI run with no AGENTPROTO_JOIN secret) — never fails the boot.\") }).strict().describe(\"Auto-join wiring: forwards a host env var (holding a join-token URL) into the sandbox by the same name, so the box can register itself with the daemon that minted the token.\").optional(), \"mounts\": z.array(z.any()).describe(\"Filesystems mounted inside the sandbox at declared paths. Maps to Mastra Workspace.mounts.\").default([] as never), \"identity\": z.any().describe(\"AIP-23 identity-ref — owner of the sandbox processes.\").optional(), \"policy\": z.any().describe(\"AIP-38 POLICY block — access grants on sandbox actions.\").optional(), \"lifecycle\": z.object({ \"pause_after_idle\": z.string().min(1).describe(\"AIP-37 event name (e.g. `idle-600` for 10 min). Provider-supported only (modal, daytona).\").optional(), \"destroy_on\": z.string().min(1).describe(\"AIP-37 event name (e.g. `workspace-close`).\").optional() }).strict().optional(), \"read_only\": z.boolean().describe(\"Reject command execution at the sandbox layer. Read-only sandbox calls fail with `sandbox_read_only`.\").default(false), \"extraPorts\": z.array(z.number().int().gte(1)).describe(\"App ports to expose at boot time. Resolved into BootedSandbox.ports (port → public URL) by providers that support port exposure.\").optional(), \"metadata\": z.record(z.string(), z.any()).describe(\"Free-form, namespaced. Adapter hints under `metadata.<adapter>.*`.\").optional() }).strict().describe(\"Validates the YAML frontmatter portion of an AIP-36 SANDBOX.md manifest, OR the inline form embedded in any other manifest's `sandbox:` block. Compute-only — durable filesystem backings live in AIP-35 STORAGE.md.\")\n\nexport type SandboxFrontmatter = z.infer<typeof sandboxFrontmatterSchema>\n","import { createDoctype } from \"@agentproto/define-doctype\"\nimport { sandboxFrontmatterSchema } from \"./schema.js\"\nimport type {\n SandboxRuntimeHandle,\n SandboxRuntimeInput,\n} from \"./types.js\"\n\n/**\n * AIP-36 reference implementation of `defineSandbox`.\n *\n * Built on `createDoctype` so the cross-AIP invariants (id pattern,\n * description length, top-level freeze, \"defineSandbox (AIP-36): …\"\n * error prefix) run uniformly with every other AIP defineX.\n *\n * Field-level validation runs the schema-derived zod from\n * `./schema.ts` against the manifest portion of the input. The\n * AIP-43 runtime slots (`factory`, `capabilities`) are HOST-OPAQUE\n * TS-runtime metadata stripped before validation and re-attached in\n * `build` — same pattern as `@agentproto/storage`'s `defineStorage`.\n *\n * Generic params:\n * TFactory — host-typed factory (e.g. `(input) => WorkspaceSandbox`\n * for the Guilde host). Defaults to `unknown`.\n * TCapabilities — opaque metadata the registry queries on. Per\n * AIP-43 § Capability metadata namespace.\n */\nconst defineSandboxInner = createDoctype<\n SandboxRuntimeInput,\n SandboxRuntimeHandle\n>({\n aip: 36,\n name: \"sandbox\",\n readDescription: false,\n // AIP-36 makes `id` standalone-only — inline sandbox blocks fall\n // back to `provider` for the cross-AIP id-pattern check (mirrors\n // AIP-43 § Identity).\n readIdentity: def => {\n if (typeof def.id === \"string\" && def.id.length > 0) return def.id\n return def.provider\n },\n validate(def) {\n const { factory: _factory, capabilities: _capabilities, ...manifest } = def\n const result = sandboxFrontmatterSchema.safeParse(manifest)\n if (!result.success) {\n throw new Error(\n `defineSandbox (AIP-36): ${result.error.issues\n .map(i => `${i.path.join(\".\")}: ${i.message}`)\n .join(\"; \")}`,\n )\n }\n // TODO: spec-36-specific cross-field rules (if/then/allOf in\n // the JSON Schema) — those don't translate to zod cleanly and\n // belong here. See @agentproto/operator's autonomy=gated rule.\n },\n build(def) {\n const { factory, capabilities, ...manifest } = def\n return {\n ...manifest,\n ...(factory !== undefined ? { factory } : {}),\n ...(capabilities !== undefined\n ? { capabilities: Object.freeze({ ...capabilities }) }\n : {}),\n } as SandboxRuntimeHandle\n },\n})\n\n/**\n * Type-aware wrapper preserving `TFactory` / `TCapabilities` generics\n * across the call.\n */\nexport function defineSandbox<\n TFactory = unknown,\n TCapabilities extends Record<string, unknown> = Record<string, unknown>,\n>(\n definition: SandboxRuntimeInput<TFactory, TCapabilities>,\n): SandboxRuntimeHandle<TFactory, TCapabilities> {\n return defineSandboxInner(\n definition as SandboxRuntimeInput,\n ) as SandboxRuntimeHandle<TFactory, TCapabilities>\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SandboxRuntimeInput, a as SandboxRuntimeHandle, b as SandboxHandle } from './schema-Bq1O5CzW.js';
2
- export { c as SandboxDefinition, s as SandboxSpecSchema } from './schema-Bq1O5CzW.js';
1
+ import { S as SandboxRuntimeInput, a as SandboxRuntimeHandle, b as SandboxHandle } from './schema-xoaf7_lQ.js';
2
+ export { c as SandboxDefinition, s as SandboxSpecSchema } from './schema-xoaf7_lQ.js';
3
3
  import { SecretResolver } from '@agentproto/secrets/exposure';
4
4
  import { DaemonAgentSessionHost } from '@agentproto/worktree';
5
5
  import 'zod';
@@ -60,6 +60,15 @@ interface BootedSandbox {
60
60
  mcpUrl: string;
61
61
  /** Provider-assigned sandbox id, for logging / lookup. */
62
62
  sandboxId: string;
63
+ /** The egress address the provider brokered for this box, when it supports
64
+ * selecting one (AIP-36 `network.egressIp` / provider config `egressIp`).
65
+ * `null` means the controller explicitly reports no dedicated address
66
+ * (for example `"shared"`); absent means the provider/controller has no
67
+ * egress-IP concept. Optional so existing providers are unaffected. */
68
+ egressIp?: {
69
+ id: string;
70
+ publicIp: string;
71
+ } | null;
63
72
  /** Opaque secret gating `mcpUrl`. Present either when `opts.expose ===
64
73
  * "private"` was honoured (see `SandboxBootOpts.expose`), OR when the
65
74
  * provider's own daemon is gated unconditionally regardless of `expose`
@@ -149,6 +158,16 @@ interface BootedSandbox {
149
158
  * that can't pause (or don't support reconnect at all) omit it; callers
150
159
  * that want to pause fall back to `stop()` when it's absent. */
151
160
  pause?(): Promise<void>;
161
+ /**
162
+ * Release THIS session's provider-side claim on the box WITHOUT stopping
163
+ * or pausing it — the shared-box survivor path. A session that leaves a box
164
+ * other sessions still drive must still give back the slot it took at
165
+ * attach (e.g. the computer controller's holder refcount); `stop()` would
166
+ * kill the box under the survivors and `pause()` would suspend it.
167
+ * Optional: providers with no per-session attach bookkeeping omit it.
168
+ * Idempotent: releasing an already-released slot is a no-op.
169
+ */
170
+ detach?(): Promise<void>;
152
171
  /**
153
172
  * Re-arm the box's remaining lifetime to `ms` from now — the same
154
173
  * mechanism a provider's own `connect()` already uses to re-arm a resumed
@@ -249,6 +268,25 @@ interface SandboxProbeResult {
249
268
  /** Provider-reported box state (e.g. "running", "paused"), when available. */
250
269
  state?: string;
251
270
  }
271
+ /**
272
+ * One box as the PROVIDER's control plane reports it - the shape
273
+ * `SandboxProvider.listBoxes` returns. It is deliberately thin: the gc only
274
+ * needs the identity, the state, how many session slots the provider still
275
+ * counts for the box, and (when the provider knows it) the memory the box
276
+ * holds and when it was created.
277
+ */
278
+ interface SandboxProviderBox {
279
+ /** Provider-assigned box id - the same id `boot()` returned. */
280
+ id: string;
281
+ /** Provider-reported state ("running", "stopped", …), when available. */
282
+ state?: string;
283
+ /** Session slots the provider still counts for this box. */
284
+ sessions?: number;
285
+ /** Memory the box holds, in MiB - when the provider reports it. */
286
+ memoryMiB?: number;
287
+ /** Creation timestamp (ISO-8601) - when the provider reports it. */
288
+ createdAt?: string;
289
+ }
252
290
  /**
253
291
  * Backend-agnostic sandbox lifecycle. Concrete implementations (e2b, modal,
254
292
  * daytona, blaxel, …) live in their own packages so this one stays free of
@@ -262,6 +300,16 @@ interface SandboxProvider {
262
300
  * which tears down its temp workspace on `stop()`) omit it; the runtime
263
301
  * errors clearly when reuse is requested against such a provider. */
264
302
  connect?(sandboxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>;
303
+ /**
304
+ * Open an existing box for a ONE-SHOT command (`sandbox_exec`) without
305
+ * joining a session. Optional: providers that can expose `exec` on an
306
+ * already-running box without taking a session slot (and without probing,
307
+ * restarting or re-tokening its daemon) implement it; `sandbox_exec` uses
308
+ * it when present and falls back to `connect()` otherwise. The returned
309
+ * handle is not an ownership claim: its `stop()` must never detach a
310
+ * session or tear down the box.
311
+ */
312
+ connectForExec?(sandboxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>;
265
313
  /**
266
314
  * Liveness probe against the PROVIDER (not the box's daemon): answers
267
315
  * whether the sandbox id still exists at all — the signal a box death
@@ -273,6 +321,38 @@ interface SandboxProvider {
273
321
  * it; a caller reading `sandboxAlive` then reports "unknown".
274
322
  */
275
323
  probe?(sandboxId: string): Promise<SandboxProbeResult>;
324
+ /**
325
+ * Destroy the box on the provider control plane WITHOUT connecting to it.
326
+ * The teardown/gc counterpart of `connect()` + `stop()`: connecting to an
327
+ * existing box implies RESUMING a suspended one first, on providers whose
328
+ * resume allocates RAM/CPU (the `computer` controller) — capacity a
329
+ * crowded host does not have, and exactly what made `sandbox rm --box`
330
+ * fail with "host capacity exhausted" even though deleting a box frees
331
+ * resources. `remove` needs no capacity: a suspended box is deleted as-is
332
+ * (its RAM snapshot is freed), releasing MORE than it ever costs.
333
+ * Must treat "the id is already gone" as SUCCESS (idempotent) so gc passes
334
+ * converge; only a provider-side rejection propagates.
335
+ * SHARED-BOX SAFETY (AIP-36): by DEFAULT `remove` must NOT force the
336
+ * destroy past a controller that still counts attached sessions — a
337
+ * shared box under live sessions must not be destroyed just because one
338
+ * caller asked. Providers that accept `opts.force` treat `409` asked
339
+ * without force as a soft "still in use" (non-fatal), and the ONLY
340
+ * caller that may force the gc path, after its attaché candidate check.
341
+ * Optional: providers that cannot destroy without a live control-plane
342
+ * route omit it; callers fall back to `connect()` + `stop()`.
343
+ */
344
+ remove?(sandboxId: string, opts?: {
345
+ force?: boolean;
346
+ }): Promise<void>;
347
+ /**
348
+ * List the boxes this provider's control plane still holds - the surface
349
+ * the orphan-box gc reads. A box the provider still lists (and still
350
+ * charges RAM for) that no live agentproto session references is an
351
+ * ORPHAN: the provider keeps it (and its memory) while nobody can use it.
352
+ * Optional: providers whose control plane cannot enumerate their boxes
353
+ * omit it, and the gc simply has no candidate for that provider.
354
+ */
355
+ listBoxes?(): Promise<SandboxProviderBox[]>;
276
356
  }
277
357
  /** Which secrets to resolve into the sandbox's env, and how. */
278
358
  interface SandboxSecretsConfig {
@@ -304,6 +384,11 @@ type SandboxAgentSessionHost = DaemonAgentSessionHost & {
304
384
  * `BootedSandbox.ports` so the runtime can record them on the session
305
385
  * descriptor without reaching into the booted handle after the fact. */
306
386
  ports?: Record<number, string>;
387
+ /** The egress address brokered for the box — forwarded from
388
+ * `BootedSandbox.egressIp` so a caller can record it without reaching
389
+ * into the booted handle after the fact. Absent when the provider has no
390
+ * egress-IP concept. */
391
+ egressIp?: BootedSandbox["egressIp"];
307
392
  /** The paired host's resolved fingerprint — forwarded from
308
393
  * `BootedSandbox.device`, present only for the `device` provider
309
394
  * (BOOTSTRAP P7a's `hostFingerprint` source). */
@@ -331,6 +416,10 @@ type SandboxAgentSessionHost = DaemonAgentSessionHost & {
331
416
  /** Close the daemon connection and PAUSE the sandbox instead of killing
332
417
  * it — only present when the booted sandbox supports `pause()`. */
333
418
  pause?(): Promise<void>;
419
+ /** Close the daemon connection and release THIS session's provider-side
420
+ * claim on the box, leaving the box itself running for other sessions —
421
+ * only present when the booted sandbox supports `detach()`. */
422
+ detach?(): Promise<void>;
334
423
  };
335
424
  /**
336
425
  * Resolve `secrets` into an env map, boot (or, when `opts.sandboxId` is set,
@@ -422,4 +511,4 @@ declare function resolveLifecyclePolicy(spec: SandboxHandle, reuse: boolean): Sa
422
511
  declare const SPEC_NAME: "agentsandbox/v1";
423
512
  declare const SPEC_VERSION: "1.0.0-alpha";
424
513
 
425
- export { type BootedSandbox, type CreateSandboxAgentSessionHostOpts, EXEC_ERROR_COMMAND_MAX_CHARS, SPEC_NAME, SPEC_VERSION, type SandboxAgentSessionHost, type SandboxBootOpts, SandboxBoxGoneError, type SandboxExecOpts, type SandboxExecResult, SandboxHandle, SandboxHostBootFailedError, type SandboxLifecyclePolicy, SandboxPortExposureUnsupportedError, type SandboxProbeResult, type SandboxProvider, SandboxQuotaError, SandboxRuntimeHandle, SandboxRuntimeInput, type SandboxSecretsConfig, type SandboxSpec, createSandboxAgentSessionHost, defineSandbox, exposePort, isSandboxBoxGoneError, isSandboxQuotaError, resolveLifecyclePolicy, scrubCommandFromMessage, shortenCommand };
514
+ export { type BootedSandbox, type CreateSandboxAgentSessionHostOpts, EXEC_ERROR_COMMAND_MAX_CHARS, SPEC_NAME, SPEC_VERSION, type SandboxAgentSessionHost, type SandboxBootOpts, SandboxBoxGoneError, type SandboxExecOpts, type SandboxExecResult, SandboxHandle, SandboxHostBootFailedError, type SandboxLifecyclePolicy, SandboxPortExposureUnsupportedError, type SandboxProbeResult, type SandboxProvider, type SandboxProviderBox, SandboxQuotaError, SandboxRuntimeHandle, SandboxRuntimeInput, type SandboxSecretsConfig, type SandboxSpec, createSandboxAgentSessionHost, defineSandbox, exposePort, isSandboxBoxGoneError, isSandboxQuotaError, resolveLifecyclePolicy, scrubCommandFromMessage, shortenCommand };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { sandboxFrontmatterSchema as SandboxSpecSchema, defineSandbox } from './chunk-JEUFE7GR.mjs';
1
+ export { sandboxFrontmatterSchema as SandboxSpecSchema, defineSandbox } from './chunk-DT5F2UYM.mjs';
2
2
  import { assertSafeSecretValue } from '@agentproto/secrets/exposure';
3
3
  import { connectDaemonAgentSessionHost } from '@agentproto/worktree';
4
4
 
@@ -84,6 +84,7 @@ async function createSandboxAgentSessionHost(opts) {
84
84
  sandboxId: booted.sandboxId,
85
85
  ...headers ? { authHeaders: headers } : {},
86
86
  ...booted.ports ? { ports: booted.ports } : {},
87
+ ...booted.egressIp !== void 0 ? { egressIp: booted.egressIp } : {},
87
88
  ...booted.expose ? { expose: booted.expose.bind(booted) } : {},
88
89
  ...booted.exec ? { exec: booted.exec.bind(booted) } : {},
89
90
  ...booted.readFile ? { readFile: booted.readFile.bind(booted) } : {},
@@ -99,6 +100,12 @@ async function createSandboxAgentSessionHost(opts) {
99
100
  await host.close();
100
101
  await booted.pause();
101
102
  }
103
+ } : {},
104
+ ...booted.detach ? {
105
+ async detach() {
106
+ await host.close();
107
+ await booted.detach();
108
+ }
102
109
  } : {}
103
110
  };
104
111
  }
@@ -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\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 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 * 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\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 /** 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 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}\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 // 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 { sandboxId: booted.sandboxId, cleanedUp },\n )\n }\n return {\n ...host,\n mcpUrl: booted.mcpUrl,\n sandboxId: booted.sandboxId,\n ...(headers ? { authHeaders: headers } : {}),\n ...(booted.ports ? { ports: booted.ports } : {}),\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 }\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 = \"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 SandboxExecOpts,\n type SandboxExecResult,\n type SandboxProvider,\n type SandboxProbeResult,\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\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 /** 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\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 * 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\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 /** 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, { env })\n } else {\n booted = await opts.provider.boot(opts.spec, { env })\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 { sandboxId: booted.sandboxId, cleanedUp },\n )\n }\n return {\n ...host,\n mcpUrl: booted.mcpUrl,\n sandboxId: booted.sandboxId,\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 = \"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 SandboxExecOpts,\n type SandboxExecResult,\n type SandboxProvider,\n type SandboxProbeResult,\n type SandboxProviderBox,\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,5 +1,5 @@
1
- import { d as SandboxFrontmatter, b as SandboxHandle } from '../schema-Bq1O5CzW.js';
2
- export { s as sandboxFrontmatterSchema } from '../schema-Bq1O5CzW.js';
1
+ import { d as SandboxFrontmatter, b as SandboxHandle } from '../schema-xoaf7_lQ.js';
2
+ export { s as sandboxFrontmatterSchema } from '../schema-xoaf7_lQ.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { sandboxFrontmatterSchema, defineSandbox } from '../chunk-JEUFE7GR.mjs';
2
- export { sandboxFrontmatterSchema } from '../chunk-JEUFE7GR.mjs';
1
+ import { sandboxFrontmatterSchema, defineSandbox } from '../chunk-DT5F2UYM.mjs';
2
+ export { sandboxFrontmatterSchema } from '../chunk-DT5F2UYM.mjs';
3
3
  import matter from 'gray-matter';
4
4
 
5
5
  /**
@@ -65,6 +65,10 @@ interface SandboxDefinition {
65
65
  timeout_ms?: number;
66
66
  memory_mb?: number;
67
67
  cpu_ms?: number;
68
+ /**
69
+ * vCPUs, may be fractional (AIP-36 v1.3). Provider config-level cpus maps onto this.
70
+ */
71
+ cpus?: number;
68
72
  };
69
73
  env?: {
70
74
  auth?: AuthBlock;
@@ -89,6 +93,10 @@ interface SandboxDefinition {
89
93
  * Hostnames the sandbox MAY reach. Empty / missing = no egress.
90
94
  */
91
95
  egress?: string[];
96
+ /**
97
+ * Public source address for outbound traffic (AIP-36 v1.3): shared | auto | dedicated | pool slot id. A provider without an address pool MUST refuse any value other than shared with sandbox_egress_pool_unavailable.
98
+ */
99
+ egressIp?: string;
92
100
  };
93
101
  /**
94
102
  * Auto-join wiring: forwards a host env var (holding a join-token URL) into the sandbox by the same name, so the box can register itself with the daemon that minted the token.
@@ -360,6 +368,7 @@ declare const sandboxFrontmatterSchema: z.ZodObject<{
360
368
  timeout_ms: z.ZodOptional<z.ZodNumber>;
361
369
  memory_mb: z.ZodOptional<z.ZodNumber>;
362
370
  cpu_ms: z.ZodOptional<z.ZodNumber>;
371
+ cpus: z.ZodOptional<z.ZodNumber>;
363
372
  }, z.core.$strict>>;
364
373
  env: z.ZodOptional<z.ZodObject<{
365
374
  auth: z.ZodOptional<z.ZodAny>;
@@ -368,6 +377,7 @@ declare const sandboxFrontmatterSchema: z.ZodObject<{
368
377
  }, z.core.$strict>>;
369
378
  network: z.ZodOptional<z.ZodObject<{
370
379
  egress: z.ZodDefault<z.ZodArray<z.ZodString>>;
380
+ egressIp: z.ZodDefault<z.ZodString>;
371
381
  }, z.core.$strict>>;
372
382
  join: z.ZodOptional<z.ZodObject<{
373
383
  tokenEnv: z.ZodString;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/sandbox",
3
- "version": "0.12.1",
3
+ "version": "0.13.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/define-doctype": "0.1.4",
52
52
  "@agentproto/secrets": "2.1.1",
53
- "@agentproto/worktree": "0.17.0",
54
- "@agentproto/workflow-runtime": "0.19.1"
53
+ "@agentproto/workflow-runtime": "0.20.1",
54
+ "@agentproto/worktree": "0.19.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.9.9",
@@ -1,47 +0,0 @@
1
- import { z } from 'zod';
2
- import { createDoctype } from '@agentproto/define-doctype';
3
-
4
- /**
5
- * @agentproto/sandbox v0.1.0-alpha
6
- * AIP-36 SANDBOX.md `defineSandbox` reference implementation.
7
- */
8
-
9
- var sandboxFrontmatterSchema = z.object({ "schema": z.literal("sandbox/v1").describe("Standalone-only. Identifies the doctype + version. Absent when the block is inlined.").optional(), "id": z.string().regex(new RegExp("^@[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$")).describe("Standalone-only. Globally addressable id `@<owner-slug>/<sandbox-slug>`.").optional(), "version": z.string().regex(new RegExp("^\\d+\\.\\d+\\.\\d+(?:[-+][\\w.\\-]+)?$")).describe("Standalone-only. Spec version of THIS file.").optional(), "provider": z.string().min(1).describe("Backend kind. Day-1 enumerated set: local | mastra-e2b | mastra-modal | mastra-daytona | mastra-blaxel | node-permission. Hosts MAY register additional ids."), "config": z.object({ "installAdapters": z.array(z.string().min(1)).describe(`Semantic harness slugs to pre-install in the box at boot (e.g. ["hermes", "claude-code"]). Each expands to @agentproto/adapter-<slug>@latest plus that adapter's declared boot extras; a config.installPackages pin for the same package always wins. Additive \u2014 absent means no extra install.`).optional() }).catchall(z.any()).describe("Provider-specific connection fields. Shape varies per provider (see AIP-36 \xA7Provider config shapes)."), "limits": z.object({ "timeout_ms": z.number().int().gte(1).optional(), "memory_mb": z.number().int().gte(1).optional(), "cpu_ms": z.number().int().gte(1).optional() }).strict().describe("Resource caps per command.").optional(), "env": z.object({ "auth": z.any().optional(), "passthrough": z.array(z.string()).describe("Static host env-var names to forward into the sandbox.").default([]), "autoPassthrough": z.boolean().describe("Opt-in: forward the spawn's resolved billing-credential env-var NAME (e.g. ANTHROPIC_API_KEY) into passthrough so a fresh box inherits host auth without the caller naming vars. Only the name is injected \u2014 the value travels via the normal passthrough mechanism.").optional() }).strict().optional(), "network": z.object({ "egress": z.array(z.string()).describe("Hostnames the sandbox MAY reach. Empty / missing = no egress.").default([]) }).strict().optional(), "join": z.object({ "tokenEnv": z.string().min(1).describe("Name of a host env var whose value (a join-token URL) should be forwarded into the sandbox under the SAME name at boot, so the box can auto-register with the daemon that minted the token. Equivalent to adding that name to env.passthrough \u2014 provided as a distinct, self-documenting field because its purpose (auto-join) is different from an arbitrary forwarded secret. Absent/no-op when the named host env var isn't actually set (e.g. a fork's CI run with no AGENTPROTO_JOIN secret) \u2014 never fails the boot.") }).strict().describe("Auto-join wiring: forwards a host env var (holding a join-token URL) into the sandbox by the same name, so the box can register itself with the daemon that minted the token.").optional(), "mounts": z.array(z.any()).describe("Filesystems mounted inside the sandbox at declared paths. Maps to Mastra Workspace.mounts.").default([]), "identity": z.any().describe("AIP-23 identity-ref \u2014 owner of the sandbox processes.").optional(), "policy": z.any().describe("AIP-38 POLICY block \u2014 access grants on sandbox actions.").optional(), "lifecycle": z.object({ "pause_after_idle": z.string().min(1).describe("AIP-37 event name (e.g. `idle-600` for 10 min). Provider-supported only (modal, daytona).").optional(), "destroy_on": z.string().min(1).describe("AIP-37 event name (e.g. `workspace-close`).").optional() }).strict().optional(), "read_only": z.boolean().describe("Reject command execution at the sandbox layer. Read-only sandbox calls fail with `sandbox_read_only`.").default(false), "extraPorts": z.array(z.number().int().gte(1)).describe("App ports to expose at boot time. Resolved into BootedSandbox.ports (port \u2192 public URL) by providers that support port exposure.").optional(), "metadata": z.record(z.string(), z.any()).describe("Free-form, namespaced. Adapter hints under `metadata.<adapter>.*`.").optional() }).strict().describe("Validates the YAML frontmatter portion of an AIP-36 SANDBOX.md manifest, OR the inline form embedded in any other manifest's `sandbox:` block. Compute-only \u2014 durable filesystem backings live in AIP-35 STORAGE.md.");
10
- var defineSandboxInner = createDoctype({
11
- aip: 36,
12
- name: "sandbox",
13
- readDescription: false,
14
- // AIP-36 makes `id` standalone-only — inline sandbox blocks fall
15
- // back to `provider` for the cross-AIP id-pattern check (mirrors
16
- // AIP-43 § Identity).
17
- readIdentity: (def) => {
18
- if (typeof def.id === "string" && def.id.length > 0) return def.id;
19
- return def.provider;
20
- },
21
- validate(def) {
22
- const { factory: _factory, capabilities: _capabilities, ...manifest } = def;
23
- const result = sandboxFrontmatterSchema.safeParse(manifest);
24
- if (!result.success) {
25
- throw new Error(
26
- `defineSandbox (AIP-36): ${result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}`
27
- );
28
- }
29
- },
30
- build(def) {
31
- const { factory, capabilities, ...manifest } = def;
32
- return {
33
- ...manifest,
34
- ...factory !== void 0 ? { factory } : {},
35
- ...capabilities !== void 0 ? { capabilities: Object.freeze({ ...capabilities }) } : {}
36
- };
37
- }
38
- });
39
- function defineSandbox(definition) {
40
- return defineSandboxInner(
41
- definition
42
- );
43
- }
44
-
45
- export { defineSandbox, sandboxFrontmatterSchema };
46
- //# sourceMappingURL=chunk-JEUFE7GR.mjs.map
47
- //# sourceMappingURL=chunk-JEUFE7GR.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/schema.ts","../src/define-sandbox.ts"],"names":[],"mappings":"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","file":"chunk-JEUFE7GR.mjs","sourcesContent":["/**\n * AIP-36 SANDBOX.md frontmatter zod schema.\n *\n * Generated from `resources/aip-36/draft/SANDBOX.schema.json` via\n * json-schema-to-zod. Imported by both `define-sandbox.ts` (TS path\n * validation) and `manifest/index.ts` (.md path validation) so every\n * field-level constraint runs in both authoring paths from a single\n * source of truth — re-run scaffold-aip to refresh after spec changes.\n *\n * Cross-field rules (if/then/allOf in JSON Schema) don't translate\n * cleanly and live in `define-sandbox.ts`'s `validate(def)` instead.\n */\n\nimport { z } from \"zod\"\n\nexport const sandboxFrontmatterSchema = z.object({ \"schema\": z.literal(\"sandbox/v1\").describe(\"Standalone-only. Identifies the doctype + version. Absent when the block is inlined.\").optional(), \"id\": z.string().regex(new RegExp(\"^@[a-z0-9][a-z0-9-]*/[a-z0-9][a-z0-9-]*$\")).describe(\"Standalone-only. Globally addressable id `@<owner-slug>/<sandbox-slug>`.\").optional(), \"version\": z.string().regex(new RegExp(\"^\\\\d+\\\\.\\\\d+\\\\.\\\\d+(?:[-+][\\\\w.\\\\-]+)?$\")).describe(\"Standalone-only. Spec version of THIS file.\").optional(), \"provider\": z.string().min(1).describe(\"Backend kind. Day-1 enumerated set: local | mastra-e2b | mastra-modal | mastra-daytona | mastra-blaxel | node-permission. Hosts MAY register additional ids.\"), \"config\": z.object({ \"installAdapters\": z.array(z.string().min(1)).describe(\"Semantic harness slugs to pre-install in the box at boot (e.g. [\\\"hermes\\\", \\\"claude-code\\\"]). Each expands to @agentproto/adapter-<slug>@latest plus that adapter's declared boot extras; a config.installPackages pin for the same package always wins. Additive — absent means no extra install.\").optional() }).catchall(z.any()).describe(\"Provider-specific connection fields. Shape varies per provider (see AIP-36 §Provider config shapes).\"), \"limits\": z.object({ \"timeout_ms\": z.number().int().gte(1).optional(), \"memory_mb\": z.number().int().gte(1).optional(), \"cpu_ms\": z.number().int().gte(1).optional() }).strict().describe(\"Resource caps per command.\").optional(), \"env\": z.object({ \"auth\": z.any().optional(), \"passthrough\": z.array(z.string()).describe(\"Static host env-var names to forward into the sandbox.\").default([] as never), \"autoPassthrough\": z.boolean().describe(\"Opt-in: forward the spawn's resolved billing-credential env-var NAME (e.g. ANTHROPIC_API_KEY) into passthrough so a fresh box inherits host auth without the caller naming vars. Only the name is injected — the value travels via the normal passthrough mechanism.\").optional() }).strict().optional(), \"network\": z.object({ \"egress\": z.array(z.string()).describe(\"Hostnames the sandbox MAY reach. Empty / missing = no egress.\").default([] as never) }).strict().optional(), \"join\": z.object({ \"tokenEnv\": z.string().min(1).describe(\"Name of a host env var whose value (a join-token URL) should be forwarded into the sandbox under the SAME name at boot, so the box can auto-register with the daemon that minted the token. Equivalent to adding that name to env.passthrough — provided as a distinct, self-documenting field because its purpose (auto-join) is different from an arbitrary forwarded secret. Absent/no-op when the named host env var isn't actually set (e.g. a fork's CI run with no AGENTPROTO_JOIN secret) — never fails the boot.\") }).strict().describe(\"Auto-join wiring: forwards a host env var (holding a join-token URL) into the sandbox by the same name, so the box can register itself with the daemon that minted the token.\").optional(), \"mounts\": z.array(z.any()).describe(\"Filesystems mounted inside the sandbox at declared paths. Maps to Mastra Workspace.mounts.\").default([] as never), \"identity\": z.any().describe(\"AIP-23 identity-ref — owner of the sandbox processes.\").optional(), \"policy\": z.any().describe(\"AIP-38 POLICY block — access grants on sandbox actions.\").optional(), \"lifecycle\": z.object({ \"pause_after_idle\": z.string().min(1).describe(\"AIP-37 event name (e.g. `idle-600` for 10 min). Provider-supported only (modal, daytona).\").optional(), \"destroy_on\": z.string().min(1).describe(\"AIP-37 event name (e.g. `workspace-close`).\").optional() }).strict().optional(), \"read_only\": z.boolean().describe(\"Reject command execution at the sandbox layer. Read-only sandbox calls fail with `sandbox_read_only`.\").default(false), \"extraPorts\": z.array(z.number().int().gte(1)).describe(\"App ports to expose at boot time. Resolved into BootedSandbox.ports (port → public URL) by providers that support port exposure.\").optional(), \"metadata\": z.record(z.string(), z.any()).describe(\"Free-form, namespaced. Adapter hints under `metadata.<adapter>.*`.\").optional() }).strict().describe(\"Validates the YAML frontmatter portion of an AIP-36 SANDBOX.md manifest, OR the inline form embedded in any other manifest's `sandbox:` block. Compute-only — durable filesystem backings live in AIP-35 STORAGE.md.\")\n\nexport type SandboxFrontmatter = z.infer<typeof sandboxFrontmatterSchema>\n","import { createDoctype } from \"@agentproto/define-doctype\"\nimport { sandboxFrontmatterSchema } from \"./schema.js\"\nimport type {\n SandboxRuntimeHandle,\n SandboxRuntimeInput,\n} from \"./types.js\"\n\n/**\n * AIP-36 reference implementation of `defineSandbox`.\n *\n * Built on `createDoctype` so the cross-AIP invariants (id pattern,\n * description length, top-level freeze, \"defineSandbox (AIP-36): …\"\n * error prefix) run uniformly with every other AIP defineX.\n *\n * Field-level validation runs the schema-derived zod from\n * `./schema.ts` against the manifest portion of the input. The\n * AIP-43 runtime slots (`factory`, `capabilities`) are HOST-OPAQUE\n * TS-runtime metadata stripped before validation and re-attached in\n * `build` — same pattern as `@agentproto/storage`'s `defineStorage`.\n *\n * Generic params:\n * TFactory — host-typed factory (e.g. `(input) => WorkspaceSandbox`\n * for the Guilde host). Defaults to `unknown`.\n * TCapabilities — opaque metadata the registry queries on. Per\n * AIP-43 § Capability metadata namespace.\n */\nconst defineSandboxInner = createDoctype<\n SandboxRuntimeInput,\n SandboxRuntimeHandle\n>({\n aip: 36,\n name: \"sandbox\",\n readDescription: false,\n // AIP-36 makes `id` standalone-only — inline sandbox blocks fall\n // back to `provider` for the cross-AIP id-pattern check (mirrors\n // AIP-43 § Identity).\n readIdentity: def => {\n if (typeof def.id === \"string\" && def.id.length > 0) return def.id\n return def.provider\n },\n validate(def) {\n const { factory: _factory, capabilities: _capabilities, ...manifest } = def\n const result = sandboxFrontmatterSchema.safeParse(manifest)\n if (!result.success) {\n throw new Error(\n `defineSandbox (AIP-36): ${result.error.issues\n .map(i => `${i.path.join(\".\")}: ${i.message}`)\n .join(\"; \")}`,\n )\n }\n // TODO: spec-36-specific cross-field rules (if/then/allOf in\n // the JSON Schema) — those don't translate to zod cleanly and\n // belong here. See @agentproto/operator's autonomy=gated rule.\n },\n build(def) {\n const { factory, capabilities, ...manifest } = def\n return {\n ...manifest,\n ...(factory !== undefined ? { factory } : {}),\n ...(capabilities !== undefined\n ? { capabilities: Object.freeze({ ...capabilities }) }\n : {}),\n } as SandboxRuntimeHandle\n },\n})\n\n/**\n * Type-aware wrapper preserving `TFactory` / `TCapabilities` generics\n * across the call.\n */\nexport function defineSandbox<\n TFactory = unknown,\n TCapabilities extends Record<string, unknown> = Record<string, unknown>,\n>(\n definition: SandboxRuntimeInput<TFactory, TCapabilities>,\n): SandboxRuntimeHandle<TFactory, TCapabilities> {\n return defineSandboxInner(\n definition as SandboxRuntimeInput,\n ) as SandboxRuntimeHandle<TFactory, TCapabilities>\n}\n"]}