Skip to main content
<Sandbox> is a task boundary for work that runs outside the parent task process. It’s provider-first: pass an injectable provider object or a registered provider id; runtime covers only the built-in legacy local transports.
The sandbox provider contract types (SandboxProvider, SandboxProviderRequest, SandboxProviderResult, ExecuteSandboxOptions) are public exports from "smthrs/sandbox". Import them with import type when writing adapters.

Egress controls

Use egress when the sandbox itself should own outbound-network configuration. Smithers doesn’t mutate the parent harness environment for this path; it serializes the egress contract into the request and passes it to the selected provider or local transport.
Provider-backed sandboxes receive the normalized contract as request.egress; the raw serializable value stays in request.config.egress for adapters needing provider-specific fields. The JSX prop is typed unknown; at execution time Smithers accepts a provider object directly or a string id only after registering a provider with registerSandboxProvider({ id, run }). The unknown type lets provider packages supply their own shapes, so TypeScript will not reject a bad provider at the JSX call site; invalid provider values fail during execution with INVALID_INPUT. Smithers core doesn’t hardcode an iron-proxy adapter. Local transports merge the generated proxy environment into the sandbox handle environment, after env, so HTTP_PROXY, HTTPS_PROXY, NO_PROXY, and NODE_EXTRA_CA_CERTS are visible inside the sandbox process. When caCertPem is provided, Smithers writes it into the request bundle at .smithers/egress/ca.crt and points local transports at /workspace/.smithers/egress/ca.crt. If the CA is already installed by the sandbox image or sidecar, use caCertPath instead. Persisted sandbox config redacts egress env values, CA PEM, and secret-binding entries.

Basic usage

workflow takes a SmithersWorkflow: the same value a workflow file’s default export holds. smithers((ctx) => ...) returns one, so the child is just a createSmithers module you import, no extra wrapping needed:
The provider’s run() result is what the parent receives: minimally { status: "finished", output, remoteRunId?, workspaceId? }, where output matches the output schema (outputs.result above). Add diffBundle when the sandbox changed files (see Result bundles).

Complete one-file provider and child workflow

The smallest complete pattern: a concrete provider object, allowNetwork={true}, and a child workflow in one file. Real providers usually create a VM or container but return the same structured SandboxProviderResult.

Execution model

  1. Smithers renders <Sandbox> as one scheduler task; children don’t become parent-run tasks.
  2. At execution time Smithers writes a request bundle under .smithers/sandboxes/<run>/<sandbox>/request-bundle.
  3. A provider receives the request, runs work remotely, and returns a local bundle path or a structured result.
  4. Smithers validates the result bundle, records sandbox lifecycle events, enforces diff review policy, applies accepted diffBundles, and returns outputs to the parent task output table.
Providers receive a SandboxProviderRequest and return a SandboxProviderResult:
Register reusable providers when a workflow should reference them by id:

Result bundles

A provider can return a path to a bundle it created:
Or it can return a structured result and let Smithers materialize the bundle locally:
Use the structured-result form when your adapter can return the output JSON and optional diffBundle directly. Use bundlePath when the remote side already wrote a full Smithers result bundle, you need to preserve larger artifacts, or the provider owns bundle materialization. Bundle limits are enforced before the result is accepted: 100 MB total, 5 MB manifest file (README.md), 1,000 patch files, bounded JSON output, and no path traversal or symlinks in bundle paths.

Diff review

reviewDiffs defaults to true. If the sandbox returns patch files or a diffBundle, Smithers records SandboxDiffReviewRequested. When autoAcceptDiffs is false, changed bundles fail closed until reviewed; when it’s true, or reviewDiffs is false, Smithers applies diffBundle via the engine diff-bundle applier. Legacy patch files are still collected and review-gated, but the apply path is diffBundle.

Nested sandboxes

Nested sandbox execution is disabled by default (a sandbox running inside another must set allowNested). Use it only when the provider and diff policy are designed for nesting. Hard cases:
  • Diff base conflicts: an inner sandbox can generate a diffBundle against a different base than the outer sandbox.
  • Cleanup ordering: an outer provider cleanup can delete the workspace before the inner provider finishes.
  • Quotas and concurrency: nested remote VMs can multiply resource usage quickly.
  • Network and secrets: inherited remote credentials may be broader than intended.
  • Event lineage: parent run, outer sandbox run, and inner sandbox run need clear ids for debugging.
For most workflows, use sibling sandboxes under a Parallel or MergeQueue instead of nesting.

Built-in local transports

When provider is omitted, Smithers uses the legacy local transport path: runtime is "bubblewrap" (the default), "docker", or "codeplane". "cloudflare" is provider-backed, via createCloudflareSandboxProvider() from smthrs/cloudflare. Unknown runtimes fail closed; Docker is no longer silently replaced by bubblewrap when unavailable.

First-class providers

Smithers ships a local microVM provider and five cloud providers, each a separate optional package mapping the provider contract onto a runtime SDK: <Sandbox provider={createDaytonaSandboxProvider({...})}> runs the child workflow on that backend with no runtime change. See Sandbox Providers for the shared contract, provider-kit, selection precedence, and a comparison table.