@agentproto/sandbox 0.2.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,6 +44,45 @@ try {
44
44
  }
45
45
  ```
46
46
 
47
+ ## Adapter auto-install on boot
48
+
49
+ A box's boot-time `npm i -g @agentproto/cli` replaces the global install and
50
+ loses the template-baked `@agentproto/adapter-*` packages. Providers that
51
+ accept `config.installPackages` (e2b, box) install those entries in the SAME
52
+ npm invocation as the CLI update; when spawning through the runtime
53
+ (`spawnAgentSession`), the adapter about to be launched — plus
54
+ `@anthropic-ai/claude-code` for the `claude-code` adapter — is injected there
55
+ automatically (a caller-declared pin always wins, nothing is injected for
56
+ non-sandbox spawns).
57
+
58
+ For harnesses BEYOND the spawned adapter, declare semantic slugs instead of
59
+ raw npm specs: `config.installAdapters: ["hermes", "claude-code"]`. Each slug
60
+ expands to `@agentproto/adapter-<slug>@latest` (plus that adapter's declared
61
+ boot extras, e.g. `@anthropic-ai/claude-code`) and merges into
62
+ `config.installPackages` with dedupe — a caller's explicit pin for the same
63
+ package always wins. An unknown slug still expands (the box's npm install is
64
+ the authority); an absent field changes nothing.
65
+
66
+ ## Billing-auth auto-passthrough (opt-in)
67
+
68
+ Set `env.autoPassthrough: true` on a sandbox spec and the runtime adds the
69
+ spawn's RESOLVED billing-credential env-var NAME (e.g. `ANTHROPIC_API_KEY`) to
70
+ `env.passthrough` before the box boots — so a fresh box inherits host auth
71
+ without the caller naming vars. Only the NAME is injected; the value travels
72
+ via the normal passthrough mechanism (host secrets broker → box env) and is
73
+ never read by the flag. Billing credential only (no `GITHUB_TOKEN`); the
74
+ caller's explicit `env.passthrough` entries are kept and deduped. When no
75
+ credential resolved — or the host cannot resolve the var — nothing is
76
+ injected and the spawn proceeds: the flag is a convenience, not a contract.
77
+
78
+ ## Lifecycle: pause is the default teardown
79
+
80
+ Closing a session PAUSES its box by default (`pause({ keepMemory: true })`)
81
+ rather than killing it — any closed box stays reattachable via
82
+ `sandbox.reuse` / `agentproto sandbox attach`. A paused box still dies at its
83
+ own `timeoutMs` (45 min by default), so paused boxes don't accumulate
84
+ indefinitely; declare `lifecycle.destroy_on` for a hard kill on close.
85
+
47
86
  ## License
48
87
 
49
88
  MIT — see [LICENSE](./LICENSE).
@@ -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() }).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(), "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-VGEBUSSP.mjs.map
47
+ //# sourceMappingURL=chunk-VGEBUSSP.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/schema.ts","../src/define-sandbox.ts"],"names":[],"mappings":"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","file":"chunk-VGEBUSSP.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(), \"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-PGQIdcQ3.js';
2
- export { c as SandboxDefinition, s as SandboxSpecSchema } from './schema-PGQIdcQ3.js';
1
+ import { S as SandboxRuntimeInput, a as SandboxRuntimeHandle, b as SandboxHandle } from './schema-h9uR_HeR.js';
2
+ export { c as SandboxDefinition, s as SandboxSpecSchema } from './schema-h9uR_HeR.js';
3
3
  import { SecretResolver } from '@agentproto/secrets/exposure';
4
4
  import { DaemonAgentSessionHost } from '@agentproto/worktree';
5
5
  import 'zod';
@@ -26,6 +26,15 @@ declare function defineSandbox<TFactory = unknown, TCapabilities extends Record<
26
26
 
27
27
  /** AIP-36 sandbox manifest handle — provider id, config, env passthrough, limits. */
28
28
  type SandboxSpec = SandboxHandle;
29
+ /**
30
+ * Thrown when a caller requests port exposure on a `BootedSandbox` whose
31
+ * provider does not support it — i.e. the sandbox handle has no `expose()`
32
+ * method. Callers should check for `expose` before calling it, or catch
33
+ * this error and fall back gracefully.
34
+ */
35
+ declare class SandboxPortExposureUnsupportedError extends Error {
36
+ constructor(message?: string);
37
+ }
29
38
  /** What a `SandboxProvider` hands back once the box is up and reachable. */
30
39
  interface BootedSandbox {
31
40
  /** The booted agentproto daemon's MCP endpoint, reachable from this process. */
@@ -50,6 +59,26 @@ interface BootedSandbox {
50
59
  * token-only provider that omits this is treated by `buildMcpConfigSnippet`
51
60
  * as `Authorization: Bearer <token>`. */
52
61
  authHeaders?: Record<string, string>;
62
+ /**
63
+ * Expose an app port on the sandbox and return its public URL. E2B returns
64
+ * `https://<port>-<sandboxId>.e2b.app`. Loopback bind is enough inside the
65
+ * VM — the provider's edge handles the forwarding.
66
+ *
67
+ * Optional: providers that cannot expose arbitrary ports omit this method.
68
+ * Callers should check for presence before calling, or catch
69
+ * `SandboxPortExposureUnsupportedError` when using `exposePort()`.
70
+ */
71
+ expose?(port: number): Promise<{
72
+ url: string;
73
+ }>;
74
+ /**
75
+ * Ports resolved at boot time from `SandboxSpec.extraPorts` — a map of
76
+ * port number to public URL. Only present when the spec declared
77
+ * `extraPorts` AND the provider supports exposure. Callers that need a
78
+ * port URL at runtime should use `expose()` directly when this map is
79
+ * absent or doesn't include the target port.
80
+ */
81
+ ports?: Record<number, string>;
53
82
  /** Tear down the sandbox. */
54
83
  stop(): Promise<void>;
55
84
  /** Pause the sandbox instead of killing it — keeps it reconnectable via
@@ -58,6 +87,13 @@ interface BootedSandbox {
58
87
  * that want to pause fall back to `stop()` when it's absent. */
