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