AIP-36: SANDBOX.md — agentsandbox/v1 (compute environment policy block)
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.
| Field | Value |
|---|---|
| AIP | 36 |
| Title | SANDBOX.md — agentsandbox/v1 (compute environment policy block) |
| Status | Draft |
| Type | Schema |
| Domain | sandbox.sh |
| Requires | AIP-1, AIP-2, AIP-17 (RUNNER), AIP-19 (SECRETS), AIP-35 (STORAGE) |
Abstract
This AIP defines the sandbox schema block — provider (which
backend), config (provider-specific connection fields),
limits (resource caps), env (variables injected at command
invocation, resolved via SECRETS.md), network (egress
allowlist) — and the defineSandbox(...) standard signature
that consumes it.
sandbox describes a single concern: the compute environment
hosting agent-issued shell commands (execute_command,
get_process_output, kill_process, etc.). It is the sibling of
STORAGE.md (AIP-35), which describes durable file
storage. Together they compose a workspace's runtime (per AIP-34).
The block is composable: any manifest that names a compute
environment MAY embed a sandbox block inline, or MAY reference a
sibling SANDBOX.md file or registry slug.
This is a schema-block AIP, not a file format users always
author standalone. There MAY be <slug>.SANDBOX.md files in a
workspace when the policy is reusable; otherwise the block is
inline in its parent manifest.
Motivation
Sandbox abstractions have appeared independently in multiple ecosystems:
- Mastra ships a
MastraSandboxinterface implemented byLocalSandbox,@mastra/e2b,@mastra/modal,@mastra/daytona,@mastra/blaxel— composed alongsideMastraFilesystemin theWorkspaceconstructor. - The mcp-core registry pattern (referenced impl: a
SandboxProviderAdapterinterface withcreate / connect / isAlive / kill / getUrl / writeFile / readFile / runCommand) — used to host third-party MCP servers in cloud sandboxes. - Subprocess sandboxes with kernel-level isolation (Node
--permissionflags +bwrap) — used to run workspace- authored code with declarative grants.
Each of these is a real, working pattern. They diverge on lifecycle (workspace-lifetime vs per-MCP-connector vs per-invocation), but they share the same provider primitive: a thing you can run a command in, with bounded I/O, network, and resource allowances.
SANDBOX.md extracts the policy declaration into a portable
block that any of these consumers can read. It's the contract
layer; lifecycle stays per-consumer.
Three problems this fixes:
-
Provider enums get incoherent if conflated with storage. Lumping
s3,azure-blob,e2b, andmodalinto oneproviderenum mixes durable backends with ephemeral compute. Splitting clarifies — STORAGE.md is files, SANDBOX.md is commands. -
Lifecycle differs. A storage backend is durable; a sandbox is ephemeral, often with pause/resume semantics. They need different operational verbs.
-
Policy axes differ. Storage policy is about read/write, exclude lists, sync mode. Sandbox policy is about resource caps, allowed network egress, env-var injection. A shared policy block can't model both cleanly.
SANDBOX.md extracts the compute-environment policy into a
portable, reusable block that composes with — but is independent
of — STORAGE.md.
Design principles
-
Inline or ref, mirroring AIP-17 / AIP-19 / AIP-35. Most consumers inline the block. Reusable policies live in their own
<slug>.SANDBOX.mdand are referenced. -
Optional in WORKSPACE.md. A workspace MAY declare no sandbox at all (storage-only workspace). The host MUST tolerate the absence and refuse
execute_command-class tools when no sandbox is configured. -
Provider names are the primary axis. The
providerstring is the first thing consumers branch on. The provider namespace is open: hosts MAY register additional provider ids. -
Config is a typed object per provider. Discriminated union keyed on
provider. NoRecord<string, unknown>opt-out. -
Env never inline. Sandbox env vars MUST resolve through AIP-19 SECRETS.md
auth.ref, not embedded plaintext. Static passthrough (NODE_ENV, DEBUG) is allowed via a separatepassthroughlist. -
Network is allow-list, not deny-list.
network.egressdeclares which hosts the sandbox MAY reach. Empty/missing = no egress. Hosts MUST enforce. -
Limits are advisory but enforceable. Hosts SHOULD enforce
timeout_ms/memory_mb/cpu_mswhen the underlying sandbox primitive supports it. Hosts that can't enforce a cap SHOULD warn at registration.
Specification
The sandbox block
sandbox:
provider: local | mastra-e2b | mastra-modal | mastra-daytona | mastra-blaxel
config:
# provider-specific shape — see "Provider config shapes" below
limits:
timeout_ms: 30000
memory_mb: 1024
cpu_ms: 30000
env:
auth:
ref: ./SECRETS.md # AIP-19 — credentials live there
state: { env: ["OPENAI_API_KEY", "STRIPE_SECRET"] }
passthrough: ["NODE_ENV", "DEBUG"] # static host vars to forward
network:
egress: ["api.openai.com", "*.anthropic.com"]
mounts: # AIP-36 mounts (see below) — optional
- source: workspace # alias for "the parent workspace storage"
at: /workspace
mode: read-write
- source: { ref: "@agentik/python-libs" }
at: /vendor/python-libs
mode: read-only
identity: # AIP-23 identity-ref — optional
ref: "bot://agentik"
lifecycle:
pause_after_idle: idle-600 # AIP-37 event name (or `idle-N` shorthand)
destroy_on: workspace-close # AIP-37 event name
read_only: false # if true, no command execution permittedStandalone SANDBOX.md frontmatter
When the block lives in its own file, the frontmatter adds an
id and version for addressability:
---
schema: sandbox/v1
id: "@<owner-slug>/<sandbox-slug>"
version: 1.0.0
provider: <as above>
config: { ... }
limits: { ... }
env: { ... }
network: { ... }
lifecycle: { ... }
read_only: false
---Embedding in a parent manifest (WORKSPACE.md example)
sandbox:
inline: # exclusive with ref
provider: mastra-e2b
config: { template: "code-interpreter-v3" }
limits: { timeout_ms: 60000, memory_mb: 2048 }
network: { egress: ["api.openai.com"] }
# OR
# ref: ./sandbox/main.SANDBOX.md
# OR
# ref: "@acme-corp/shared-modal-policy"Required fields
| Field | Type | Description |
|---|---|---|
provider | string | Backend kind. See enumerated set + extension rules below. |
config | object | Provider-specific connection fields. Shape varies per provider. |
Optional fields
| Field | Type | Default | Description |
|---|---|---|---|
limits | object | { timeout_ms: 30000 } | Resource caps per command. |
env | object | {} | { auth: { ref, state }, passthrough: [...] }. |
network | object | { egress: [] } | { egress: string[] } — host allowlist. Empty = no egress. |
mounts | array | [] | Filesystems mounted inside the sandbox. See "Mounts" section below. |
identity | object | array | (none) | AIP-23 identity-ref block — owner of the sandbox processes. See AIP-23 identity-ref. |
policy | object | array | (none) | AIP-38 POLICY block — access grants on sandbox actions (sandbox:execute, sandbox:network-egress, etc.). Inline / ref / file. See AIP-38 POLICY.md. |
lifecycle | object | {} | { pause_after_idle_ms?, destroy_on_workspace_close? }. |
read_only | boolean | false | Reject command execution at the sandbox layer. |
metadata | object | {} | Free-form, namespaced. |
Standalone-only fields
| Field | Type | Description |
|---|---|---|
schema | string | Always sandbox/v1. |
id | string | @<owner-slug>/<sandbox-slug>. Globally addressable when reused. |
version | semver string | Spec version of THIS file. |
Provider enumerated set (Day 1)
provider | Implementation | Lifecycle | Notes |
|---|---|---|---|
local | Mastra LocalSandbox (from @mastra/core/workspace) | workspace-lifetime | Dev / self-host. Default in createAgentWorkspace({ enableSandbox: true }). |
mastra-e2b | @mastra/e2b | per-call, fresh | Cloud isolation. |
mastra-modal | @mastra/modal | persistent w/ pause/resume | Long-lived sessions. |
mastra-daytona | @mastra/daytona | persistent | Dev-environments. |
mastra-blaxel | @mastra/blaxel | per-call | Cloud isolation. |
node-permission | Node --permission subprocess + optional bwrap | per-invocation | Per-tool grants for workspace-authored code. |
Hosts MAY register additional providers (e.g. flyio-machines,
cloudflare-containers, custom internal). The registry name MUST
NOT collide with the enumerated set.
On the relationship to existing implementations. The
mcp-core SandboxProviderAdapter (with create / connect / isAlive / kill / getUrl / writeFile / readFile / runCommand) is
prior art that informed this AIP. Where Mastra's MastraSandbox
is available for a given provider, conformant implementations
SHOULD wrap it rather than reimplement. The AIP defines the
declarative contract (what a workspace asks for); the
runtime execution is the consumer's choice — Mastra
sandboxes for the canonical path, custom adapters for cases
Mastra doesn't yet cover.
Provider config shapes
# local
config:
cwd: string # working directory; defaults to workspace root
shell: "/bin/bash" | "/bin/sh" # default: /bin/sh
# mastra-e2b
config:
template: string # e2b template id
api_key_ref: string # SECRETS.md slug for E2B API key
region?: "us-east-1" | "eu-west-1"
# mastra-modal
config:
app_name: string
function_name: string
api_token_ref: string
# mastra-daytona
config:
workspace_id: string
api_key_ref: string
# mastra-blaxel
config:
workspace: string
api_key_ref: stringdefineSandbox standard signature
defineSandbox(definition: SandboxDefinition): SandboxHandle
interface SandboxDefinition {
schema?: "sandbox/v1" // standalone files only
id?: string // standalone files only
version?: string // standalone files only
provider: string
config: Record<string, unknown> // typed per provider; spec'd in this AIP
limits?: {
timeoutMs?: number
memoryMb?: number
cpuMs?: number
}
env?: {
auth?: { ref?: string; state?: { env?: string[] } }
passthrough?: string[]
}
network?: {
egress: string[]
}
lifecycle?: {
pauseAfterIdleMs?: number
destroyOnWorkspaceClose?: boolean
}
readOnly?: boolean
metadata?: Record<string, unknown>
}Conformance rules
-
Inline and ref are mutually exclusive. A consumer manifest embedding the block uses exactly one form per occurrence.
-
Env credentials never inline. Implementations MUST reject sandbox blocks containing plaintext credentials. Use
env.auth.ref→ SECRETS.md per AIP-19. -
Network egress is allow-list. A sandbox with no
networkblock MUST be granted no egress. Validators that silently allow all egress on missing config are non-conformant. -
read_only: trueMUST be enforced. Command execution through a read-only sandbox handle MUST fail with a typed error (sandbox_read_only) before reaching the backend. -
No I/O at parse time. Parsing a SANDBOX.md or sandbox block MUST NOT trigger credential resolution, network calls, or backend instantiation. Materialization is lazy.
-
Lifecycle features are advisory. A
pause_after_idle_msdeclaration on a provider that doesn't support pause/resume (e.g.local,mastra-e2b) MUST be ignored — not rejected.
Mounts (filesystems inside the sandbox)
The mounts block declares which filesystems are accessible from
inside the sandbox at which paths. Maps directly to Mastra's
Workspace.mounts field (since @mastra/core@1.31) — a sandbox
provider that wraps a Mastra workspace passes the resolved mounts
through.
sandbox:
mounts:
- source: workspace # alias = the parent workspace's storage
at: /workspace
mode: read-write
- source: { ref: "@agentik/python-libs" } # AIP-34 workspace ref
at: /vendor/python-libs
mode: read-only
- source: { inline: { provider: cloud-bucket, config: {...} } } # ad-hoc
at: /cache
mode: read-writeEach entry:
| Field | Type | Description |
|---|---|---|
source | string | object | What to mount. "workspace" = the parent workspace's primary storage. Otherwise an inline storage block (per AIP-35) or a ref to a workspace / storage. |
at | string | Absolute path inside the sandbox where the mount appears. Hosts MUST normalise (no .., leading /). |
mode | "read-write" | "read-only" | Default read-write. A read-only mount blocks writes at the sandbox layer. |
Sandbox-relative semantics : the path at is inside the
sandbox process, not the host. Sandboxed processes see the mounts
as regular filesystem paths (e.g. cat /workspace/README.md).
Host-side, the bytes route back through the storage provider that
backs the mount.
Why mounts matter : without a mount, a sandbox like E2B/Modal has only its own ephemeral fs. Bytes written there die when the sandbox dies. Mounts let the sandbox read AND persist into durable workspace storage — closing the loop for github-backed workspaces where the agent commits code from inside the sandbox.
Composition with STORAGE.md
A workspace's runtime is the product of its storage and sandbox declarations:
# WORKSPACE.md
storage:
inline:
provider: github
config: { owner: acme, repo: workspace, branch: main }
sandbox:
inline:
provider: mastra-e2b
config: { template: "code-interpreter-v3" }
network: { egress: ["api.openai.com"] }
mounts:
- source: workspace # mount the github-backed storage
at: /workspace # inside the e2b sandbox
mode: read-writeThe host materializes both; agent-callable file tools route to the
storage filesystem, agent-callable command tools route to the
sandbox. Sandboxed commands write to /workspace → the storage
provider routes those writes through its sync layer (commit + push
per the storage's STORAGE.md.sync policy).
Reference resolution
A ref field accepts three forms (same as STORAGE.md):
- Workspace-relative path:
./sandbox/main.SANDBOX.md - Cross-workspace path:
../shared/team.SANDBOX.md - Registry slug:
@<owner-slug>/<sandbox-slug>
Resolution failures MUST surface as a typed error
(sandbox_ref_unresolvable).
Example — standalone SANDBOX.md
---
schema: sandbox/v1
id: "@acme-corp/shared-modal-policy"
version: 1.0.0
provider: mastra-modal
config:
app_name: agentik-sandbox
function_name: code_runner
api_token_ref: org/acme/modal-token
limits:
timeout_ms: 120000
memory_mb: 4096
env:
auth:
ref: ./SECRETS.md
state: { env: ["OPENAI_API_KEY", "STRIPE_SECRET"] }
passthrough: ["NODE_ENV"]
network:
egress: ["api.openai.com", "api.stripe.com"]
lifecycle:
pause_after_idle_ms: 300000
destroy_on_workspace_close: false
read_only: false
---
## Description
Shared Modal sandbox for Acme's analytics workspaces. Pauses after
5 minutes idle to control cost; reuses the org's Modal API token.Security considerations
SANDBOX.md is declarative: a malicious manifest can claim
any provider or config. Hosts MUST validate:
env.auth.refresolves to a SECRETS.md the workspace's owner is authorised to reveal.network.egressentries are on an allow-list of permitted destinations under workspace policy.provideris registered in the host's provider registry; unknown providers MUST be rejected, never silently treated as a default.
Sandbox providers vary widely in their isolation guarantees.
Hosts SHOULD document which providers are considered "trusted"
under their policy framework. A local provider grants
host-process access — granting it to an untrusted manifest is a
sandbox escape by definition.
Open questions
-
Multi-sandbox per workspace. Should a workspace declare multiple sandboxes (e.g.
python+nodeseparately)? Defer until concrete need. -
Sandbox-to-storage mount syntax.Resolved by themountsblock above (added 2026-05-03 per AIP-36 v1.1). -
Cost / quota declarations. Sandbox usage is metered by most cloud providers. Whether limits include a cost cap (
max_cost_units_per_run) is open.
See also
- AIP-17 — RUNNER.md — same composition pattern
- AIP-19 — SECRETS.md — referenced from
env.auth.ref - AIP-23 — IDENTITY.md — referenced from
identity? - AIP-26 — CODE.md — same composition pattern (inline / ref)
- AIP-34 — WORKSPACE.md — the primary consumer
- AIP-35 — STORAGE.md — sibling primitive (filesystem)
- AIP-37 — LIFECYCLE.md — event vocabulary referenced by
lifecycle.{pause_after_idle, destroy_on}
Resources
Supporting artifacts for AIP-36. Links open the file on GitHub — markdown and JSON render natively in GitHub's viewer. Browse the full resource tree →
AIP-35: STORAGE.md — agentstorage/v1 (storage policy block)
A composable schema block defining the `storage` field — provider, config, sync semantics, auth ref, exclude rules — for any manifest that names a backing store. Reused by WORKSPACE.md (AIP-34) and any future manifest that names persistent state. Inline or ref, mirroring AIP-17 RUNNER and AIP-19 SECRETS.
AIP-37: LIFECYCLE.md — agentlifecycle/v1 (event vocabulary)
A vocabulary AIP defining the standard lifecycle event names that hosts fire and that policy blocks (storage sync, sandbox lifecycle, etc.) reference. Not a runtime — just a shared language so configs across hosts mean the same thing.