59
88
  pause?(): Promise<void>;
60
89
  }
90
+ /**
91
+ * Expose a port on a booted sandbox. Throws `SandboxPortExposureUnsupportedError`
92
+ * when the provider's sandbox handle has no `expose()` method.
93
+ */
94
+ declare function exposePort(booted: BootedSandbox, port: number): Promise<{
95
+ url: string;
96
+ }>;
61
97
  /** Env resolved from secrets, handed to `provider.boot`. */
62
98
  interface SandboxBootOpts {
63
99
  env: Record<string, string>;
@@ -112,9 +148,20 @@ interface CreateSandboxAgentSessionHostOpts {
112
148
  sandboxId?: string;
113
149
  }
114
150
  type SandboxAgentSessionHost = DaemonAgentSessionHost & {
151
+ /** The booted sandbox daemon's MCP endpoint (`BootedSandbox.mcpUrl`) —
152
+ * surfaced so a caller can drive the box's OTHER daemon tools (app_install,
153
+ * command_execute, …) the same way the session host drives agent_start. */
154
+ mcpUrl: string;
115
155
  /** Provider-assigned sandbox id (`BootedSandbox.sandboxId`) — surfaced so a
116
156
  * caller can record it (there's no local PID for a sandboxed session). */
117
157
  sandboxId: string;
158
+ /** Ports resolved at boot from `SandboxSpec.extraPorts` — forwarded from
159
+ * `BootedSandbox.ports` so the runtime can record them on the session
160
+ * descriptor without reaching into the booted handle after the fact. */
161
+ ports?: Record<number, string>;
162
+ /** Expose an app port and return its public URL — forwarded from
163
+ * `BootedSandbox.expose`. Absent when the provider doesn't support it. */
164
+ expose?: BootedSandbox["expose"];
118
165
  /** Close the daemon connection AND tear down the sandbox. */
119
166
  stop(): Promise<void>;
120
167
  /** Close the daemon connection and PAUSE the sandbox instead of killing
@@ -140,9 +187,10 @@ declare function createSandboxAgentSessionHost(opts: CreateSandboxAgentSessionHo
140
187
  */
141
188
 
142
189
  interface SandboxLifecyclePolicy {
143
- /** What session close should do to the box: kill it (ephemeral, the
144
- * default) or pause it (keeps it reconnectable via `SandboxProvider.
145
- * connect`). */
190
+ /** What session close should do to the box: pause it (keeps it
191
+ * reconnectable via `SandboxProvider.connect`) or kill it (ephemeral).
192
+ * Pause is the default: absent any explicit lifecycle declaration the
193
+ * box is paused on close, and dies at its own `timeoutMs` anyway. */
146
194
  teardown: "kill" | "pause";
147
195
  /** Idle window in milliseconds, parsed from the AIP-37 `idle-<seconds>`
148
196
  * event name. Undefined when the spec doesn't declare
@@ -150,12 +198,13 @@ interface SandboxLifecyclePolicy {
150
198
  pauseAfterIdleMs?: number;
151
199
  }
152
200
  /**
153
- * `reuse` is true when this spawn asked to reconnect to an existing
154
- * sandbox id (`agent_start.sandbox.reuse`) such a box defaults to
155
- * "pause" on close even absent an explicit `lifecycle` block, since
156
- * killing it would defeat the point of having reconnected. An explicit
157
- * `destroy_on` always wins over both `reuse` and `pause_after_idle`: the
158
- * spec is stating outright that this box must not survive session close.
201
+ * Pause is the default teardown: absent `destroy_on`, `pause_after_idle`
202
+ * AND `reuse`, a closed box is paused (`SandboxProvider.connect`-able)
203
+ * rather than killed it still dies at its own `timeoutMs`, so pausing
204
+ * never accumulates boxes indefinitely. The explicit declarations stay
205
+ * authoritative: an `destroy_on` always kills (the spec states outright
206
+ * the box must not survive session close), and `pause_after_idle` /
207
+ * `reuse` pause (which the default now agrees with).
159
208
  */
160
209
  declare function resolveLifecyclePolicy(spec: SandboxHandle, reuse: boolean): SandboxLifecyclePolicy;
161
210
 
@@ -173,4 +222,4 @@ declare function resolveLifecyclePolicy(spec: SandboxHandle, reuse: boolean): Sa
173
222
  declare const SPEC_NAME: "agentsandbox/v1";
174
223
  declare const SPEC_VERSION: "1.0.0-alpha";
175
224
 
176
- export { type BootedSandbox, type CreateSandboxAgentSessionHostOpts, SPEC_NAME, SPEC_VERSION, type SandboxAgentSessionHost, type SandboxBootOpts, SandboxHandle, type SandboxLifecyclePolicy, type SandboxProvider, SandboxRuntimeHandle, SandboxRuntimeInput, type SandboxSecretsConfig, type SandboxSpec, createSandboxAgentSessionHost, defineSandbox, resolveLifecyclePolicy };
225
+ export { type BootedSandbox, type CreateSandboxAgentSessionHostOpts, SPEC_NAME, SPEC_VERSION, type SandboxAgentSessionHost, type SandboxBootOpts, SandboxHandle, type SandboxLifecyclePolicy, SandboxPortExposureUnsupportedError, type SandboxProvider, SandboxRuntimeHandle, SandboxRuntimeInput, type SandboxSecretsConfig, type SandboxSpec, createSandboxAgentSessionHost, defineSandbox, exposePort, resolveLifecyclePolicy };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { sandboxFrontmatterSchema as SandboxSpecSchema, defineSandbox } from './chunk-VSYQ4BUI.mjs';
1
+ export { sandboxFrontmatterSchema as SandboxSpecSchema, defineSandbox } from './chunk-VGEBUSSP.mjs';
2
2
  import { assertSafeSecretValue } from '@agentproto/secrets/exposure';
3
3
  import { connectDaemonAgentSessionHost } from '@agentproto/worktree';
4
4
 
@@ -6,6 +6,20 @@ import { connectDaemonAgentSessionHost } from '@agentproto/worktree';
6
6
  * @agentproto/sandbox v0.1.0-alpha
7
7
  * AIP-36 SANDBOX.md `defineSandbox` reference implementation.
8
8
  */
9
+ var SandboxPortExposureUnsupportedError = class extends Error {
10
+ constructor(message) {
11
+ super(message ?? "This sandbox provider does not support port exposure.");
12
+ this.name = "SandboxPortExposureUnsupportedError";
13
+ }
14
+ };
15
+ async function exposePort(booted, port) {
16
+ if (!booted.expose) {
17
+ throw new SandboxPortExposureUnsupportedError(
18
+ `sandbox "${booted.sandboxId}" does not support port exposure \u2014 the provider has no expose() implementation.`
19
+ );
20
+ }
21
+ return booted.expose(port);
22
+ }
9
23
  async function createSandboxAgentSessionHost(opts) {
10
24
  const env = await resolveSandboxSecretsEnv(opts.secrets);
11
25
  let booted;
@@ -28,7 +42,10 @@ async function createSandboxAgentSessionHost(opts) {
28
42
  }
29
43
  return {
30
44
  ...host,
45
+ mcpUrl: booted.mcpUrl,
31
46
  sandboxId: booted.sandboxId,
47
+ ...booted.ports ? { ports: booted.ports } : {},
48
+ ...booted.expose ? { expose: booted.expose.bind(booted) } : {},
32
49
  async stop() {
33
50
  await host.close();
34
51
  await booted.stop();
@@ -63,7 +80,7 @@ var IDLE_EVENT_PATTERN = /^idle-(\d+)$/;
63
80
  function resolveLifecyclePolicy(spec, reuse) {
64
81
  if (spec.lifecycle?.destroy_on) return { teardown: "kill" };
65
82
  const pauseAfterIdleMs = parseIdleAfterMs(spec.lifecycle?.pause_after_idle);
66
- const teardown = reuse || pauseAfterIdleMs !== void 0 ? "pause" : "kill";
83
+ const teardown = "pause";
67
84
  return { teardown, ...pauseAfterIdleMs !== void 0 ? { pauseAfterIdleMs } : {} };
68
85
  }
69
86
  function parseIdleAfterMs(event) {
@@ -77,6 +94,6 @@ function parseIdleAfterMs(event) {
77
94
  var SPEC_NAME = "agentsandbox/v1";
78
95
  var SPEC_VERSION = "1.0.0-alpha";
79
96
 
80
- export { SPEC_NAME, SPEC_VERSION, createSandboxAgentSessionHost, resolveLifecyclePolicy };
97
+ export { SPEC_NAME, SPEC_VERSION, SandboxPortExposureUnsupportedError, createSandboxAgentSessionHost, exposePort, resolveLifecyclePolicy };
81
98
  //# sourceMappingURL=index.mjs.map
82
99
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/agent-session-host.ts","../src/lifecycle.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * AIP-36 sandbox-backed `AgentSessionHost`.\n *\n * The seam an `AgentStep` binds against (`AgentSessionHost`,\n * `@agentproto/workflow-runtime`) is already satisfiable by a *remote*\n * daemon via `connectDaemonAgentSessionHost` (`@agentproto/worktree`) —\n * it just needs a reachable MCP URL. So running a coding-agent step\n * inside a sandbox is: boot a provider-specific box that exposes an\n * agentproto daemon's MCP endpoint as a URL, then hand that URL to the\n * daemon host unchanged. No new session-host implementation, no\n * bespoke spawn/prompt plumbing — this module only wires secrets → env\n * → `provider.boot` → `connectDaemonAgentSessionHost`.\n */\n\nimport { assertSafeSecretValue, type SecretResolver } from \"@agentproto/secrets/exposure\"\nimport { connectDaemonAgentSessionHost, type DaemonAgentSessionHost } from \"@agentproto/worktree\"\nimport type { SandboxHandle } from \"./types.js\"\n\n/** AIP-36 sandbox manifest handle — provider id, config, env passthrough, limits. */\nexport type SandboxSpec = SandboxHandle\n\n/** What a `SandboxProvider` hands back once the box is up and reachable. */\nexport interface BootedSandbox {\n /** The booted agentproto daemon's MCP endpoint, reachable from this process. */\n mcpUrl: string\n /** Provider-assigned sandbox id, for logging / lookup. */\n sandboxId: string\n /** Opaque secret gating `mcpUrl`, present when `opts.expose === \"private\"`\n * was honoured (see `SandboxBootOpts.expose`). Absent for the default\n * public-exposure path (boot-and-drive) and for providers/paths that\n * can't gate the port at all — a caller that needs a gated URL (e.g.\n * `attachSandbox`) MUST treat a missing token as \"not gated\", not as\n * \"no auth needed\". The token is the raw secret; how a client must\n * PRESENT it (bearer header, cookie, …) is provider-specific — see\n * `authHeaders`. */\n token?: string\n /** Exact HTTP header(s) a client must send to authenticate against the\n * gated `mcpUrl` — the provider's own answer to \"how do I present the\n * token\". Box, for instance, gates its private hostname with a\n * `Cookie: _port_auth=<token>` (verified live: bearer/query are ignored,\n * the port edge only honours the cookie), so it returns that here rather\n * than leaving the caller to guess a scheme. Present iff `token` is; a\n * token-only provider that omits this is treated by `buildMcpConfigSnippet`\n * as `Authorization: Bearer <token>`. */\n authHeaders?: Record<string, string>\n /** Tear down the sandbox. */\n stop(): Promise<void>\n /** Pause the sandbox instead of killing it — keeps it reconnectable via\n * `SandboxProvider.connect(sandboxId, ...)` later. Optional: providers\n * that can't pause (or don't support reconnect at all) omit it; callers\n * that want to pause fall back to `stop()` when it's absent. */\n pause?(): Promise<void>\n}\n\n/** Env resolved from secrets, handed to `provider.boot`. */\nexport interface SandboxBootOpts {\n env: Record<string, string>\n /**\n * How the provider should expose the daemon's port. `\"public\"` (the\n * default when omitted) is boot-and-drive's ephemeral, provider-owned,\n * ungated URL. `\"private\"` asks the provider for a PERSISTENT,\n * token-gated URL instead — set by `attachSandbox`, which produces a\n * durable connection descriptor and must never emit an ungated one.\n * Providers that don't support gating simply ignore this and omit\n * `BootedSandbox.token`; the caller is responsible for treating that as\n * a failure when it needed a gated URL.\n */\n expose?: \"public\" | \"private\"\n /**\n * Keep the sandbox awake indefinitely for the always-on rendezvous model\n * — set by `attachSandbox` when its own `keepAlive` opt is true. A\n * provider that supports an explicit no-auto-stop/no-expiry assertion\n * (e.g. Box's `ttlSeconds: null`) should (re-)apply it as part of\n * `connect()`, defensively, even if the sandbox already defaults to it.\n * Providers with no such concept simply ignore this.\n */\n keepAlive?: boolean\n}\n\n/**\n * Backend-agnostic sandbox lifecycle. Concrete implementations (e2b, modal,\n * daytona, blaxel, …) live in their own packages so this one stays free of\n * vendor SDK dependencies — see `@agentproto/sandbox-e2b`.\n */\nexport interface SandboxProvider {\n boot(spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>\n /** Reconnect to an already-booted (possibly paused) sandbox instead of\n * booting a fresh one — the reuse path (`agent_start.sandbox.reuse`).\n * Optional: providers that can't reconnect (e.g. the `local` passthrough,\n * which tears down its temp workspace on `stop()`) omit it; the runtime\n * errors clearly when reuse is requested against such a provider. */\n connect?(sandboxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>\n}\n\n/** Which secrets to resolve into the sandbox's env, and how. */\nexport interface SandboxSecretsConfig {\n /** Secret slugs to resolve (e.g. `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`). */\n slugs: readonly string[]\n /** Resolves a slug to its value. Defaults to reading `process.env[slug]`. */\n resolver?: SecretResolver\n}\n\nexport interface CreateSandboxAgentSessionHostOpts {\n provider: SandboxProvider\n spec: SandboxSpec\n secrets: SandboxSecretsConfig\n /** Reconnect to this existing sandbox id instead of booting a fresh box —\n * requires `provider.connect`; throws a clear error otherwise. */\n sandboxId?: string\n}\n\nexport type SandboxAgentSessionHost = DaemonAgentSessionHost & {\n /** 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 /** Close the daemon connection AND tear down the sandbox. */\n stop(): Promise<void>\n /** Close the daemon connection and PAUSE the sandbox instead of killing\n * it — only present when the booted sandbox supports `pause()`. */\n pause?(): Promise<void>\n}\n\n/**\n * Resolve `secrets` into an env map, boot (or, when `opts.sandboxId` is set,\n * reconnect to) the sandbox with it, then connect the #202 daemon host to\n * the sandbox's exposed MCP URL. `stop()` closes the daemon connection\n * before tearing down the sandbox (never leaks the box on a client-side\n * error); `pause()` does the same but pauses rather than kills.\n */\nexport async function createSandboxAgentSessionHost(\n opts: CreateSandboxAgentSessionHostOpts,\n): Promise<SandboxAgentSessionHost> {\n const env = await resolveSandboxSecretsEnv(opts.secrets)\n let booted: BootedSandbox\n if (opts.sandboxId !== undefined) {\n if (!opts.provider.connect) {\n throw new Error(\n `createSandboxAgentSessionHost: reuse requested for sandbox \"${opts.sandboxId}\", ` +\n \"but this provider has no connect() — it can only boot fresh sandboxes.\",\n )\n }\n booted = await opts.provider.connect(opts.sandboxId, opts.spec, { env })\n } else {\n booted = await opts.provider.boot(opts.spec, { env })\n }\n let host: DaemonAgentSessionHost\n try {\n host = await connectDaemonAgentSessionHost({ url: booted.mcpUrl })\n } catch (err) {\n await booted.stop()\n throw err\n }\n return {\n ...host,\n sandboxId: booted.sandboxId,\n async stop(): Promise<void> {\n await host.close()\n await booted.stop()\n },\n ...(booted.pause\n ? {\n async pause(): Promise<void> {\n await host.close()\n await booted.pause!()\n },\n }\n : {}),\n }\n}\n\nconst defaultProcessEnvResolver: SecretResolver = name => process.env[name] ?? null\n\n/** Resolve every configured slug, failing loudly (no silent gaps in the sandbox env). */\nasync function resolveSandboxSecretsEnv(\n config: SandboxSecretsConfig,\n): Promise<Record<string, string>> {\n const resolver = config.resolver ?? defaultProcessEnvResolver\n const env: Record<string, string> = {}\n for (const slug of config.slugs) {\n const value = await resolver(slug)\n if (value === null || value === undefined) {\n throw new Error(\n `createSandboxAgentSessionHost: missing secret \"${slug}\" — set it in the ` +\n \"host process's environment, or pass a resolver that can supply it.\",\n )\n }\n assertSafeSecretValue(slug, value)\n env[slug] = value\n }\n return env\n}\n","/**\n * AIP-36 `lifecycle` policy resolution — maps a `SandboxHandle`'s\n * `lifecycle.pause_after_idle` / `lifecycle.destroy_on` (plus whether this\n * boot is a request to reconnect to an existing box) to a concrete\n * teardown decision. Pure and host-agnostic: the actual pause-vs-kill call\n * happens in `@agentproto/runtime`'s sandbox proxy, which just reads this\n * policy back off.\n */\n\nimport type { SandboxHandle } from \"./types.js\"\n\nexport interface SandboxLifecyclePolicy {\n /** What session close should do to the box: kill it (ephemeral, the\n * default) or pause it (keeps it reconnectable via `SandboxProvider.\n * connect`). */\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 * `reuse` is true when this spawn asked to reconnect to an existing\n * sandbox id (`agent_start.sandbox.reuse`) — such a box defaults to\n * \"pause\" on close even absent an explicit `lifecycle` block, since\n * killing it would defeat the point of having reconnected. An explicit\n * `destroy_on` always wins over both `reuse` and `pause_after_idle`: the\n * spec is stating outright that this 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\" = reuse || pauseAfterIdleMs !== undefined ? \"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 type SandboxSpec,\n type BootedSandbox,\n type SandboxBootOpts,\n type SandboxProvider,\n type SandboxSecretsConfig,\n type CreateSandboxAgentSessionHostOpts,\n type SandboxAgentSessionHost,\n} from \"./agent-session-host.js\"\n\nexport { resolveLifecyclePolicy, type SandboxLifecyclePolicy } from \"./lifecycle.js\"\n"]}
1
+ {"version":3,"sources":["../src/agent-session-host.ts","../src/lifecycle.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * AIP-36 sandbox-backed `AgentSessionHost`.\n *\n * The seam an `AgentStep` binds against (`AgentSessionHost`,\n * `@agentproto/workflow-runtime`) is already satisfiable by a *remote*\n * daemon via `connectDaemonAgentSessionHost` (`@agentproto/worktree`) —\n * it just needs a reachable MCP URL. So running a coding-agent step\n * inside a sandbox is: boot a provider-specific box that exposes an\n * agentproto daemon's MCP endpoint as a URL, then hand that URL to the\n * daemon host unchanged. No new session-host implementation, no\n * bespoke spawn/prompt plumbing — this module only wires secrets → env\n * → `provider.boot` → `connectDaemonAgentSessionHost`.\n */\n\nimport { assertSafeSecretValue, type SecretResolver } from \"@agentproto/secrets/exposure\"\nimport { connectDaemonAgentSessionHost, type DaemonAgentSessionHost } from \"@agentproto/worktree\"\nimport type { SandboxHandle } from \"./types.js\"\n\n/** AIP-36 sandbox manifest handle — provider id, config, env passthrough, limits. */\nexport type SandboxSpec = SandboxHandle\n\n/**\n * Thrown when a caller requests port exposure on a `BootedSandbox` whose\n * provider does not support it — i.e. the sandbox handle has no `expose()`\n * method. Callers should check for `expose` before calling it, or catch\n * this error and fall back gracefully.\n */\nexport class SandboxPortExposureUnsupportedError extends Error {\n constructor(message?: string) {\n super(message ?? \"This sandbox provider does not support port exposure.\")\n this.name = \"SandboxPortExposureUnsupportedError\"\n }\n}\n\n/** What a `SandboxProvider` hands back once the box is up and reachable. */\nexport interface BootedSandbox {\n /** The booted agentproto daemon's MCP endpoint, reachable from this process. */\n mcpUrl: string\n /** Provider-assigned sandbox id, for logging / lookup. */\n sandboxId: string\n /** Opaque secret gating `mcpUrl`, present when `opts.expose === \"private\"`\n * was honoured (see `SandboxBootOpts.expose`). Absent for the default\n * public-exposure path (boot-and-drive) and for providers/paths that\n * can't gate the port at all — a caller that needs a gated URL (e.g.\n * `attachSandbox`) MUST treat a missing token as \"not gated\", not as\n * \"no auth needed\". The token is the raw secret; how a client must\n * PRESENT it (bearer header, cookie, …) is provider-specific — see\n * `authHeaders`. */\n token?: string\n /** Exact HTTP header(s) a client must send to authenticate against the\n * gated `mcpUrl` — the provider's own answer to \"how do I present the\n * token\". Box, for instance, gates its private hostname with a\n * `Cookie: _port_auth=<token>` (verified live: bearer/query are ignored,\n * the port edge only honours the cookie), so it returns that here rather\n * than leaving the caller to guess a scheme. Present iff `token` is; a\n * token-only provider that omits this is treated by `buildMcpConfigSnippet`\n * as `Authorization: Bearer <token>`. */\n authHeaders?: Record<string, string>\n /**\n * Expose an app port on the sandbox and return its public URL. E2B returns\n * `https://<port>-<sandboxId>.e2b.app`. Loopback bind is enough inside the\n * VM — the provider's edge handles the forwarding.\n *\n * Optional: providers that cannot expose arbitrary ports omit this method.\n * Callers should check for presence before calling, or catch\n * `SandboxPortExposureUnsupportedError` when using `exposePort()`.\n */\n expose?(port: number): Promise<{ url: string }>\n /**\n * Ports resolved at boot time from `SandboxSpec.extraPorts` — a map of\n * port number to public URL. Only present when the spec declared\n * `extraPorts` AND the provider supports exposure. Callers that need a\n * port URL at runtime should use `expose()` directly when this map is\n * absent or doesn't include the target port.\n */\n ports?: Record<number, string>\n /** Tear down the sandbox. */\n stop(): Promise<void>\n /** Pause the sandbox instead of killing it — keeps it reconnectable via\n * `SandboxProvider.connect(sandboxId, ...)` later. Optional: providers\n * that can't pause (or don't support reconnect at all) omit it; callers\n * that want to pause fall back to `stop()` when it's absent. */\n pause?(): Promise<void>\n}\n\n/**\n * Expose a port on a booted sandbox. Throws `SandboxPortExposureUnsupportedError`\n * when the provider's sandbox handle has no `expose()` method.\n */\nexport async function exposePort(booted: BootedSandbox, port: number): Promise<{ url: string }> {\n if (!booted.expose) {\n throw new SandboxPortExposureUnsupportedError(\n `sandbox \"${booted.sandboxId}\" does not support port exposure — ` +\n \"the provider has no expose() implementation.\",\n )\n }\n return booted.expose(port)\n}\n\n/** Env resolved from secrets, handed to `provider.boot`. */\nexport interface SandboxBootOpts {\n env: Record<string, string>\n /**\n * How the provider should expose the daemon's port. `\"public\"` (the\n * default when omitted) is boot-and-drive's ephemeral, provider-owned,\n * ungated URL. `\"private\"` asks the provider for a PERSISTENT,\n * token-gated URL instead — set by `attachSandbox`, which produces a\n * durable connection descriptor and must never emit an ungated one.\n * Providers that don't support gating simply ignore this and omit\n * `BootedSandbox.token`; the caller is responsible for treating that as\n * a failure when it needed a gated URL.\n */\n expose?: \"public\" | \"private\"\n /**\n * Keep the sandbox awake indefinitely for the always-on rendezvous model\n * — set by `attachSandbox` when its own `keepAlive` opt is true. A\n * provider that supports an explicit no-auto-stop/no-expiry assertion\n * (e.g. Box's `ttlSeconds: null`) should (re-)apply it as part of\n * `connect()`, defensively, even if the sandbox already defaults to it.\n * Providers with no such concept simply ignore this.\n */\n keepAlive?: boolean\n}\n\n/**\n * Backend-agnostic sandbox lifecycle. Concrete implementations (e2b, modal,\n * daytona, blaxel, …) live in their own packages so this one stays free of\n * vendor SDK dependencies — see `@agentproto/sandbox-e2b`.\n */\nexport interface SandboxProvider {\n boot(spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>\n /** Reconnect to an already-booted (possibly paused) sandbox instead of\n * booting a fresh one — the reuse path (`agent_start.sandbox.reuse`).\n * Optional: providers that can't reconnect (e.g. the `local` passthrough,\n * which tears down its temp workspace on `stop()`) omit it; the runtime\n * errors clearly when reuse is requested against such a provider. */\n connect?(sandboxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>\n}\n\n/** Which secrets to resolve into the sandbox's env, and how. */\nexport interface SandboxSecretsConfig {\n /** Secret slugs to resolve (e.g. `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`). */\n slugs: readonly string[]\n /** Resolves a slug to its value. Defaults to reading `process.env[slug]`. */\n resolver?: SecretResolver\n}\n\nexport interface CreateSandboxAgentSessionHostOpts {\n provider: SandboxProvider\n spec: SandboxSpec\n secrets: SandboxSecretsConfig\n /** Reconnect to this existing sandbox id instead of booting a fresh box —\n * requires `provider.connect`; throws a clear error otherwise. */\n sandboxId?: string\n}\n\nexport type SandboxAgentSessionHost = DaemonAgentSessionHost & {\n /** The booted sandbox daemon's MCP endpoint (`BootedSandbox.mcpUrl`) —\n * surfaced so a caller can drive the box's OTHER daemon tools (app_install,\n * command_execute, …) the same way the session host drives agent_start. */\n mcpUrl: string\n /** Provider-assigned sandbox id (`BootedSandbox.sandboxId`) — surfaced so a\n * caller can record it (there's no local PID for a sandboxed session). */\n sandboxId: string\n /** Ports resolved at boot from `SandboxSpec.extraPorts` — forwarded from\n * `BootedSandbox.ports` so the runtime can record them on the session\n * descriptor without reaching into the booted handle after the fact. */\n ports?: Record<number, string>\n /** Expose an app port and return its public URL — forwarded from\n * `BootedSandbox.expose`. Absent when the provider doesn't support it. */\n expose?: BootedSandbox[\"expose\"]\n /** Close the daemon connection AND tear down the sandbox. */\n stop(): Promise<void>\n /** Close the daemon connection and PAUSE the sandbox instead of killing\n * it — only present when the booted sandbox supports `pause()`. */\n pause?(): Promise<void>\n}\n\n/**\n * Resolve `secrets` into an env map, boot (or, when `opts.sandboxId` is set,\n * reconnect to) the sandbox with it, then connect the #202 daemon host to\n * the sandbox's exposed MCP URL. `stop()` closes the daemon connection\n * before tearing down the sandbox (never leaks the box on a client-side\n * error); `pause()` does the same but pauses rather than kills.\n */\nexport async function createSandboxAgentSessionHost(\n opts: CreateSandboxAgentSessionHostOpts,\n): Promise<SandboxAgentSessionHost> {\n const env = await resolveSandboxSecretsEnv(opts.secrets)\n let booted: BootedSandbox\n if (opts.sandboxId !== undefined) {\n if (!opts.provider.connect) {\n throw new Error(\n `createSandboxAgentSessionHost: reuse requested for sandbox \"${opts.sandboxId}\", ` +\n \"but this provider has no connect() — it can only boot fresh sandboxes.\",\n )\n }\n booted = await opts.provider.connect(opts.sandboxId, opts.spec, { env })\n } else {\n booted = await opts.provider.boot(opts.spec, { env })\n }\n let host: DaemonAgentSessionHost\n try {\n host = await connectDaemonAgentSessionHost({ url: booted.mcpUrl })\n } catch (err) {\n await booted.stop()\n throw err\n }\n return {\n ...host,\n mcpUrl: booted.mcpUrl,\n sandboxId: booted.sandboxId,\n ...(booted.ports ? { ports: booted.ports } : {}),\n ...(booted.expose ? { expose: booted.expose.bind(booted) } : {}),\n async stop(): Promise<void> {\n await host.close()\n await booted.stop()\n },\n ...(booted.pause\n ? {\n async pause(): Promise<void> {\n await host.close()\n await booted.pause!()\n },\n }\n : {}),\n }\n}\n\nconst defaultProcessEnvResolver: SecretResolver = name => process.env[name] ?? null\n\n/** Resolve every configured slug, failing loudly (no silent gaps in the sandbox env). */\nasync function resolveSandboxSecretsEnv(\n config: SandboxSecretsConfig,\n): Promise<Record<string, string>> {\n const resolver = config.resolver ?? defaultProcessEnvResolver\n const env: Record<string, string> = {}\n for (const slug of config.slugs) {\n const value = await resolver(slug)\n if (value === null || value === undefined) {\n throw new Error(\n `createSandboxAgentSessionHost: missing secret \"${slug}\" — set it in the ` +\n \"host process's environment, or pass a resolver that can supply it.\",\n )\n }\n assertSafeSecretValue(slug, value)\n env[slug] = value\n }\n return env\n}\n","/**\n * AIP-36 `lifecycle` policy resolution — maps a `SandboxHandle`'s\n * `lifecycle.pause_after_idle` / `lifecycle.destroy_on` (plus whether this\n * boot is a request to reconnect to an existing box) to a concrete\n * teardown decision. Pure and host-agnostic: the actual pause-vs-kill call\n * happens in `@agentproto/runtime`'s sandbox proxy, which just reads this\n * policy back off.\n */\n\nimport type { SandboxHandle } from \"./types.js\"\n\nexport interface SandboxLifecyclePolicy {\n /** What session close should do to the box: pause it (keeps it\n * reconnectable via `SandboxProvider.connect`) or kill it (ephemeral).\n * Pause is the default: absent any explicit lifecycle declaration the\n * box is paused on close, and dies at its own `timeoutMs` anyway. */\n teardown: \"kill\" | \"pause\"\n /** Idle window in milliseconds, parsed from the AIP-37 `idle-<seconds>`\n * event name. Undefined when the spec doesn't declare\n * `lifecycle.pause_after_idle`. */\n pauseAfterIdleMs?: number\n}\n\nconst IDLE_EVENT_PATTERN = /^idle-(\\d+)$/\n\n/**\n * Pause is the default teardown: absent `destroy_on`, `pause_after_idle`\n * AND `reuse`, a closed box is paused (`SandboxProvider.connect`-able)\n * rather than killed — it still dies at its own `timeoutMs`, so pausing\n * never accumulates boxes indefinitely. The explicit declarations stay\n * authoritative: an `destroy_on` always kills (the spec states outright\n * the box must not survive session close), and `pause_after_idle` /\n * `reuse` pause (which the default now agrees with).\n */\nexport function resolveLifecyclePolicy(spec: SandboxHandle, reuse: boolean): SandboxLifecyclePolicy {\n if (spec.lifecycle?.destroy_on) return { teardown: \"kill\" }\n\n const pauseAfterIdleMs = parseIdleAfterMs(spec.lifecycle?.pause_after_idle)\n const teardown: \"kill\" | \"pause\" = \"pause\"\n return { teardown, ...(pauseAfterIdleMs !== undefined ? { pauseAfterIdleMs } : {}) }\n}\n\nfunction parseIdleAfterMs(event: string | undefined): number | undefined {\n if (!event) return undefined\n const match = IDLE_EVENT_PATTERN.exec(event)\n if (!match) return undefined\n return Number(match[1]) * 1000\n}\n","/**\n * @agentproto/sandbox — AIP-36 SANDBOX.md `defineSandbox` reference impl.\n *\n * A composable schema block defining the `sandbox` field — provider, config, command env, network egress, resource limits — for any manifest that names a compute environment for agent-issued shell commands. Sibling primitive to STORAGE.md (AIP-35); inline or ref, mirroring AIP-17 RUNNER and AIP-19 SECRETS.\n *\n * Spec: https://agentproto.sh/docs/aip-36\n *\n * Authoring paths:\n * - TS: `defineSandbox({...})` → `SandboxHandle`\n * - MD: `parseSandboxManifest(src) → sandboxFromManifest({...})` → `SandboxHandle`\n */\n\nexport const SPEC_NAME = \"agentsandbox/v1\" as const\nexport const SPEC_VERSION = \"1.0.0-alpha\" as const\n\nexport { defineSandbox } from \"./define-sandbox.js\"\nexport type {\n SandboxDefinition,\n SandboxHandle,\n SandboxRuntimeInput,\n SandboxRuntimeHandle,\n} from \"./types.js\"\n\n/** The AIP-36 frontmatter zod schema, under the name consumers that accept\n * an inline `SandboxSpec` (e.g. `@agentproto/runtime`'s `agent_start.sandbox`)\n * validate against. Same schema `define-sandbox.ts`/`manifest/index.ts` use. */\nexport { sandboxFrontmatterSchema as SandboxSpecSchema } from \"./schema.js\"\n\nexport {\n createSandboxAgentSessionHost,\n exposePort,\n SandboxPortExposureUnsupportedError,\n type SandboxSpec,\n type BootedSandbox,\n type SandboxBootOpts,\n type SandboxProvider,\n type SandboxSecretsConfig,\n type CreateSandboxAgentSessionHostOpts,\n type SandboxAgentSessionHost,\n} from \"./agent-session-host.js\"\n\nexport { resolveLifecyclePolicy, type SandboxLifecyclePolicy } from \"./lifecycle.js\"\n"]}
@@ -1,5 +1,5 @@
1
- import { d as SandboxFrontmatter, b as SandboxHandle } from '../schema-PGQIdcQ3.js';
2
- export { s as sandboxFrontmatterSchema } from '../schema-PGQIdcQ3.js';
1
+ import { d as SandboxFrontmatter, b as SandboxHandle } from '../schema-h9uR_HeR.js';
2
+ export { s as sandboxFrontmatterSchema } from '../schema-h9uR_HeR.js';
3
3
  import 'zod';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { sandboxFrontmatterSchema, defineSandbox } from '../chunk-VSYQ4BUI.mjs';
2
- export { sandboxFrontmatterSchema } from '../chunk-VSYQ4BUI.mjs';
1
+ import { sandboxFrontmatterSchema, defineSandbox } from '../chunk-VGEBUSSP.mjs';
2
+ export { sandboxFrontmatterSchema } from '../chunk-VGEBUSSP.mjs';
3
3
  import matter from 'gray-matter';
4
4
 
5
5
  /**
@@ -48,7 +48,16 @@ interface SandboxDefinition {
48
48
  /**
49
49
  * Provider-specific connection fields. Shape varies per provider (see AIP-36 §Provider config shapes).
50
50
  */
51
- config: {};
51
+ config: {
52
+ /**
53
+ * Semantic harness slugs to pre-install in the box at boot (e.g. `["hermes", "claude-code"]`).
54
+ * Each expands to `@agentproto/adapter-<slug>@latest` plus that adapter's declared boot extras;
55
+ * a `config.installPackages` pin for the same package always wins. Additive — absent means no
56
+ * extra install.
57
+ */
58
+ installAdapters?: string[];
59
+ [k: string]: unknown;
60
+ };
52
61
  /**
53
62
  * Resource caps per command.
54
63
  */
@@ -63,6 +72,17 @@ interface SandboxDefinition {
63
72
  * Static host env-var names to forward into the sandbox.
64
73
  */
65
74
  passthrough?: string[];
75
+ /**
76
+ * Opt-in: forward the spawn's RESOLVED billing-credential env-var NAME
77
+ * (e.g. `ANTHROPIC_API_KEY`) into `passthrough` so a fresh box inherits
78
+ * host auth without the caller naming vars. Only the name is injected —
79
+ * the value travels via the normal passthrough mechanism (host secrets
80
+ * broker → box env) and is never read by this flag. Billing credential
81
+ * only: other vars (GITHUB_TOKEN, …) stay the job of an explicit
82
+ * `passthrough`. A convenience, not a contract: when no credential
83
+ * resolved, nothing is injected and the spawn proceeds.
84
+ */
85
+ autoPassthrough?: boolean;
66
86
  };
67
87
  network?: {
68
88
  /**
@@ -92,6 +112,13 @@ interface SandboxDefinition {
92
112
  * Reject command execution at the sandbox layer. Read-only sandbox calls fail with `sandbox_read_only`.
93
113
  */
94
114
  read_only?: boolean;
115
+ /**
116
+ * App ports to expose at boot time. Resolved into `BootedSandbox.ports` (port → public URL)
117
+ * by providers that support port exposure (e.g. e2b via `getHost(port)`). Providers that do
118
+ * not support exposure ignore this field — callers that need a URL must check `ports` on the
119
+ * booted handle and call `expose()` explicitly when the field is absent.
120
+ */
121
+ extraPorts?: number[];
95
122
  /**
96
123
  * Free-form, namespaced. Adapter hints under `metadata.<adapter>.*`.
97
124
  */
@@ -317,7 +344,9 @@ declare const sandboxFrontmatterSchema: z.ZodObject<{
317
344
  id: z.ZodOptional<z.ZodString>;
318
345
  version: z.ZodOptional<z.ZodString>;
319
346
  provider: z.ZodString;
320
- config: z.ZodRecord<z.ZodString, z.ZodAny>;
347
+ config: z.ZodObject<{
348
+ installAdapters: z.ZodOptional<z.ZodArray<z.ZodString>>;
349
+ }, z.core.$catchall<z.ZodAny>>;
321
350
  limits: z.ZodOptional<z.ZodObject<{
322
351
  timeout_ms: z.ZodOptional<z.ZodNumber>;
323
352
  memory_mb: z.ZodOptional<z.ZodNumber>;
@@ -326,17 +355,20 @@ declare const sandboxFrontmatterSchema: z.ZodObject<{
326
355
  env: z.ZodOptional<z.ZodObject<{
327
356
  auth: z.ZodOptional<z.ZodAny>;
328
357
  passthrough: z.ZodDefault<z.ZodArray<z.ZodString>>;
358
+ autoPassthrough: z.ZodOptional<z.ZodBoolean>;
329
359
  }, z.core.$strict>>;
330
360
  network: z.ZodOptional<z.ZodObject<{
331
361
  egress: z.ZodDefault<z.ZodArray<z.ZodString>>;
332
362
  }, z.core.$strict>>;
333
363
  mounts: z.ZodDefault<z.ZodArray<z.ZodAny>>;
334
364
  identity: z.ZodOptional<z.ZodAny>;
365
+ policy: z.ZodOptional<z.ZodAny>;
335
366
  lifecycle: z.ZodOptional<z.ZodObject<{
336
367
  pause_after_idle: z.ZodOptional<z.ZodString>;
337
368
  destroy_on: z.ZodOptional<z.ZodString>;
338
369
  }, z.core.$strict>>;
339
370
  read_only: z.ZodDefault<z.ZodBoolean>;
371
+ extraPorts: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
340
372
  metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
341
373
  }, z.core.$strict>;
342
374
  type SandboxFrontmatter = z.infer<typeof sandboxFrontmatterSchema>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/sandbox",
3
- "version": "0.2.6",
3
+ "version": "0.4.0",
4
4
  "description": "@agentproto/sandbox — AIP-36 SANDBOX.md reference implementation. A composable schema block defining the `sandbox` field — provider, config, command env, network egress, resource limits — for any manifest that names a compute environment for agent-issued shell commands. Sibling primitive to STORAGE.md (AIP-35); inline or ref, mirroring AIP-17 RUNNER and AIP-19 SECRETS. Also ships createSandboxAgentSessionHost — the provider-agnostic seam that runs an AgentStep's coding-agent turn inside a booted sandbox by pointing the existing daemon-backed AgentSessionHost at its MCP URL.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -48,17 +48,17 @@
48
48
  "dependencies": {
49
49
  "gray-matter": "^4.0.3",
50
50
  "zod": "^4.5.4",
51
- "@agentproto/define-doctype": "0.1.1",
52
- "@agentproto/secrets": "0.2.4",
53
- "@agentproto/workflow-runtime": "0.9.0",
54
- "@agentproto/worktree": "0.5.5"
51
+ "@agentproto/secrets": "0.2.5",
52
+ "@agentproto/workflow-runtime": "0.11.0",
53
+ "@agentproto/define-doctype": "0.1.2",
54
+ "@agentproto/worktree": "0.6.2"
55
55
  },
56
56
  "devDependencies": {
57
- "@types/node": "^25.6.2",
57
+ "@types/node": "^25.9.5",
58
58
  "tsup": "^8.5.1",
59
59
  "typescript": "^5.9.3",
60
- "vitest": "^3.2.4",
61
- "@agentproto/tooling": "0.1.0-alpha.0"
60
+ "vitest": "^3.2.7",
61
+ "@agentproto/tooling": "0.1.0"
62
62
  },
63
63
  "scripts": {
64
64
  "dev": "tsup --watch",
@@ -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.record(z.string(), 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([]) }).strict().optional(), "network": z.object({ "egress": z.array(z.string()).describe("Hostnames the sandbox MAY reach. Empty / missing = no egress.").default([]) }).strict().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(), "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), "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-VSYQ4BUI.mjs.map
47
- //# sourceMappingURL=chunk-VSYQ4BUI.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/schema.ts","../src/define-sandbox.ts"],"names":[],"mappings":"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","file":"chunk-VSYQ4BUI.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.record(z.string(), 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) }).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(), \"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(), \"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), \"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"]}