<Sandbox> is a task boundary for work that should execute outside the parent task process. The public component is provider-first: pass an injectable provider object or a registered provider id. runtime remains only for the built-in legacy local transports.
"smithers-orchestrator/sandbox": SandboxProvider,
SandboxProviderRequest, SandboxProviderResult,
SandboxDiffBundleLike, SandboxEgressConfig, and
ExecuteSandboxOptions. Import them with import type when writing adapters.
Egress controls
Useegress when the sandbox itself should own outbound-network configuration. Smithers does not mutate the parent harness environment for this path. It serializes the egress contract into the sandbox request and passes it to the selected provider or local transport.
request.egress, and the raw serializable value remains in request.config.egress for adapters that need 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 does not hardcode a Freestyle or iron-proxy sandbox 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 a SmithersWorkflow, so the child
is a normal createSmithers module you import; no extra wrapping is needed:
run() returns the result your parent receives. The minimal
structured shape is { status: "finished", output, remoteRunId?, workspaceId? },
where output matches the output schema (outputs.result above). Add a
diffBundle when the sandbox changed files (see Result bundles).
Complete one-file provider and child workflow
This is the smallest complete pattern: a concrete provider object,allowNetwork={true}, and a child workflow in the same file. Real providers
usually create a VM or container, but they still return the same structured
SandboxProviderResult.
Execution model
- Smithers renders
<Sandbox>as one scheduler task. Children do not become parent-run tasks. - At execution time Smithers writes a request bundle under
.smithers/sandboxes/<run>/<sandbox>/request-bundle. - A provider receives the request, runs work remotely, and returns either a local bundle path or a structured result.
- Smithers validates the result bundle, records sandbox lifecycle events, enforces diff review policy, applies accepted
diffBundles, and returnsoutputsto the parent task output table.
Result bundles
A provider can return a path to a bundle it created:diffBundle directly. Use bundlePath when the remote side already
wrote a full Smithers result bundle, when you need to preserve larger artifacts,
or when 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 a review path accepts them. When autoAcceptDiffs is true, or reviewDiffs is false, Smithers applies diffBundle through 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 sandbox must setallowNested.
Use nesting only when the provider and diff policy are designed for it. The hard cases are:
- Diff base conflicts: an inner sandbox can generate a
diffBundleagainst 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.
Parallel or MergeQueue instead of nesting.
Built-in local transports
Whenprovider is omitted, Smithers uses the legacy local transport path. runtime may be "bubblewrap", "docker", or "codeplane". If runtime is omitted, the local path defaults to "bubblewrap".
Unknown runtimes now fail closed. Docker is not silently replaced by bubblewrap when Docker is unavailable.
Freestyle provider example
Freestyle VMs are full sandboxes with nested virtualization, full networking, and the ability to scale to more resources than alternatives. Use Freestyle VMs when you want to give your agents a real computer rather than a code runner. Seeexamples/freestyle/ for a provider adapter that creates a Freestyle VM, writes setup and request files with vm.fs.writeTextFile(), executes a command with vm.exec(), reads smithers-result.json with vm.fs.readTextFile(), and returns a Smithers sandbox result.
Freestyle’s current VM docs show the stable package as freestyle, VM creation through freestyle.vms.create(), file I/O through vm.fs.writeTextFile() / vm.fs.readTextFile(), command execution with vm.exec(), and lifecycle controls such as start(), stop(), resize(), fork(), delete({ vmId }), and idleTimeoutSeconds. Relevant Freestyle docs: