iii-sandbox
v0.17.0-next.1Spawn ephemeral microVMs and expose 14 sandbox::* triggers (lifecycle + filesystem) for isolated command execution and file ops.
full markdown
/workers/iii-sandbox.md?version=0.17.0-next.1. paste it into an llm prompt or pipe it through curl from a worker.install
configuration
- auto_install: true
default_cpus: 1
default_idle_timeout_secs: 300
default_memory_mb: 512
image_allowlist:
- python
- node
max_concurrent_sandboxes: 32dependencies
readme
iii-sandbox
Spawn ephemeral microVMs from worker code or the terminal. The daemon registers 14 sandbox::* triggers — 4 lifecycle ops plus 10 filesystem ops — every one called via iii.trigger(). Each sandbox boots in a few hundred milliseconds, runs commands isolated from the host, and is reaped when idle. The overlay filesystem is discarded on stop.
Implementation:
crates/iii-worker/src/sandbox_daemon/. Ships inside theiii-workerbinary; the engine starts the daemon asiii-worker sandbox-daemonwheniii-sandboxappears inconfig.yaml.
Use it for: running untrusted code, AI-agent tool calls, one-shot scripts, per-request isolation.
Don't use it for: long-lived services (use a regular worker), durable stateful tasks (overlay is wiped on stop).
Agent Quickstart
If you are an AI agent calling sandbox::*, start here. Two paths:
One-call workflow (recommended): sandbox::run
Boots a VM, drops your code in /tmp/run.{ext}, runs the interpreter, captures
stdout/stderr, and stops the VM. One call, one response.
{
"trigger": "sandbox::run",
"payload": {
"image": "node",
"code": "console.log('hello from sandbox')",
"lang": "node"
}
}lang accepts "node", "python", "shell", or a custom interpreter binary path.
Pass keep_sandbox: true if you want to keep the VM alive to inspect afterwards.
Surgical workflow: sandbox::create -> sandbox::fs::write -> sandbox::exec -> sandbox::stop
Use this when you need fine-grained control over multiple operations on one sandbox.
// 1. boot
{ "trigger": "sandbox::create", "payload": { "image": "node" } }
// 2. write a file (content accepts a UTF-8 string directly)
{ "trigger": "sandbox::fs::write", "payload": {
"sandbox_id": "<uuid>", "path": "/home/app/main.js",
"content": "console.log('hi')\n"
} }
// 3. run it (3 valid cmd shapes — pick whichever you like)
{ "trigger": "sandbox::exec", "payload": {
"sandbox_id": "<uuid>", "cmd": "node", "args": ["/home/app/main.js"]
} }
// 4. tear down
{ "trigger": "sandbox::stop", "payload": { "sandbox_id": "<uuid>" } }Three accepted cmd shapes for sandbox::exec
{ "cmd": "node /home/app/main.js" } // shell-line, shlex-split
{ "cmd": "node", "args": ["/home/app/main.js"] } // classic POSIX argv
{ "argv": ["node", "/home/app/main.js"] } // single argv arrayShlex is not bash — cmd: "echo $HOME && pwd" won't expand variables or chain
commands. Use sandbox::run with lang: "shell" for bash semantics.
Two accepted env shapes for sandbox::create, sandbox::exec, sandbox::run
{ "env": ["FOO=bar", "PATH=/usr/bin"] } // wire shape (legacy)
{ "env": { "FOO": "bar", "PATH": "/usr/bin" } } // agent-natural shapeError responses carry a self-healing payload
Every error returns JSON encoded inside error.message (see SandboxErrorWire docs).
Parse it once:
const detail = JSON.parse(err.message);
// detail.code, detail.type, detail.message, detail.docs_url, detail.retryable
// detail.fix — ready-to-send next-call payload, null if not auto-fixable
// detail.fix_note — one-liner explaining why fix is nullIf detail.fix is non-null, your next call is await fn(detail.fix). The error IS
the recovery path.
Error codes (anchors below)
Host requirements
Sandboxes run as libkrun microVMs and need hardware virtualization on the host:
- macOS: Apple Silicon (M-series). Intel Macs can't boot sandboxes.
- Linux:
/dev/kvmreadable by the engine process. - Windows: unsupported.
Hosts without hardware virtualization will fail sandbox::create with error S300 and a stderr tail from the failed VM process. See S300 in docs/api-reference/sandbox.mdx for the full diagnostic flow.
Sample Configuration
- name: iii-sandbox
config:
auto_install: true
image_allowlist:
- python
- node
default_idle_timeout_secs: 300
max_concurrent_sandboxes: 32
default_cpus: 1
default_memory_mb: 512iii worker add iii-sandbox appends this block to your config.yaml. Trim or extend image_allowlist and custom_images to control what callers can boot.
Configuration
| Field | Type | Default | Description |
|---|---|---|---|
auto_install |
boolean | true |
Pull the image from its OCI ref on first use when the rootfs isn't cached. Set false in air-gapped or pre-provisioned deployments — callers get S101 and operators pre-pull with iii worker add iiidev/. |
image_allowlist |
string[] | [] |
Fail-closed list of image names that may be booted. Entries must be preset names (python, node) or keys from custom_images. Empty list denies everything — sandbox::create returns S100 for every request. |
default_idle_timeout_secs |
number | 300 |
Reap a sandbox when now - last_exec_at exceeds this. The reaper runs every 10 s. Per-request idle_timeout_secs on sandbox::create overrides. |
max_concurrent_sandboxes |
number | 32 |
Hard cap on live sandboxes. The 33rd concurrent sandbox::create returns S400. Size by host RAM (default RAM per sandbox × cap ≤ available RAM). |
default_cpus |
number | 1 |
vCPUs per sandbox when the request omits cpus. |
default_memory_mb |
number | 512 |
RAM ceiling per sandbox when the request omits memory_mb. |
per_image_caps |
map | {} |
Per-image hard caps. Each value is { max_cpus: N, max_memory_mb: N }. Requests exceeding a cap return S400. |
custom_images |
map | {} |
Deployment-specific images beyond the built-in presets. Map key is the name used in image_allowlist and the image field on sandbox::create; value is a fully-qualified OCI reference (e.g. ghcr.io/acme/my-app:1.2.3). Preset names (python, node) are reserved. See docs/api-reference/sandbox.mdx. |
Triggers
All 14 triggers are dispatched via iii.trigger({ function_id, payload, timeoutMs }). Recommended timeoutMs is in each table; lifecycle ops have meaningful timeout pressure, filesystem ops generally don't (the daemon is local).
Lifecycle (4)
sandbox::create
Boot a microVM and return a sandbox_id. Recommended timeoutMs: 300_000 (cold pull can take 5-30 s).
| Field | Type | Default | Description |
|---|---|---|---|
image |
string | required | Preset (python, node) or custom_images key. |
cpus |
number | default_cpus |
vCPUs. Capped by per_image_caps. |
memory_mb |
number | default_memory_mb |
RAM ceiling. Capped by per_image_caps. |
name |
string | none | Human label for sandbox::list. |
network |
boolean | false |
Enable guest networking. |
idle_timeout_secs |
number | default_idle_timeout_secs |
Override the per-sandbox idle reaper. |
env |
string[] | [] |
K=V entries injected into the guest. |
Returns: { sandbox_id, image }.
sandbox::exec
Run a command inside a live sandbox. Recommended timeoutMs: 35_000 (daemon's 30 s default + 5 s margin).
| Field | Type | Default | Description |
|---|---|---|---|
sandbox_id |
string (UUID) | required | From sandbox::create. |
cmd |
string | required | Executable name. |
args |
string[] | [] |
argv tail. |
stdin |
string (base64) | none | Bytes piped to the process's stdin. |
env |
string[] | [] |
K=V entries merged on top of the boot env. |
timeout_ms |
number | 300_000 |
Per-exec deadline enforced inside the daemon. Sized for cold npm install / pip install / cargo build; pass a smaller value for probes and version checks. |
workdir |
string | guest home | Working directory. |
Returns: { stdout, stderr, exit_code, timed_out, duration_ms, success }.
sandbox::list
Enumerate active sandboxes. Empty payload ({}). Returns an array of { sandbox_id, image, name, status, created_at, last_exec_at }.
sandbox::stop
Tear down a sandbox and reclaim resources.
| Field | Type | Default | Description |
|---|---|---|---|
sandbox_id |
string (UUID) | required | Sandbox to stop. |
wait |
boolean | false |
Block until the VM process has exited. |
Returns: { sandbox_id, stopped }.
Filesystem (10)
All filesystem triggers take sandbox_id (UUID) plus operation-specific fields. Bumping the sandbox's idle clock is automatic — fs activity counts as liveness. Errors return S2xx.
sandbox::fs::ls
| Field | Type | Description |
|---|---|---|
sandbox_id |
string | Sandbox to operate in. |
path |
string | Directory to list. |
Returns: { entries: FsEntry[] } where each entry has { name, is_dir, size, mode, mtime, is_symlink }.
sandbox::fs::stat
| Field | Type | Description |
|---|---|---|
sandbox_id |
string | Sandbox to operate in. |
path |
string | Path to inspect. |
Returns: { name, is_dir, size, mode, mtime, is_symlink }.
sandbox::fs::mkdir
| Field | Type | Default | Description |
|---|---|---|---|
sandbox_id |
string | required | Sandbox to operate in. |
path |
string | required | Directory to create. |
mode |
string | "0755" |
Octal permissions. |
parents |
boolean | false |
Create intermediate directories (mkdir -p). |
Returns: { created: boolean }.
sandbox::fs::write
Stream a file into the sandbox. The content field is a StreamChannelRef that the caller writes to (channel-paced — no envelope timeout cap).
| Field | Type | Default | Description |
|---|---|---|---|
sandbox_id |
string | required | Sandbox to operate in. |
path |
string | required | Destination path inside the guest. |
mode |
string | "0644" |
Octal permissions for the new file. |
parents |
boolean | false |
Create missing parent directories. |
content |
StreamChannelRef | required | Channel handle the caller writes bytes to. |
Returns: { bytes_written, path }.
sandbox::fs::read
Read a file out of the sandbox. Always returns a content: StreamChannelRef the caller can subscribe to for the full file bytes. For UTF-8 text files under 1 MiB, the response also includes an inline body: string so callers can short-circuit the channel subscription and use the body directly.
| Field | Type | Description |
|---|---|---|
sandbox_id |
string | Sandbox to operate in. |
path |
string | Path to read. |
Returns: { content: StreamChannelRef, body?: string, size, mode, mtime }.
contentis always populated. The same bytes are delivered through it whether or notbodyis also set, so peers that statically typecontentasStreamChannelRefkeep working unchanged.bodyis present (Some) for files that fit in the 1 MiB inline cap and decode cleanly as UTF-8. Absent (None) for large or binary files — read those throughcontentinstead.
sandbox::fs::rm
| Field | Type | Default | Description |
|---|---|---|---|
sandbox_id |
string | required | Sandbox to operate in. |
path |
string | required | Path to remove. |
recursive |
boolean | false |
Recurse into directories (rm -r). |
Returns: { removed: boolean }.
sandbox::fs::chmod
| Field | Type | Default | Description |
|---|---|---|---|
sandbox_id |
string | required | Sandbox to operate in. |
path |
string | required | Target path. |
mode |
string | required | Octal permissions (e.g. "0644"). |
uid |
number | unchanged | New owner UID. |
gid |
number | unchanged | New group GID. |
recursive |
boolean | false |
Apply recursively to a directory tree. |
Returns: { updated: number } — count of paths changed.
sandbox::fs::mv
| Field | Type | Default | Description |
|---|---|---|---|
sandbox_id |
string | required | Sandbox to operate in. |
src |
string | required | Source path. |
dst |
string | required | Destination path. |
overwrite |
boolean | false |
Allow overwriting an existing destination. |
Returns: { moved: boolean }.
sandbox::fs::grep
Recursive regex search.
| Field | Type | Default | Description |
|---|---|---|---|
sandbox_id |
string | required | Sandbox to operate in. |
path |
string | required | Root path. |
pattern |
string | required | Regex (RE2 syntax). |
recursive |
boolean | true |
Walk subdirectories. |
ignore_case |
boolean | false |
Case-insensitive match. |
include_glob |
string[] | [] |
Gitignore-style include filter. |
exclude_glob |
string[] | [] |
Gitignore-style exclude filter. |
max_matches |
number | 10000 |
Stop after N matches. |
max_line_bytes |
number | 4096 |
Truncate any single line longer than this. |
Returns: { matches: FsMatch[], truncated } where each match has { path, line_no, byte_offset, line }.
sandbox::fs::sed
Find-and-replace across files. Pass either files (explicit list) or path (walk like grep) — exactly one. Passing both, or neither, returns S210.
| Field | Type | Default | Description |
|---|---|---|---|
sandbox_id |
string | required | Sandbox to operate in. |
files |
string[] | [] |
Explicit list of paths. Mutually exclusive with path. |
path |
string | none | Root path to walk. Mutually exclusive with files. |
recursive |
boolean | true |
Walk subdirectories. Only meaningful with path. |
include_glob |
string[] | [] |
Include filter (only with path). |
exclude_glob |
string[] | [] |
Exclude filter (only with path). |
pattern |
string | required | Regex or literal (see regex flag). |
replacement |
string | required | Replacement string. |
regex |
boolean | true |
Treat pattern as a regex; false for literal. |
first_only |
boolean | false |
Replace only the first match in each file. |
ignore_case |
boolean | false |
Case-insensitive match. |
Returns: { results: FsSedFileResult[], total_replacements }.
Example: create → exec → stop
import { registerWorker } from 'iii-sdk'
const iii = registerWorker('ws://127.0.0.1:49134')
const { sandbox_id } = await iii.trigger({
function_id: 'sandbox::create',
payload: { image: 'python', cpus: 1, memory_mb: 512 },
timeoutMs: 300_000,
})
const out = await iii.trigger({
function_id: 'sandbox::exec',
payload: { sandbox_id, cmd: 'python3', args: ['-c', 'print(2 + 2)'] },
timeoutMs: 35_000,
})
console.log(out.stdout) // "4\n"
await iii.trigger({
function_id: 'sandbox::stop',
payload: { sandbox_id, wait: true },
})from iii import register_worker
iii = register_worker("ws://127.0.0.1:49134")
result = await iii.trigger({
"function_id": "sandbox::create",
"payload": {"image": "python", "cpus": 1, "memory_mb": 512},
"timeout_ms": 300_000,
})
sandbox_id = result["sandbox_id"]
out = await iii.trigger({
"function_id": "sandbox::exec",
"payload": {"sandbox_id": sandbox_id, "cmd": "python3", "args": ["-c", "print(2 + 2)"]},
"timeout_ms": 35_000,
})
print(out["stdout"]) # "4\n"
await iii.trigger({
"function_id": "sandbox::stop",
"payload": {"sandbox_id": sandbox_id, "wait": True},
})CLI
The iii sandbox subcommands wrap a curated subset of the lifecycle and fs surface:
iii sandbox run <image> -- <cmd> [args...] # one-shot create+exec+stop
iii sandbox create <image> [--idle-timeout N] # boot, print sandbox_id
iii sandbox exec <sandbox_id> -- <cmd> ... # exec into a live sandbox
iii sandbox list
iii sandbox stop <sandbox_id>
iii sandbox upload <sandbox_id> <local> <remote>
iii sandbox download <sandbox_id> <remote> <local>Anything outside this set (e.g. fs::grep, fs::sed, fs::chmod) is reachable only via iii.trigger() from worker code.
Errors
The daemon returns typed SandboxErrors with S-codes embedded in the wire
payload ({type, code, message, docs_url, fix, fix_note, retryable}).
error.docs_url resolves to one of the anchors below. The anchor IDs are
case-sensitive HTML anchors so a regression test
(sandbox_docs_anchor_stability) can verify each SandboxErrorCode has a
documented home.
Request validation
S001 — invalid request
Bad UUID, missing required field, ambiguous cmd/args/argv combo, invalid
env var name. Check error.message for the specific cause.
S002 — sandbox not found
The sandbox_id is well-formed but no sandbox with that id exists. Call
sandbox::create first.
S003 — concurrent exec
Another exec is already in flight on this sandbox. Await it before submitting another.
S004 — sandbox already stopped
The sandbox was reaped or explicitly stopped. Create a new one.
Image catalog
S100 — image not in catalog
The image value isn't a built-in preset (python, node) and isn't a key in
sandbox.custom_images of iii.config.yaml. Either pick a known preset or add
a custom_images entry.
S101 — rootfs missing
The image is in the catalog but the rootfs isn't on disk. Operator action:
run iii worker add on the host.
S102 — auto-install failed (transient)
Pull or extract of the image bundle failed. Often transient — retry.
Exec runtime
S200 — exec timed out
The timeout_ms window elapsed before the binary exited. Raise the timeout or
break the work into smaller steps.
Filesystem
S210 — fs invalid request
Mutually exclusive fields, missing required field, unsupported operation
combination. Check error.message.
S211 — file not found
S212 — wrong file type (expected file, found directory, or vice versa)
S213 — already exists
S214 — directory not empty
S215 — permission denied
S216 — fs i/o error
S217 — invalid regex pattern
S218 — fs channel aborted (transient)
The streaming channel closed before the operation completed. Retry.
S219 — fs operation unsupported
The sandbox supervisor inside the VM is too old to implement this fs op. Upgrade iii-worker.
Platform
S300 — VM boot failed
The microVM couldn't boot. Almost always missing virtualization on the host
(no /dev/kvm, Intel Mac, Windows). Check the stderr tail in error.message.
S400 — resource limit exceeded
Capacity bound (max_concurrent_sandboxes, per-image caps) reached.
See also
docs/api-reference/sandbox.mdx— full payload reference, S-code diagnostics, custom images, environment variables, troubleshooting.docs/how-to/developing-sandbox-workers— how worker processes themselves run inside isolated microVMs (different topic from this trigger surface).
api reference (json)
{
"functions": [
{
"description": "List bootable images: bundled presets plus operator-registered custom_images. Call this before sandbox::create when you don't know what's available.",
"metadata": {},
"name": "sandbox::catalog::list",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "`sandbox::catalog::list` request. No fields — kept as a struct (not `()`) so adding optional filters (e.g. `include_oci_refs: bool`) later is a non-breaking serde change.",
"title": "CatalogListRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"CatalogEntry": {
"description": "One row in the catalog response. `name` is the catalog key that callers pass to `sandbox::create` (or `sandbox::run`); `oci_ref` is the fully-qualified OCI reference the daemon will pull. `kind` lets agents distinguish bundled presets from operator-registered custom images without having to maintain their own preset allowlist.",
"properties": {
"kind": {
"$ref": "#/definitions/CatalogEntryKind"
},
"name": {
"type": "string"
},
"oci_ref": {
"type": "string"
}
},
"required": [
"kind",
"name",
"oci_ref"
],
"type": "object"
},
"CatalogEntryKind": {
"oneOf": [
{
"description": "Bundled with the daemon binary. Stable identifier across releases.",
"enum": [
"preset"
],
"type": "string"
},
{
"description": "Operator-registered under `sandbox.custom_images` in `iii.config.yaml`. Specific to this deployment.",
"enum": [
"custom"
],
"type": "string"
}
]
}
},
"properties": {
"images": {
"description": "Every image the daemon will accept on `sandbox::create.image`. Presets come first in stable order; custom entries follow, sorted by `name` for determinism so an agent that diffs two `catalog::list` responses sees stable output.",
"items": {
"$ref": "#/definitions/CatalogEntry"
},
"type": "array"
}
},
"required": [
"images"
],
"title": "CatalogListResponse",
"type": "object"
}
},
{
"description": "Create an ephemeral sandbox VM. `image` must be a preset (`\"python\"`, `\"node\"`) or a `custom_images` key from iii.config.yaml; OCI refs are NOT accepted unless they match a catalog key. `env` accepts both `Vec<\"K=V\">` and `{ K: V }` map shapes.",
"metadata": {},
"name": "sandbox::create",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"EnvShape": {
"anyOf": [
{
"description": "Original wire shape: `[\"FOO=bar\", \"PATH=/usr/bin\"]`.",
"items": {
"type": "string"
},
"type": "array"
},
{
"additionalProperties": {
"type": "string"
},
"description": "Agent-natural shape: `{ \"FOO\": \"bar\", \"PATH\": \"/usr/bin\" }`. Iteration is sorted by key (BTreeMap) so two callers passing the same map get the same env-var ordering.",
"type": "object"
}
],
"description": "Environment-variable input. Agents naturally pass `{ FOO: \"bar\" }` (matching Docker/npm/k8s mental models); the original wire shape was `Vec<\"K=V\">`. The untagged enum accepts both; `into_kv_vec()` normalises to the canonical `Vec<String>` the runner expects.\n\n`Default` is the empty vec form (the historical wire shape)."
}
},
"examples": [
{
"env": {
"NODE_ENV": "production"
},
"idle_timeout_secs": 600,
"image": "node",
"memory_mb": 512
}
],
"properties": {
"cpus": {
"default": null,
"description": "vCPU count; daemon/image default applies when omitted.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"env": {
"allOf": [
{
"$ref": "#/definitions/EnvShape"
}
],
"description": "Environment entries injected into the VM. Accepts either a `Vec<\"K=V\">` (original wire shape) or a `{ K: V }` map. `handle_create` normalises to the `Vec<String>` shape the boot path expects before invoking the launcher."
},
"idle_timeout_secs": {
"default": null,
"description": "Auto-stop the VM after this many seconds of inactivity; daemon default applies when omitted.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"image": {
"description": "Catalog name of the image to boot. Bundled presets are `\"python\"` and `\"node\"`; pass either string verbatim. The only other accepted values are the literal keys of `sandbox.custom_images` in `iii.config.yaml` — set by the operator. Do NOT pass an OCI ref like `\"ghcr.io/iii-hq/node:latest\"` or `\"docker.io/library/node:20\"` unless that exact string is the catalog key. Unknown values return S100 with the allowed set in the error message.",
"type": "string"
},
"memory_mb": {
"default": null,
"description": "Memory cap in MiB; daemon/image default applies when omitted.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"name": {
"default": null,
"description": "Human label surfaced by `sandbox::list`; not an identifier.",
"type": [
"string",
"null"
]
},
"network": {
"default": null,
"description": "Whether the VM gets outbound networking; daemon default when omitted.",
"type": [
"boolean",
"null"
]
}
},
"required": [
"image"
],
"title": "CreateRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"image": {
"type": "string"
},
"sandbox_id": {
"type": "string"
}
},
"required": [
"image",
"sandbox_id"
],
"title": "CreateResponse",
"type": "object"
}
},
{
"description": "Execute a command inside a live sandbox. `cmd` accepts three shapes: (1) a shell-style line that gets shlex-split (`cmd: \"node -v\"`), (2) `cmd` + `args` (the POSIX shape, `cmd: \"node\", args: [\"-v\"]`), (3) an `argv` array (`argv: [\"node\", \"-v\"]`). Shell metacharacters in the shell-line shape are NOT interpreted; use `sandbox::run` with `lang: \"shell\"` for bash semantics. `env` accepts both `Vec<\"K=V\">` and `{ K: V }` map shapes.",
"metadata": {},
"name": "sandbox::exec",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"EnvShape": {
"anyOf": [
{
"description": "Original wire shape: `[\"FOO=bar\", \"PATH=/usr/bin\"]`.",
"items": {
"type": "string"
},
"type": "array"
},
{
"additionalProperties": {
"type": "string"
},
"description": "Agent-natural shape: `{ \"FOO\": \"bar\", \"PATH\": \"/usr/bin\" }`. Iteration is sorted by key (BTreeMap) so two callers passing the same map get the same env-var ordering.",
"type": "object"
}
],
"description": "Environment-variable input. Agents naturally pass `{ FOO: \"bar\" }` (matching Docker/npm/k8s mental models); the original wire shape was `Vec<\"K=V\">`. The untagged enum accepts both; `into_kv_vec()` normalises to the canonical `Vec<String>` the runner expects.\n\n`Default` is the empty vec form (the historical wire shape)."
}
},
"examples": [
{
"args": [
"/home/app/index.js"
],
"cmd": "node",
"env": {
"NODE_ENV": "production"
},
"sandbox_id": "00000000-0000-0000-0000-000000000000",
"timeout_ms": 300000
}
],
"properties": {
"args": {
"default": [],
"description": "Argv tail passed to `cmd` (each entry is one argv slot).",
"items": {
"type": "string"
},
"type": "array"
},
"argv": {
"default": [],
"description": "Alternative input shape: a single argv array where the first element is the binary and the rest are arguments. Mutually exclusive with `cmd` having whitespace OR `args` being set.",
"items": {
"type": "string"
},
"type": "array"
},
"cmd": {
"default": "",
"description": "The binary to execute. Accepts three shapes (`handle_exec` picks one and normalises `cmd`/`args` before passing to the runner):\n\n1. **Shell line**: `cmd: \"node /home/app/index.js\"`. If `cmd` contains whitespace AND `args` is empty AND `argv` is empty, `cmd` is shlex-split into `(head, tail)` and `head` becomes the binary while `tail` becomes the argv. 2. **cmd + args**: `cmd: \"node\", args: [\"-v\"]`. The classic POSIX shape; unchanged from earlier versions. 3. **argv array**: `argv: [\"node\", \"/home/app/index.js\"]`. Wins over `cmd`/`args` if non-empty; the first element is the binary, the rest are arguments.\n\nShlex is NOT bash. Shell metacharacters (`;`, `|`, `&&`, `>`, pipes, redirects, variable expansion) inside the shell-line shape are split as text, not interpreted. Use `sandbox::run` with `lang: \"shell\"` if you need bash semantics.",
"type": "string"
},
"env": {
"allOf": [
{
"$ref": "#/definitions/EnvShape"
}
],
"description": "Environment entries added to the child. Accepts either a `Vec<\"K=V\">` (the original wire shape) or a `{ K: V }` map. `handle_exec` normalises to `Vec<String>` before invoking the runner."
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
},
"stdin": {
"default": null,
"description": "Base64-encoded bytes piped to the child's stdin.",
"type": [
"string",
"null"
]
},
"timeout_ms": {
"default": null,
"description": "Kill-after window in ms. Defaults to 300_000 (5 minutes) — sized for cold `npm install` / `pip install` / `cargo build`. Pass a smaller value (e.g. 10_000) for probes and version checks.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"workdir": {
"default": null,
"description": "Working directory inside the sandbox; image default when omitted.",
"type": [
"string",
"null"
]
}
},
"required": [
"sandbox_id"
],
"title": "ExecRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"duration_ms": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"exit_code": {
"format": "int32",
"type": [
"integer",
"null"
]
},
"stderr": {
"type": "string"
},
"stdout": {
"type": "string"
},
"success": {
"type": "boolean"
},
"timed_out": {
"type": "boolean"
}
},
"required": [
"duration_ms",
"stderr",
"stdout",
"success",
"timed_out"
],
"title": "ExecResponse",
"type": "object"
}
},
{
"description": "Change file permissions (and optionally owner) inside a sandbox. Example: { sandbox_id: \"...\", path: \"/home/app/script.sh\", mode: \"0755\" }",
"metadata": {},
"name": "sandbox::fs::chmod",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"mode": "0755",
"path": "/home/app/script.sh",
"sandbox_id": "00000000-0000-0000-0000-000000000000"
}
],
"properties": {
"gid": {
"default": null,
"description": "Optional GID to chown to. Pair with `uid` for a full chown.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
},
"mode": {
"description": "Octal permissions (e.g. `\"0644\"`, `\"0755\"`).",
"type": "string"
},
"path": {
"description": "Absolute path to modify inside the sandbox guest.",
"type": "string"
},
"recursive": {
"default": false,
"description": "Apply recursively to all entries under `path`.",
"type": "boolean"
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
},
"uid": {
"default": null,
"description": "Optional UID to chown to. Pair with `gid` for a full chown.",
"format": "uint32",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"required": [
"mode",
"path",
"sandbox_id"
],
"title": "ChmodRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"updated": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"updated"
],
"title": "ChmodResponse",
"type": "object"
}
},
{
"description": "Search for a regex pattern in files inside a sandbox. Walks `path` recursively by default. Example: { sandbox_id: \"...\", path: \"/home/app/src\", pattern: \"TODO\" }",
"metadata": {},
"name": "sandbox::fs::grep",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"include_glob": [
"**/*.js",
"**/*.ts"
],
"path": "/home/app/src",
"pattern": "TODO|FIXME",
"recursive": true,
"sandbox_id": "00000000-0000-0000-0000-000000000000"
}
],
"properties": {
"exclude_glob": {
"default": [],
"description": "Gitignore-style exclude filter applied relative to `path`.",
"items": {
"type": "string"
},
"type": "array"
},
"ignore_case": {
"default": false,
"description": "Case-insensitive match.",
"type": "boolean"
},
"include_glob": {
"default": [],
"description": "Gitignore-style include filter applied relative to `path`.",
"items": {
"type": "string"
},
"type": "array"
},
"max_line_bytes": {
"default": 4096,
"description": "Maximum bytes per matched line before content is truncated with `…`. Default 4096.",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"max_matches": {
"default": 10000,
"description": "Maximum number of matches before truncation. Default 10_000.",
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"path": {
"description": "Root path to search inside the sandbox guest. Treated as a directory when `recursive: true`, else as a single file.",
"type": "string"
},
"pattern": {
"description": "Regex pattern (Rust regex syntax, anchored fragments allowed).",
"type": "string"
},
"recursive": {
"default": true,
"description": "Descend into subdirectories under `path`. Defaults to true.",
"type": "boolean"
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
}
},
"required": [
"path",
"pattern",
"sandbox_id"
],
"title": "GrepRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FsMatch": {
"description": "Single grep hit. `line` is 1-based. `content` is already truncated to `max_line_bytes` (with a trailing `…`) if the original line was longer.",
"properties": {
"content": {
"type": "string"
},
"line": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"path": {
"type": "string"
}
},
"required": [
"content",
"line",
"path"
],
"type": "object"
}
},
"properties": {
"matches": {
"items": {
"$ref": "#/definitions/FsMatch"
},
"type": "array"
},
"truncated": {
"type": "boolean"
}
},
"required": [
"matches",
"truncated"
],
"title": "GrepResponse",
"type": "object"
}
},
{
"description": "List directory contents inside a sandbox. Example: { sandbox_id: \"...\", path: \"/home/app\" }",
"metadata": {},
"name": "sandbox::fs::ls",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"path": "/home/app",
"sandbox_id": "00000000-0000-0000-0000-000000000000"
}
],
"properties": {
"path": {
"description": "Absolute path of the directory to list inside the sandbox guest.",
"type": "string"
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
}
},
"required": [
"path",
"sandbox_id"
],
"title": "LsRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FsEntry": {
"description": "Single directory entry returned by `FsOp::Ls` / `FsOp::Stat`. `mode` is the octal permission string (e.g. `\"0644\"`); `mtime` is Unix seconds. `is_symlink` reflects the entry itself — FS handlers never follow symlinks unless the op doc-comment says otherwise.",
"properties": {
"is_dir": {
"type": "boolean"
},
"is_symlink": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"mtime": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"size": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"is_dir",
"is_symlink",
"mode",
"mtime",
"name",
"size"
],
"type": "object"
}
},
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FsEntry"
},
"type": "array"
}
},
"required": [
"entries"
],
"title": "LsResponse",
"type": "object"
}
},
{
"description": "Create a directory inside a sandbox. Pass `parents:true` to make missing parents like `mkdir -p`. Example: { sandbox_id: \"...\", path: \"/home/app/cache\", mode: \"0755\", parents: true }",
"metadata": {},
"name": "sandbox::fs::mkdir",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"mode": "0755",
"parents": true,
"path": "/home/app/cache",
"sandbox_id": "00000000-0000-0000-0000-000000000000"
}
],
"properties": {
"mode": {
"default": "0755",
"description": "Octal permissions for the new directory (e.g. `\"0755\"`).",
"type": "string"
},
"parents": {
"default": false,
"description": "Create intermediate parent directories like `mkdir -p`.",
"type": "boolean"
},
"path": {
"description": "Absolute path of the directory to create inside the sandbox guest.",
"type": "string"
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
}
},
"required": [
"path",
"sandbox_id"
],
"title": "MkdirRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"created": {
"type": "boolean"
}
},
"required": [
"created"
],
"title": "MkdirResponse",
"type": "object"
}
},
{
"description": "Move or rename a path inside a sandbox. Pass `overwrite:true` to clobber an existing destination. Example: { sandbox_id: \"...\", src: \"/home/app/old.js\", dst: \"/home/app/new.js\" }",
"metadata": {},
"name": "sandbox::fs::mv",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"dst": "/home/app/new.js",
"overwrite": false,
"sandbox_id": "00000000-0000-0000-0000-000000000000",
"src": "/home/app/old.js"
}
],
"properties": {
"dst": {
"description": "Destination absolute path inside the sandbox guest.",
"type": "string"
},
"overwrite": {
"default": false,
"description": "Allow overwriting `dst` if it already exists.",
"type": "boolean"
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
},
"src": {
"description": "Source absolute path inside the sandbox guest.",
"type": "string"
}
},
"required": [
"dst",
"sandbox_id",
"src"
],
"title": "MvRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"moved": {
"type": "boolean"
}
},
"required": [
"moved"
],
"title": "MvResponse",
"type": "object"
}
},
{
"description": "Read a file from a sandbox. Always returns `content`: a StreamChannelRef callers can subscribe to for the full file bytes. For UTF-8 text files under 1 MiB, the response also carries an inline `body` string so callers can short-circuit the subscription. Example: { sandbox_id: \"...\", path: \"/home/app/index.js\" }",
"metadata": {},
"name": "sandbox::fs::read",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"path": "/home/app/index.js",
"sandbox_id": "00000000-0000-0000-0000-000000000000"
}
],
"properties": {
"path": {
"description": "Absolute path to read inside the sandbox guest.",
"type": "string"
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
}
},
"required": [
"path",
"sandbox_id"
],
"title": "ReadRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ChannelDirection": {
"enum": [
"read",
"write"
],
"type": "string"
},
"StreamChannelRef": {
"properties": {
"access_key": {
"type": "string"
},
"channel_id": {
"type": "string"
},
"direction": {
"$ref": "#/definitions/ChannelDirection"
}
},
"required": [
"access_key",
"channel_id",
"direction"
],
"type": "object"
}
},
"properties": {
"body": {
"description": "Inline UTF-8 body. Populated for text files under [`INLINE_BUFFER_CAP`] (1 MiB) that decode cleanly as UTF-8. `None` for large or binary files; subscribe to `content` instead. When `Some`, the same bytes are also delivered through `content` so legacy callers keep working — new callers can use `body` directly and skip the channel subscription.",
"type": [
"string",
"null"
]
},
"content": {
"allOf": [
{
"$ref": "#/definitions/StreamChannelRef"
}
],
"description": "Channel ref for the file body. Always set; callers can subscribe to receive the full file contents as bytes. Preserved for wire compatibility with peers that statically type this field as `StreamChannelRef`."
},
"mode": {
"type": "string"
},
"mtime": {
"format": "int64",
"type": "integer"
},
"size": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"content",
"mode",
"mtime",
"size"
],
"title": "ReadResponse",
"type": "object"
}
},
{
"description": "Remove a file or directory inside a sandbox. Pass `recursive:true` to remove directories with contents. Example: { sandbox_id: \"...\", path: \"/home/app/temp\", recursive: true }",
"metadata": {},
"name": "sandbox::fs::rm",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"path": "/home/app/temp",
"recursive": true,
"sandbox_id": "00000000-0000-0000-0000-000000000000"
}
],
"properties": {
"path": {
"description": "Absolute path to remove inside the sandbox guest.",
"type": "string"
},
"recursive": {
"default": false,
"description": "Remove directories and their contents recursively (like `rm -rf`).",
"type": "boolean"
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
}
},
"required": [
"path",
"sandbox_id"
],
"title": "RmRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"removed": {
"type": "boolean"
}
},
"required": [
"removed"
],
"title": "RmResponse",
"type": "object"
}
},
{
"description": "Find-and-replace in files inside a sandbox. Pass either `path` (walked like grep) OR `files` (explicit list), not both. Example: { sandbox_id: \"...\", path: \"/home/app/src\", pattern: \"foo\", replacement: \"bar\" }",
"metadata": {},
"name": "sandbox::fs::sed",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Find-and-replace request, accepting either an explicit `files` list or a `path` walked like grep does. Exactly one of those two must be provided — `handle_sed` returns S210 otherwise.",
"examples": [
{
"path": "/home/app/src",
"pattern": "foo",
"recursive": true,
"replacement": "bar",
"sandbox_id": "00000000-0000-0000-0000-000000000000"
}
],
"properties": {
"exclude_glob": {
"default": [],
"description": "Gitignore-style exclude filter applied to paths relative to `path`. Only meaningful with `path`.",
"items": {
"type": "string"
},
"type": "array"
},
"files": {
"default": [],
"description": "Legacy form: explicit list of paths to rewrite.",
"items": {
"type": "string"
},
"type": "array"
},
"first_only": {
"default": false,
"type": "boolean"
},
"ignore_case": {
"default": false,
"type": "boolean"
},
"include_glob": {
"default": [],
"description": "Gitignore-style include filter applied to paths relative to `path`. Only meaningful with `path`.",
"items": {
"type": "string"
},
"type": "array"
},
"path": {
"default": null,
"description": "New form: walk `path` like grep does. May be a directory or a single file. Mutually exclusive with `files`.",
"type": [
"string",
"null"
]
},
"pattern": {
"type": "string"
},
"recursive": {
"default": true,
"description": "Whether to descend into subdirectories. Only meaningful with `path`. Defaults to `true` so the path-form behaves like grep.",
"type": "boolean"
},
"regex": {
"default": true,
"type": "boolean"
},
"replacement": {
"type": "string"
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
}
},
"required": [
"pattern",
"replacement",
"sandbox_id"
],
"title": "SedRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"FsSedFileResult": {
"description": "One `sed` file-level outcome. `success=false` carries the human failure message in `error`; otherwise `error` is absent.",
"properties": {
"error": {
"type": [
"string",
"null"
]
},
"path": {
"type": "string"
},
"replacements": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"success": {
"type": "boolean"
}
},
"required": [
"path",
"replacements",
"success"
],
"type": "object"
}
},
"properties": {
"results": {
"items": {
"$ref": "#/definitions/FsSedFileResult"
},
"type": "array"
},
"total_replacements": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"results",
"total_replacements"
],
"title": "SedResponse",
"type": "object"
}
},
{
"description": "Stat a path inside a sandbox. Example: { sandbox_id: \"...\", path: \"/home/app/index.js\" }",
"metadata": {},
"name": "sandbox::fs::stat",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"examples": [
{
"path": "/home/app/index.js",
"sandbox_id": "00000000-0000-0000-0000-000000000000"
}
],
"properties": {
"path": {
"description": "Absolute path to inspect inside the sandbox guest.",
"type": "string"
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
}
},
"required": [
"path",
"sandbox_id"
],
"title": "StatRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Mirrors `FsEntry`. Note: the shell protocol does not carry uid/gid; those fields are absent at this trigger level.",
"properties": {
"is_dir": {
"type": "boolean"
},
"is_symlink": {
"type": "boolean"
},
"mode": {
"type": "string"
},
"mtime": {
"format": "int64",
"type": "integer"
},
"name": {
"type": "string"
},
"size": {
"format": "uint64",
"minimum": 0,
"type": "integer"
}
},
"required": [
"is_dir",
"is_symlink",
"mode",
"mtime",
"name",
"size"
],
"title": "StatResponse",
"type": "object"
}
},
{
"description": "Write a file into a sandbox. `content` accepts a UTF-8 string (recommended for source/text), a StreamChannelRef object (for large uploads), or use `content_b64` for small binary. Example: { sandbox_id: \"...\", path: \"/home/app/index.js\", content: \"console.log('hi')\\n\" }",
"metadata": {},
"name": "sandbox::fs::write",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"ChannelDirection": {
"enum": [
"read",
"write"
],
"type": "string"
},
"StreamChannelRef": {
"properties": {
"access_key": {
"type": "string"
},
"channel_id": {
"type": "string"
},
"direction": {
"$ref": "#/definitions/ChannelDirection"
}
},
"required": [
"access_key",
"channel_id",
"direction"
],
"type": "object"
},
"WriteContent": {
"anyOf": [
{
"description": "Inline UTF-8 string. Written to the file verbatim.",
"type": "string"
},
{
"allOf": [
{
"$ref": "#/definitions/StreamChannelRef"
}
],
"description": "Streaming channel for large / binary uploads."
}
],
"description": "File body for `sandbox::fs::write`. Untagged so the JSON shape decides which variant runs:\n\n- A bare JSON string (`\"console.log('hi')\"`) → [`WriteContent::Utf8`]. This is what LLM agents naturally pass and the recommended form for source files / configs / small text. - An object that matches [`StreamChannelRef`] → [`WriteContent::Stream`]. The existing channel-streaming path for large or binary payloads coming from a programmatic caller that can construct a channel.\n\nSerde tries variants in declaration order; `Utf8` matches first because every JSON string deserialises into `String`, and `StreamChannelRef` requires an object. Binary inline data uses the separate `content_b64` field on [`WriteRequest`] (not a variant here, so a caller can't accidentally pass base64 expecting it to be decoded — they have to opt in by name)."
}
},
"examples": [
{
"content": "console.log('hello world')\n",
"mode": "0644",
"parents": true,
"path": "/home/app/index.js",
"sandbox_id": "00000000-0000-0000-0000-000000000000"
}
],
"properties": {
"content": {
"anyOf": [
{
"$ref": "#/definitions/WriteContent"
},
{
"type": "null"
}
],
"description": "File body. Pass a UTF-8 string for source/text (the agent-natural form), or a `StreamChannelRef` object for streaming large/binary uploads. Mutually exclusive with `content_b64`; exactly one of the two must be set."
},
"content_b64": {
"default": null,
"description": "Base64-encoded inline body for small binary payloads. Mutually exclusive with `content`.",
"type": [
"string",
"null"
]
},
"mode": {
"default": "0644",
"description": "Octal permissions for the new file (default `\"0644\"`).",
"type": "string"
},
"parents": {
"default": false,
"description": "Create missing parent directories before writing.",
"type": "boolean"
},
"path": {
"description": "Absolute destination path inside the sandbox guest.",
"type": "string"
},
"sandbox_id": {
"description": "UUID returned by `sandbox::create`.",
"type": "string"
}
},
"required": [
"path",
"sandbox_id"
],
"title": "WriteRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"bytes_written": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"path": {
"type": "string"
}
},
"required": [
"bytes_written",
"path"
],
"title": "WriteResponse",
"type": "object"
}
},
{
"description": "List active sandboxes",
"metadata": {},
"name": "sandbox::list",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ListRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"SandboxSummary": {
"properties": {
"age_secs": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"exec_in_progress": {
"type": "boolean"
},
"image": {
"type": "string"
},
"name": {
"type": [
"string",
"null"
]
},
"sandbox_id": {
"type": "string"
},
"stopped": {
"type": "boolean"
}
},
"required": [
"age_secs",
"exec_in_progress",
"image",
"sandbox_id",
"stopped"
],
"type": "object"
}
},
"properties": {
"sandboxes": {
"items": {
"$ref": "#/definitions/SandboxSummary"
},
"type": "array"
}
},
"required": [
"sandboxes"
],
"title": "ListResponse",
"type": "object"
}
},
{
"description": "Run code in an ephemeral sandbox in ONE call. Composes create + fs::write + exec + stop. `lang` selects the interpreter (`node`, `python`, `shell`, or a custom binary path). Sandbox auto-stops on success and on failure unless `keep_sandbox: true`. Example: { image: \"node\", code: \"console.log('hi')\", lang: \"node\" }",
"metadata": {},
"name": "sandbox::run",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"EnvShape": {
"anyOf": [
{
"description": "Original wire shape: `[\"FOO=bar\", \"PATH=/usr/bin\"]`.",
"items": {
"type": "string"
},
"type": "array"
},
{
"additionalProperties": {
"type": "string"
},
"description": "Agent-natural shape: `{ \"FOO\": \"bar\", \"PATH\": \"/usr/bin\" }`. Iteration is sorted by key (BTreeMap) so two callers passing the same map get the same env-var ordering.",
"type": "object"
}
],
"description": "Environment-variable input. Agents naturally pass `{ FOO: \"bar\" }` (matching Docker/npm/k8s mental models); the original wire shape was `Vec<\"K=V\">`. The untagged enum accepts both; `into_kv_vec()` normalises to the canonical `Vec<String>` the runner expects.\n\n`Default` is the empty vec form (the historical wire shape)."
},
"RunFile": {
"description": "Optional sibling file to drop into the sandbox before the main code runs.",
"properties": {
"content": {
"description": "UTF-8 file body. Streaming and base64 are not supported here; callers needing those should use `sandbox::create` + repeated `sandbox::fs::write` calls instead.",
"type": "string"
},
"path": {
"description": "Absolute path inside the sandbox guest where the file lands.",
"type": "string"
}
},
"required": [
"content",
"path"
],
"type": "object"
}
},
"examples": [
{
"code": "console.log('hello world')",
"image": "node",
"lang": "node",
"timeout_ms": 300000
}
],
"properties": {
"code": {
"description": "The actual code to run. Written to a `/tmp/run.{ext}` file inside the sandbox; the chosen interpreter runs that file.",
"type": "string"
},
"env": {
"allOf": [
{
"$ref": "#/definitions/EnvShape"
}
],
"description": "Env vars exposed to the interpreter. Accepts both `Vec<\"K=V\">` and `{ K: V }` map shapes (same as `sandbox::exec.env`)."
},
"files": {
"description": "Optional sibling files (extra source modules, config, fixtures).",
"items": {
"$ref": "#/definitions/RunFile"
},
"type": "array"
},
"image": {
"description": "Catalog name of the image to boot (preset or `custom_images` key). Same value space as `sandbox::create`.",
"type": "string"
},
"keep_sandbox": {
"default": false,
"description": "If true, the sandbox is NOT stopped after the run completes; `sandbox_id` is returned so the caller can poke around. Default false (the sandbox is torn down on either success or failure).",
"type": "boolean"
},
"lang": {
"description": "Selects the interpreter and file extension. Required. Built-in values: `\"node\"`, `\"python\"`, `\"shell\"`. Any other string is treated as a literal interpreter binary path inside the VM and the file is written to `/tmp/run.txt`. There is no default — there's no honest universal answer to \"what language is this code\", and silently defaulting to shell makes Python or JS code produce confusing line-by-line failures.",
"type": "string"
},
"stdin": {
"default": null,
"description": "Base64-encoded bytes piped to the interpreter's stdin.",
"type": [
"string",
"null"
]
},
"timeout_ms": {
"default": null,
"description": "Kill-after window for the interpreter, in ms. Defaults to 300_000 (5 minutes); pass a smaller value to fail fast on quick probes.",
"format": "uint64",
"minimum": 0,
"type": [
"integer",
"null"
]
}
},
"required": [
"code",
"image",
"lang"
],
"title": "RunRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"duration_ms": {
"format": "uint64",
"minimum": 0,
"type": "integer"
},
"exit_code": {
"format": "int32",
"type": [
"integer",
"null"
]
},
"sandbox_id": {
"description": "Present only if `keep_sandbox: true` was set on the request. Otherwise null (sandbox auto-stopped).",
"type": [
"string",
"null"
]
},
"stderr": {
"type": "string"
},
"stdout": {
"type": "string"
},
"success": {
"type": "boolean"
},
"timed_out": {
"type": "boolean"
}
},
"required": [
"duration_ms",
"stderr",
"stdout",
"success",
"timed_out"
],
"title": "RunResponse",
"type": "object"
}
},
{
"description": "Stop and remove a running sandbox. Set `wait: true` to block until the VM process exits and resources are reclaimed.",
"metadata": {},
"name": "sandbox::stop",
"request_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"sandbox_id": {
"type": "string"
},
"wait": {
"default": false,
"description": "Block until the VM is fully reaped before returning.",
"type": "boolean"
}
},
"required": [
"sandbox_id"
],
"title": "StopRequest",
"type": "object"
},
"response_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"sandbox_id": {
"type": "string"
},
"stopped": {
"type": "boolean"
}
},
"required": [
"sandbox_id",
"stopped"
],
"title": "StopResponse",
"type": "object"
}
}
],
"triggers": []
}