<Sandbox> spawns a child workflow inside an isolated runtime, ships a request bundle to it, waits for execution to finish, and collects the result bundle back into the parent workflow. Diffs produced inside the sandbox can be reviewed and optionally auto-accepted before they are applied to the host environment. Use <Sandbox> when a task needs a clean filesystem, network isolation, or a reproducible dependency environment that must not share state with the caller.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique sandbox identifier within the workflow run. |
output | ZodObject | DrizzleTable | string | required | Output target for the collected bundle result. |
workflow | (...args: any[]) => any | undefined | Child workflow definition to execute inside the sandbox. |
input | unknown | undefined | Input value passed to the child workflow. |
runtime | "bubblewrap" | "docker" | "codeplane" | "bubblewrap" | Execution runtime. Falls back to "bubblewrap" if Docker is not available. |
allowNetwork | boolean | false | Whether the sandbox has outbound network access. |
reviewDiffs | boolean | true | Trigger the diff review event when the bundle contains patch files. |
autoAcceptDiffs | boolean | false | Automatically accept diffs without requiring human approval. |
image | string | undefined | Docker image to use for the docker runtime. |
env | Record<string, string> | undefined | Environment variables injected into the container. |
ports | Array<{ host: number; container: number }> | undefined | Port mappings for Docker containers. |
volumes | SandboxVolumeMount[] | undefined | Volume mounts for Docker containers. |
memoryLimit | string | undefined | Memory limit for the container (e.g. "512m", "2g"). |
cpuLimit | string | undefined | CPU limit for the container (e.g. "0.5", "2"). |
command | string | undefined | Override the default entrypoint command inside the sandbox. |
workspace | SandboxWorkspaceSpec | undefined | Codeplane workspace configuration. |
skipIf | boolean | false | Skip the sandbox entirely. Returns null. |
timeoutMs | number | undefined | Total sandbox execution timeout in milliseconds. |
heartbeatTimeoutMs | number | undefined | Heartbeat timeout in milliseconds. |
retries | number | undefined | Number of retry attempts on failure. |
retryPolicy | RetryPolicy | undefined | Retry policy configuration. |
continueOnFail | boolean | false | Continue workflow execution even if the sandbox fails. |
cache | CachePolicy | undefined | Cache policy for the sandbox result. |
dependsOn | string[] | undefined | Explicit dependency IDs that must complete before this sandbox starts. |
needs | Record<string, string> | undefined | Named output bindings from other steps. |
label | string | id | Display label shown in the workflow UI. |
meta | Record<string, unknown> | undefined | Arbitrary metadata attached to the sandbox event. |
children | ReactNode | undefined | Child workflow body when using a createSmithers()-bound Sandbox wrapper. |
SandboxVolumeMount
| Field | Type | Description |
|---|---|---|
host | string | Absolute path on the host machine. |
container | string | Path inside the container. |
readonly | boolean | Mount as read-only if true. |
SandboxWorkspaceSpec
| Field | Type | Description |
|---|---|---|
name | string | Workspace name in the Codeplane account. |
snapshotId | string | Snapshot ID to restore before execution. |
idleTimeoutSecs | number | Seconds of inactivity before the workspace stops. |
persistence | "ephemeral" | "sticky" | Whether the workspace is discarded after each run ("ephemeral") or kept between runs ("sticky"). |
Basic usage with Docker
Run a code-generation workflow inside a Docker container with a specific image and resource limits:Codeplane persistent workspace
Use a Codeplane workspace with a pre-built snapshot for faster startup and sticky persistence across runs:With diff review and conditional auto-accept
Run a refactoring workflow that produces patches. Auto-accept only when the parent input explicitly approves:Runtime comparison
| Feature | bubblewrap | docker | codeplane |
|---|---|---|---|
| Requires external daemon | No | Yes (Docker) | Yes (API credentials) |
| Custom image | No | Yes (image) | Workspace snapshot |
| Port mapping | No | Yes (ports) | No |
| Volume mounts | No | Yes (volumes) | No |
| Resource limits | No | Yes (memoryLimit, cpuLimit) | No |
| Environment variables | No | Yes (env) | No |
| Persistent workspace | No | No | Yes (persistence: "sticky") |
| Snapshot restore | No | No | Yes (snapshotId) |
| Idle timeout | No | No | Yes (idleTimeoutSecs) |
| Auto-fallback target | — | bubblewrap | — |
| External credentials required | No | No | CODEPLANE_API_URL, CODEPLANE_API_KEY |
How sandbox execution works
When the engine mounts a<Sandbox> node it follows this sequence:
- Checks the active sandbox count against the concurrency limit. Fails immediately if the limit is reached.
- Creates a
request-bundledirectory under.smithers/sandboxes/<runId>/<sandboxId>/and writes an initialREADME.mdmanifest withstatus: "pending". - Calls the transport layer’s
createto provision the runtime environment (container, workspace, or local process). - Ships the request bundle to the sandbox via
ship. - Executes
smithers up bundle.tsxinside the sandbox. - Runs the child workflow as a detached child run.
- Writes the child run’s output and logs into a result bundle.
- Calls
collecton the transport to retrieve the result bundle path. - Validates the bundle: size, manifest structure, and patch path safety.
- If the bundle contains patches and
reviewDiffsistrue, emitsSandboxDiffReviewRequested. IfautoAcceptDiffsisfalse, throws and leaves patches unapplied. - If
autoAcceptDiffsistrue, emitsSandboxDiffAcceptedand returnsmanifest.outputsto the parent workflow. - Always calls
cleanupon the transport handle in afinallyblock, even on failure.
Delta transport
The sandbox communicates with the host through a file-based delta transport. The host writes a request bundle — a directory containing aREADME.md JSON manifest — and the sandbox writes a result bundle back to a separate result/ directory. The transport layer (SandboxTransport) abstracts the mechanics of moving those directories into and out of the runtime. Each transport operation is timed and reported to the sandboxTransportDurationMs metric.
The SandboxTransportService interface exposes five operations:
| Method | Description |
|---|---|
create(config) | Provision the runtime and return a SandboxHandle. |
ship(bundlePath, handle) | Copy the request bundle into the runtime. |
execute(command, handle) | Run a command inside the runtime. |
collect(handle) | Retrieve the result bundle from the runtime. |
cleanup(handle) | Destroy or release the runtime environment. |
Bundle structure and validation
Every result bundle must pass validation before the parent workflow receives its outputs.README.md manifest is a JSON object with this shape:
status must be one of "finished", "failed", or "cancelled". Any other value causes validation to throw before the bundle is used.
Bundle limits
| Limit | Value |
|---|---|
| Total bundle size | 100 MB |
README.md size | 5 MB |
| Maximum patch files | 1,000 |
| Bundle path length | 1,024 characters |
| Run ID length | 256 characters |
| Output JSON depth | 16 levels |
| Output array length | 512 items |
| Output string length | 64 KB per string |
Runtime auto-fallback
Whenruntime="docker" is set and the Docker daemon is not reachable at startup, <Sandbox> silently falls back to "bubblewrap". The resolved runtime is recorded in the sandbox config and surfaced in the SandboxCreated event. No other runtime combination triggers automatic fallback.
Concurrency limits
The maximum number of simultaneously active sandboxes within a single workflow run is controlled by theSMITHERS_MAX_CONCURRENT_SANDBOXES environment variable. It defaults to 10. If the limit is reached when a new <Sandbox> node is mounted, the component throws immediately with SANDBOX_EXECUTION_FAILED.
Streaming log capture
If the child workflow produces alogs/stream.ndjson file during execution, that file is included in the result bundle and its path is available as logsPath in the validated bundle. Log capture does not contribute to the bundle size estimate until the bundle is written.
Custom command override
Usecommand to replace the default smithers up bundle.tsx entrypoint:
Passing input to the sandbox
Theinput prop is serialized into the request bundle manifest and passed directly to the child workflow as its input. Any JSON-serializable value is valid:
Security notes
<Sandbox> enforces several controls to prevent unsafe bundles from affecting the host filesystem.
Path traversal protection. Every patch file path in the bundle manifest is resolved relative to patches/ and checked with path.relative. Any path that resolves outside the bundle root (..) causes an immediate TOOL_PATH_ESCAPE error and the bundle is rejected before any files are applied.
Patch file limit. Bundles with more than 1,000 .patch files are rejected. This prevents resource exhaustion from unbounded file enumeration during bundle validation.
README.md size limit. The README.md manifest is capped at 5 MB. Oversized manifests are rejected before their JSON is parsed, preventing memory exhaustion from malformed bundles.
Network isolation. allowNetwork defaults to false. Each runtime enforces this constraint at the environment level, not in application code.
Docker image pinning. Specify an exact digest or a pinned tag in image to prevent image drift between runs. Untagged images pull latest which is non-deterministic.
Codeplane credentials. The codeplane runtime requires CODEPLANE_API_URL and CODEPLANE_API_KEY environment variables. If either is missing, the sandbox fails at create time with INVALID_INPUT rather than at execution time.
Rendering
<Sandbox> renders to a <smithers:sandbox> host element. The child workflow definition is passed as the internal __smithersSandboxWorkflow attribute and the input as __smithersSandboxInput. These internal attributes are consumed by the engine and are not visible in the workflow tree. When skipIf is true the component returns null and no sandbox is provisioned.
Notes
- A sandbox that fails during execution records
status: "failed"in the local database and emits aSandboxFailedevent. The error is re-thrown to the parent workflow unlesscontinueOnFail={true}. cleanupis always called in afinallyblock. Cleanup errors are silently swallowed to avoid masking the original failure.reviewDiffsdefaults totrue. SetautoAcceptDiffs={true}to bypass the approval gate in automated pipelines.- The
workspace.persistencefield only affects the Codeplane runtime."ephemeral"workspaces are destroyed after each run;"sticky"workspaces are retained and reused on the next run with the sameworkspace.name. snapshotIdrestores a named Codeplane snapshot before execution begins, enabling fast environment setup without a full install step on every run.- Steps declared in
dependsOnmust complete successfully before the sandbox is provisioned. The sandbox does not count toward the concurrency limit until provisioning begins.