@agentproto/sandbox 0.1.0-alpha.0 → 0.1.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
@@ -18,6 +18,32 @@ const x = defineSandbox({
18
18
  })
19
19
  ```
20
20
 
21
+ ## Running an agent step inside a sandbox
22
+
23
+ `createSandboxAgentSessionHost` turns any `SandboxProvider` into an
24
+ `AgentSessionHost` (`@agentproto/workflow-runtime`) — the seam `runWorkflow`
25
+ injects for every `AgentStep`. It resolves the requested secrets into an env
26
+ map, boots the sandbox with that env, then connects
27
+ `connectDaemonAgentSessionHost` (`@agentproto/worktree`) to the sandbox's
28
+ exposed agentproto daemon — reusing the same daemon-backed session host a
29
+ local run would use, unchanged.
30
+
31
+ ```ts
32
+ import { createSandboxAgentSessionHost } from "@agentproto/sandbox"
33
+ import { e2bSandboxProvider } from "@agentproto/sandbox-e2b"
34
+
35
+ const host = await createSandboxAgentSessionHost({
36
+ provider: e2bSandboxProvider,
37
+ spec: { provider: "e2b", config: {} },
38
+ secrets: { slugs: ["OPENROUTER_API_KEY"] },
39
+ })
40
+ try {
41
+ // await runWorkflow({ workflow, input, agents: host })
42
+ } finally {
43
+ await host.stop()
44
+ }
45
+ ```
46
+
21
47
  ## License
22
48
 
23
49
  MIT — see [LICENSE](./LICENSE).
package/dist/index.d.ts CHANGED
@@ -1,5 +1,8 @@
1
- import { S as SandboxRuntimeInput, a as SandboxRuntimeHandle } from './types-CCVWFnWl.js';
2
- export { b as SandboxDefinition, c as SandboxHandle } from './types-CCVWFnWl.js';
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';
3
+ import { SecretResolver } from '@agentproto/secrets/exposure';
4
+ import { DaemonAgentSessionHost } from '@agentproto/worktree';
5
+ import 'zod';
3
6
 
4
7
  /**
5
8
  * Type-aware wrapper preserving `TFactory` / `TCapabilities` generics
@@ -7,6 +10,117 @@ export { b as SandboxDefinition, c as SandboxHandle } from './types-CCVWFnWl.js'
7
10
  */
8
11
  declare function defineSandbox<TFactory = unknown, TCapabilities extends Record<string, unknown> = Record<string, unknown>>(definition: SandboxRuntimeInput<TFactory, TCapabilities>): SandboxRuntimeHandle<TFactory, TCapabilities>;
9
12
 
13
+ /**
14
+ * AIP-36 sandbox-backed `AgentSessionHost`.
15
+ *
16
+ * The seam an `AgentStep` binds against (`AgentSessionHost`,
17
+ * `@agentproto/workflow-runtime`) is already satisfiable by a *remote*
18
+ * daemon via `connectDaemonAgentSessionHost` (`@agentproto/worktree`) —
19
+ * it just needs a reachable MCP URL. So running a coding-agent step
20
+ * inside a sandbox is: boot a provider-specific box that exposes an
21
+ * agentproto daemon's MCP endpoint as a URL, then hand that URL to the
22
+ * daemon host unchanged. No new session-host implementation, no
23
+ * bespoke spawn/prompt plumbing — this module only wires secrets → env
24
+ * → `provider.boot` → `connectDaemonAgentSessionHost`.
25
+ */
26
+
27
+ /** AIP-36 sandbox manifest handle — provider id, config, env passthrough, limits. */
28
+ type SandboxSpec = SandboxHandle;
29
+ /** What a `SandboxProvider` hands back once the box is up and reachable. */
30
+ interface BootedSandbox {
31
+ /** The booted agentproto daemon's MCP endpoint, reachable from this process. */
32
+ mcpUrl: string;
33
+ /** Provider-assigned sandbox id, for logging / lookup. */
34
+ sandboxId: string;
35
+ /** Tear down the sandbox. */
36
+ stop(): Promise<void>;
37
+ /** Pause the sandbox instead of killing it — keeps it reconnectable via
38
+ * `SandboxProvider.connect(sandboxId, ...)` later. Optional: providers
39
+ * that can't pause (or don't support reconnect at all) omit it; callers
40
+ * that want to pause fall back to `stop()` when it's absent. */
41
+ pause?(): Promise<void>;
42
+ }
43
+ /** Env resolved from secrets, handed to `provider.boot`. */
44
+ interface SandboxBootOpts {
45
+ env: Record<string, string>;
46
+ }
47
+ /**
48
+ * Backend-agnostic sandbox lifecycle. Concrete implementations (e2b, modal,
49
+ * daytona, blaxel, …) live in their own packages so this one stays free of
50
+ * vendor SDK dependencies — see `@agentproto/sandbox-e2b`.
51
+ */
52
+ interface SandboxProvider {
53
+ boot(spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>;
54
+ /** Reconnect to an already-booted (possibly paused) sandbox instead of
55
+ * booting a fresh one — the reuse path (`agent_start.sandbox.reuse`).
56
+ * Optional: providers that can't reconnect (e.g. the `local` passthrough,
57
+ * which tears down its temp workspace on `stop()`) omit it; the runtime
58
+ * errors clearly when reuse is requested against such a provider. */
59
+ connect?(sandboxId: string, spec: SandboxSpec, opts: SandboxBootOpts): Promise<BootedSandbox>;
60
+ }
61
+ /** Which secrets to resolve into the sandbox's env, and how. */
62
+ interface SandboxSecretsConfig {
63
+ /** Secret slugs to resolve (e.g. `OPENROUTER_API_KEY`, `ANTHROPIC_API_KEY`). */
64
+ slugs: readonly string[];
65
+ /** Resolves a slug to its value. Defaults to reading `process.env[slug]`. */
66
+ resolver?: SecretResolver;
67
+ }
68
+ interface CreateSandboxAgentSessionHostOpts {
69
+ provider: SandboxProvider;
70
+ spec: SandboxSpec;
71
+ secrets: SandboxSecretsConfig;
72
+ /** Reconnect to this existing sandbox id instead of booting a fresh box —
73
+ * requires `provider.connect`; throws a clear error otherwise. */
74
+ sandboxId?: string;
75
+ }
76
+ type SandboxAgentSessionHost = DaemonAgentSessionHost & {
77
+ /** Provider-assigned sandbox id (`BootedSandbox.sandboxId`) — surfaced so a
78
+ * caller can record it (there's no local PID for a sandboxed session). */
79
+ sandboxId: string;
80
+ /** Close the daemon connection AND tear down the sandbox. */
81
+ stop(): Promise<void>;
82
+ /** Close the daemon connection and PAUSE the sandbox instead of killing
83
+ * it — only present when the booted sandbox supports `pause()`. */
84
+ pause?(): Promise<void>;
85
+ };
86
+ /**
87
+ * Resolve `secrets` into an env map, boot (or, when `opts.sandboxId` is set,
88
+ * reconnect to) the sandbox with it, then connect the #202 daemon host to
89
+ * the sandbox's exposed MCP URL. `stop()` closes the daemon connection
90
+ * before tearing down the sandbox (never leaks the box on a client-side
91
+ * error); `pause()` does the same but pauses rather than kills.
92
+ */
93
+ declare function createSandboxAgentSessionHost(opts: CreateSandboxAgentSessionHostOpts): Promise<SandboxAgentSessionHost>;
94
+
95
+ /**
96
+ * AIP-36 `lifecycle` policy resolution — maps a `SandboxHandle`'s
97
+ * `lifecycle.pause_after_idle` / `lifecycle.destroy_on` (plus whether this
98
+ * boot is a request to reconnect to an existing box) to a concrete
99
+ * teardown decision. Pure and host-agnostic: the actual pause-vs-kill call
100
+ * happens in `@agentproto/runtime`'s sandbox proxy, which just reads this
101
+ * policy back off.
102
+ */
103
+
104
+ interface SandboxLifecyclePolicy {
105
+ /** What session close should do to the box: kill it (ephemeral, the
106
+ * default) or pause it (keeps it reconnectable via `SandboxProvider.
107
+ * connect`). */
108
+ teardown: "kill" | "pause";
109
+ /** Idle window in milliseconds, parsed from the AIP-37 `idle-<seconds>`
110
+ * event name. Undefined when the spec doesn't declare
111
+ * `lifecycle.pause_after_idle`. */
112
+ pauseAfterIdleMs?: number;
113
+ }
114
+ /**
115
+ * `reuse` is true when this spawn asked to reconnect to an existing
116
+ * sandbox id (`agent_start.sandbox.reuse`) — such a box defaults to
117
+ * "pause" on close even absent an explicit `lifecycle` block, since
118
+ * killing it would defeat the point of having reconnected. An explicit
119
+ * `destroy_on` always wins over both `reuse` and `pause_after_idle`: the
120
+ * spec is stating outright that this box must not survive session close.
121
+ */
122
+ declare function resolveLifecyclePolicy(spec: SandboxHandle, reuse: boolean): SandboxLifecyclePolicy;
123
+
10
124
  /**
11
125
  * @agentproto/sandbox — AIP-36 SANDBOX.md `defineSandbox` reference impl.
12
126
  *
@@ -21,4 +135,4 @@ declare function defineSandbox<TFactory = unknown, TCapabilities extends Record<
21
135
  declare const SPEC_NAME: "agentsandbox/v1";
22
136
  declare const SPEC_VERSION: "1.0.0-alpha";
23
137
 
24
- export { SPEC_NAME, SPEC_VERSION, SandboxRuntimeHandle, SandboxRuntimeInput, defineSandbox };
138
+ 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 };
package/dist/index.mjs CHANGED
@@ -1,14 +1,82 @@
1
- export { defineSandbox } from './chunk-VSYQ4BUI.mjs';
1
+ export { sandboxFrontmatterSchema as SandboxSpecSchema, defineSandbox } from './chunk-VSYQ4BUI.mjs';
2
+ import { assertSafeSecretValue } from '@agentproto/secrets/exposure';
3
+ import { connectDaemonAgentSessionHost } from '@agentproto/worktree';
2
4
 
3
5
  /**
4
6
  * @agentproto/sandbox v0.1.0-alpha
5
7
  * AIP-36 SANDBOX.md `defineSandbox` reference implementation.
6
8
  */
9
+ async function createSandboxAgentSessionHost(opts) {
10
+ const env = await resolveSandboxSecretsEnv(opts.secrets);
11
+ let booted;
12
+ if (opts.sandboxId !== void 0) {
13
+ if (!opts.provider.connect) {
14
+ throw new Error(
15
+ `createSandboxAgentSessionHost: reuse requested for sandbox "${opts.sandboxId}", but this provider has no connect() \u2014 it can only boot fresh sandboxes.`
16
+ );
17
+ }
18
+ booted = await opts.provider.connect(opts.sandboxId, opts.spec, { env });
19
+ } else {
20
+ booted = await opts.provider.boot(opts.spec, { env });
21
+ }
22
+ let host;
23
+ try {
24
+ host = await connectDaemonAgentSessionHost({ url: booted.mcpUrl });
25
+ } catch (err) {
26
+ await booted.stop();
27
+ throw err;
28
+ }
29
+ return {
30
+ ...host,
31
+ sandboxId: booted.sandboxId,
32
+ async stop() {
33
+ await host.close();
34
+ await booted.stop();
35
+ },
36
+ ...booted.pause ? {
37
+ async pause() {
38
+ await host.close();
39
+ await booted.pause();
40
+ }
41
+ } : {}
42
+ };
43
+ }
44
+ var defaultProcessEnvResolver = (name) => process.env[name] ?? null;
45
+ async function resolveSandboxSecretsEnv(config) {
46
+ const resolver = config.resolver ?? defaultProcessEnvResolver;
47
+ const env = {};
48
+ for (const slug of config.slugs) {
49
+ const value = await resolver(slug);
50
+ if (value === null || value === void 0) {
51
+ throw new Error(
52
+ `createSandboxAgentSessionHost: missing secret "${slug}" \u2014 set it in the host process's environment, or pass a resolver that can supply it.`
53
+ );
54
+ }
55
+ assertSafeSecretValue(slug, value);
56
+ env[slug] = value;
57
+ }
58
+ return env;
59
+ }
60
+
61
+ // src/lifecycle.ts
62
+ var IDLE_EVENT_PATTERN = /^idle-(\d+)$/;
63
+ function resolveLifecyclePolicy(spec, reuse) {
64
+ if (spec.lifecycle?.destroy_on) return { teardown: "kill" };
65
+ const pauseAfterIdleMs = parseIdleAfterMs(spec.lifecycle?.pause_after_idle);
66
+ const teardown = reuse || pauseAfterIdleMs !== void 0 ? "pause" : "kill";
67
+ return { teardown, ...pauseAfterIdleMs !== void 0 ? { pauseAfterIdleMs } : {} };
68
+ }
69
+ function parseIdleAfterMs(event) {
70
+ if (!event) return void 0;
71
+ const match = IDLE_EVENT_PATTERN.exec(event);
72
+ if (!match) return void 0;
73
+ return Number(match[1]) * 1e3;
74
+ }
7
75
 
