@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 +26 -0
- package/dist/index.d.ts +117 -3
- package/dist/index.mjs +70 -2
- package/dist/index.mjs.map +1 -1
- package/dist/manifest/index.d.ts +4 -45
- package/dist/{types-CCVWFnWl.d.ts → schema-PGQIdcQ3.d.ts} +45 -1
- package/package.json +6 -3
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 './
|
|
2
|
-
export {
|
|
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
|
package/dist/index.mjs.map
CHANGED
|
@@ -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":";;;;;;;;AA2FA,eAAsB,8BACpB,IAAA,EACkC;AAClC,EAAA,MAAM,GAAA,GAAM,MAAM,wBAAA,CAAyB,IAAA,CAAK,OAAO,CAAA;AACvD,EAAA,IAAI,MAAA;AACJ,EAAA,IAAI,IAAA,CAAK,cAAc,MAAA,EAAW;AAChC,IAAA,IAAI,CAAC,IAAA,CAAK,QAAA,CAAS,OAAA,EAAS;AAC1B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,CAAA,4DAAA,EAA+D,KAAK,SAAS,CAAA,8EAAA;AAAA,OAE/E;AAAA,IACF;AACA,IAAA,MAAA,GAAS,MAAM,IAAA,CAAK,QAAA,CAAS,OAAA,CAAQ,IAAA,CAAK,WAAW,IAAA,CAAK,IAAA,EAAM,EAAE,GAAA,EAAK,CAAA;AAAA,EACzE,CAAA,MAAO;AACL,IAAA,MAAA,GAAS,MAAM,KAAK,QAAA,CAAS,IAAA,CAAK,KAAK,IAAA,EAAM,EAAE,KAAK,CAAA;AAAA,EACtD;AACA,EAAA,IAAI,IAAA;AACJ,EAAA,IAAI;AACF,IAAA,IAAA,GAAO,MAAM,6BAAA,CAA8B,EAAE,GAAA,EAAK,MAAA,CAAO,QAAQ,CAAA;AAAA,EACnE,SAAS,GAAA,EAAK;AACZ,IAAA,MAAM,OAAO,IAAA,EAAK;AAClB,IAAA,MAAM,GAAA;AAAA,EACR;AACA,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,WAAW,MAAA,CAAO,SAAA;AAAA,IAClB,MAAM,IAAA,GAAsB;AAC1B,MAAA,MAAM,KAAK,KAAA,EAAM;AACjB,MAAA,MAAM,OAAO,IAAA,EAAK;AAAA,IACpB,CAAA;AAAA,IACA,GAAI,OAAO,KAAA,GACP;AAAA,MACE,MAAM,KAAA,GAAuB;AAC3B,QAAA,MAAM,KAAK,KAAA,EAAM;AACjB,QAAA,MAAM,OAAO,KAAA,EAAO;AAAA,MACtB;AAAA,QAEF;AAAC,GACP;AACF;AAEA,IAAM,yBAAA,GAA4C,CAAA,IAAA,KAAQ,OAAA,CAAQ,GAAA,CAAI,IAAI,CAAA,IAAK,IAAA;AAG/E,eAAe,yBACb,MAAA,EACiC;AACjC,EAAA,MAAM,QAAA,GAAW,OAAO,QAAA,IAAY,yBAAA;AACpC,EAAA,MAAM,MAA8B,EAAC;AACrC,EAAA,KAAA,MAAW,IAAA,IAAQ,OAAO,KAAA,EAAO;AAC/B,IAAA,MAAM,KAAA,GAAQ,MAAM,QAAA,CAAS,IAAI,CAAA;AACjC,IAAA,IAAI,KAAA,KAAU,IAAA,IAAQ,KAAA,KAAU,MAAA,EAAW;AACzC,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,kDAAkD,IAAI,CAAA,yFAAA;AAAA,OAExD;AAAA,IACF;AACA,IAAA,qBAAA,CAAsB,MAAM,KAAK,CAAA;AACjC,IAAA,GAAA,CAAI,IAAI,CAAA,GAAI,KAAA;AAAA,EACd;AACA,EAAA,OAAO,GAAA;AACT;;;AClIA,IAAM,kBAAA,GAAqB,cAAA;AAUpB,SAAS,sBAAA,CAAuB,MAAqB,KAAA,EAAwC;AAClG,EAAA,IAAI,KAAK,SAAA,EAAW,UAAA,EAAY,OAAO,EAAE,UAAU,MAAA,EAAO;AAE1D,EAAA,MAAM,gBAAA,GAAmB,gBAAA,CAAiB,IAAA,CAAK,SAAA,EAAW,gBAAgB,CAAA;AAC1E,EAAA,MAAM,QAAA,GAA6B,KAAA,IAAS,gBAAA,KAAqB,MAAA,GAAY,OAAA,GAAU,MAAA;AACvF,EAAA,OAAO,EAAE,UAAU,GAAI,gBAAA,KAAqB,SAAY,EAAE,gBAAA,EAAiB,GAAI,EAAC,EAAG;AACrF;AAEA,SAAS,iBAAiB,KAAA,EAA+C;AACvE,EAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,EAAA,MAAM,KAAA,GAAQ,kBAAA,CAAmB,IAAA,CAAK,KAAK,CAAA;AAC3C,EAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,EAAA,OAAO,MAAA,CAAO,KAAA,CAAM,CAAC,CAAC,CAAA,GAAI,GAAA;AAC5B;;;ACjCO,IAAM,SAAA,GAAY;AAClB,IAAM,YAAA,GAAe","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"]}
|
package/dist/manifest/index.d.ts
CHANGED
|
@@ -1,47 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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",
|