8
76
  // src/index.ts
9
77
  var SPEC_NAME = "agentsandbox/v1";
10
78
  var SPEC_VERSION = "1.0.0-alpha";
11
79
 
12
- export { SPEC_NAME, SPEC_VERSION };
80
+ export { SPEC_NAME, SPEC_VERSION, createSandboxAgentSessionHost, resolveLifecyclePolicy };
13
81
  //# sourceMappingURL=index.mjs.map
14
82
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;AAYO,IAAM,SAAA,GAAY;AAClB,IAAM,YAAA,GAAe","file":"index.mjs","sourcesContent":["/**\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"]}
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 /** 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\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,47 +1,6 @@
1
- import { z } from 'zod';
2
- import { c as SandboxHandle } from '../types-CCVWFnWl.js';
3
-
4
- /**
5
- * AIP-36 SANDBOX.md frontmatter zod schema.
6
- *
7
- * Generated from `resources/aip-36/draft/SANDBOX.schema.json` via
8
- * json-schema-to-zod. Imported by both `define-sandbox.ts` (TS path
9
- * validation) and `manifest/index.ts` (.md path validation) so every
10
- * field-level constraint runs in both authoring paths from a single
11
- * source of truth — re-run scaffold-aip to refresh after spec changes.
12
- *
13
- * Cross-field rules (if/then/allOf in JSON Schema) don't translate
14
- * cleanly and live in `define-sandbox.ts`'s `validate(def)` instead.
15
- */
16
-
17
- declare const sandboxFrontmatterSchema: z.ZodObject<{
18
- schema: z.ZodOptional<z.ZodLiteral<"sandbox/v1">>;
19
- id: z.ZodOptional<z.ZodString>;
20
- version: z.ZodOptional<z.ZodString>;
21
- provider: z.ZodString;
22
- config: z.ZodRecord<z.ZodString, z.ZodAny>;
23
- limits: z.ZodOptional<z.ZodObject<{
24
- timeout_ms: z.ZodOptional<z.ZodNumber>;
25
- memory_mb: z.ZodOptional<z.ZodNumber>;
26
- cpu_ms: z.ZodOptional<z.ZodNumber>;
27
- }, z.core.$strict>>;
28
- env: z.ZodOptional<z.ZodObject<{
29
- auth: z.ZodOptional<z.ZodAny>;
30
- passthrough: z.ZodDefault<z.ZodArray<z.ZodString>>;
31
- }, z.core.$strict>>;
32
- network: z.ZodOptional<z.ZodObject<{
33
- egress: z.ZodDefault<z.ZodArray<z.ZodString>>;
34
- }, z.core.$strict>>;
35
- mounts: z.ZodDefault<z.ZodArray<z.ZodAny>>;
36
- identity: z.ZodOptional<z.ZodAny>;
37
- lifecycle: z.ZodOptional<z.ZodObject<{
38
- pause_after_idle: z.ZodOptional<z.ZodString>;
39
- destroy_on: z.ZodOptional<z.ZodString>;
40
- }, z.core.$strict>>;
41
- read_only: z.ZodDefault<z.ZodBoolean>;
42
- metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
43
- }, z.core.$strict>;
44
- type SandboxFrontmatter = z.infer<typeof sandboxFrontmatterSchema>;
1
+ import { d as SandboxFrontmatter, b as SandboxHandle } from '../schema-PGQIdcQ3.js';
2
+ export { s as sandboxFrontmatterSchema } from '../schema-PGQIdcQ3.js';
3
+ import 'zod';
45
4
 
46
5
  /**
47
6
  * AIP-36 SANDBOX.md sidecar parser + manifest-to-handle constructor.
@@ -66,4 +25,4 @@ interface SandboxManifest {
66
25
  declare function parseSandboxManifest(source: string): SandboxManifest;
67
26
  declare function sandboxFromManifest(manifest: SandboxManifest): SandboxHandle;
68
27
 
69
- export { type SandboxFrontmatter, type SandboxManifest, parseSandboxManifest, sandboxFromManifest, sandboxFrontmatterSchema };
28
+ export { SandboxFrontmatter, type SandboxManifest, parseSandboxManifest, sandboxFromManifest };
@@ -1,3 +1,5 @@
1
+ import { z } from 'zod';
2
+
1
3
  /**
2
4
  * AIP-36 SandboxDefinition + SandboxHandle.
3
5
  *
@@ -297,4 +299,46 @@ type SandboxRuntimeHandle<TFactory = unknown, TCapabilities extends Record<strin
297
299
  readonly capabilities?: Readonly<TCapabilities>;
298
300
  };
299
301
 
300
- export type { SandboxRuntimeInput as S, SandboxRuntimeHandle as a, SandboxDefinition as b, SandboxHandle as c };
302
+ /**
303
+ * AIP-36 SANDBOX.md frontmatter zod schema.
304
+ *
305
+ * Generated from `resources/aip-36/draft/SANDBOX.schema.json` via
306
+ * json-schema-to-zod. Imported by both `define-sandbox.ts` (TS path
307
+ * validation) and `manifest/index.ts` (.md path validation) so every
308
+ * field-level constraint runs in both authoring paths from a single
309
+ * source of truth — re-run scaffold-aip to refresh after spec changes.
310
+ *
311
+ * Cross-field rules (if/then/allOf in JSON Schema) don't translate
312
+ * cleanly and live in `define-sandbox.ts`'s `validate(def)` instead.
313
+ */
314
+
315
+ declare const sandboxFrontmatterSchema: z.ZodObject<{
316
+ schema: z.ZodOptional<z.ZodLiteral<"sandbox/v1">>;
317
+ id: z.ZodOptional<z.ZodString>;
318
+ version: z.ZodOptional<z.ZodString>;
319
+ provider: z.ZodString;
320
+ config: z.ZodRecord<z.ZodString, z.ZodAny>;
321
+ limits: z.ZodOptional<z.ZodObject<{
322
+ timeout_ms: z.ZodOptional<z.ZodNumber>;
323
+ memory_mb: z.ZodOptional<z.ZodNumber>;
324
+ cpu_ms: z.ZodOptional<z.ZodNumber>;
325
+ }, z.core.$strict>>;
326
+ env: z.ZodOptional<z.ZodObject<{
327
+ auth: z.ZodOptional<z.ZodAny>;
328
+ passthrough: z.ZodDefault<z.ZodArray<z.ZodString>>;
329
+ }, z.core.$strict>>;
330
+ network: z.ZodOptional<z.ZodObject<{
331
+ egress: z.ZodDefault<z.ZodArray<z.ZodString>>;
332
+ }, z.core.$strict>>;
333
+ mounts: z.ZodDefault<z.ZodArray<z.ZodAny>>;
334
+ identity: z.ZodOptional<z.ZodAny>;
335
+ lifecycle: z.ZodOptional<z.ZodObject<{
336
+ pause_after_idle: z.ZodOptional<z.ZodString>;
337
+ destroy_on: z.ZodOptional<z.ZodString>;
338
+ }, z.core.$strict>>;
339
+ read_only: z.ZodDefault<z.ZodBoolean>;
340
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
341
+ }, z.core.$strict>;
342
+ type SandboxFrontmatter = z.infer<typeof sandboxFrontmatterSchema>;
343
+
344
+ export { type SandboxRuntimeInput as S, type SandboxRuntimeHandle as a, type SandboxHandle as b, type SandboxDefinition as c, type SandboxFrontmatter as d, sandboxFrontmatterSchema as s };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentproto/sandbox",
3
- "version": "0.1.0-alpha.0",
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.",
3
+ "version": "0.1.0",
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",
7
7
  "aip-36",
@@ -48,7 +48,10 @@
48
48
  "dependencies": {
49
49
  "gray-matter": "^4.0.3",
50
50
  "zod": "^4.4.3",
51
- "@agentproto/define-doctype": "0.1.0"
51
+ "@agentproto/define-doctype": "0.1.0",
52
+ "@agentproto/secrets": "0.1.0",
53
+ "@agentproto/worktree": "0.2.0",
54
+ "@agentproto/workflow-runtime": "0.3.0"
52
55
  },
53
56
  "devDependencies": {
54
57
  "@types/node": "^25.6.2",