Skip to main content

Sandbox Providers

A <Sandbox> boundary runs a child workflow outside the parent task process. Where it runs is decided by a provider. A provider is a plain object that takes a request and returns a result. Smithers ships first-class providers for five cloud backends and a shared kit so a new provider is a thin adapter. The built-in providers are Daytona, Vercel, AWS, GCP, and Cloudflare. Each is a separate optional package with its own SDK as an optional dependency, so you install only what you use.

The SandboxProvider contract

A provider implements run() and an optional cleanup():
type SandboxProvider = {
  id: string;
  run(request: SandboxProviderRequest): Promise<SandboxProviderResult> | SandboxProviderResult;
  cleanup?(request: SandboxProviderRequest): Promise<void> | void;
};
run() receives the child workflow, the <Sandbox input> value, the request and result bundle paths, the network and output-size limits, an abort signal, and a heartbeat callback. It returns either a bundlePath to a bundle it wrote, or a structured { status, output, remoteRunId?, workspaceId?, diffBundle? }. Smithers validates the result, enforces diff-review policy, and returns the outputs to the parent task. See <Sandbox> for the full request and result types.

The provider-kit

Cloud providers do not implement run() from scratch. They call createCommandSandboxProvider from smithers-orchestrator/sandbox, which owns the request/result-file protocol once so every provider behaves the same:
  1. Create a vendor session and emit a session-created heartbeat.
  2. Write the request JSON, carrying only the safe fields (runId, sandboxId, input, config, allowNetwork, maxOutputBytes, redacted egress).
  3. Merge the command env: options.env, the egress env, and the two path vars below. It never copies arbitrary process.env.
  4. Run the entry command with the request’s toolTimeoutMs and abort signal.
  5. Parse the result from stdout if it starts with {, otherwise read the result file. A nonzero exit with no valid result JSON throws SANDBOX_EXECUTION_FAILED with stdout and stderr truncated to maxOutputBytes and redacted.
  6. On cleanup, resolve the cached session and destroy it per policy.
A provider author implements a small SandboxSession seam, not the protocol:
type SandboxSession = {
  readonly remoteId: string;
  readonly writeFile: (path: string, content: string) => Promise<void>;
  readonly readFile: (path: string) => Promise<string>;
  readonly exec: (command: string, opts: SandboxExecOptions) => Promise<SandboxExecResult>;
  readonly destroy?: () => Promise<void>;
};
The kit hands the entry command two env vars:
  • SMITHERS_SANDBOX_REQUEST_PATH: where to read the request JSON.
  • SMITHERS_SANDBOX_RESULT_PATH: where to write the result JSON.
The entry command either prints the result JSON to stdout or writes it to SMITHERS_SANDBOX_RESULT_PATH. Providers with no shared filesystem (AWS, GCP) transport those files through S3 or GCS and inject extra vars pointing the entry at the object keys.

Selecting a provider

There are three ways to pick a provider, in precedence order:
  1. Provider object (primary). Pass the factory result straight to the prop: <Sandbox provider={createDaytonaSandboxProvider({...})} workflow={child} />.
  2. Registered id. Register once, then reference by id: registerSandboxProvider(createVercelSandboxProvider({...})) then <Sandbox provider="vercel-sandbox" />. The provider packages also export register<Provider>SandboxProvider(...) convenience wrappers.
  3. Env default (planned, not yet shipped). A future SMITHERS_SANDBOX_PROVIDER env var will select a registered id when a <Sandbox> sets no explicit provider, resolving a registered id only (never auto-creating a cloud client). It is not implemented yet; setting it today has no effect. The intended precedence, once shipped, mirrors the DB-backend chain: an explicit provider prop wins, then workflow config, then SMITHERS_SANDBOX_PROVIDER, then the local default. Until then, select a provider with method 1 or 2.
Credentials always come from factory options first and the vendor env chain second. They are never required in request.config.

Egress and secret redaction

Egress is configured on the <Sandbox egress> prop and reaches the provider as the normalized request.egress. The kit projects it into the command env through sandboxEgressEnv(), and when caCertPem is set it uploads the CA to the workspace so NODE_EXTRA_CA_CERTS resolves inside the sandbox. See <Sandbox> egress controls. Providers with a real remote filesystem (Daytona, Vercel, Cloudflare) resolve that CA path directly. The object-store providers (AWS, GCP) have no shared filesystem, so they upload the CA to their bundle store and pass its location to the container: AWS injects SMITHERS_SANDBOX_CA_S3_KEY and GCP injects SMITHERS_SANDBOX_CA_GCS_OBJECT. The container entry command must download that object and write it to the path named by NODE_EXTRA_CA_CERTS before it makes outbound calls. Credentials are used only to build the local SDK client. They are never placed in the request JSON and never forwarded into the remote env unless you list them explicitly in options.env. redactSandboxProviderValue() redacts keys containing token, secret, key, password, credential, or authorization from every thrown message, heartbeat payload, and the persisted configJson audit row.

Provider comparison

ProviderPackageProvider idComputeBundle transportAuthExit code
Daytonasmithers-orchestrator/daytonadaytona-sandboxDaytona sandboxSandbox filesystemDAYTONA_API_KEYnumeric
Vercelsmithers-orchestrator/vercelvercel-sandboxVercel SandboxSandbox filesystemOIDC or token trionumeric
AWSsmithers-orchestrator/awsaws-sandboxFargate or CodeBuildS3 bucket (BYO)AWS credential chainFargate numeric, CodeBuild status
GCPsmithers-orchestrator/gcpgcp-sandboxCloud Run JobsGCS bucket (BYO)Application Default Credentialstask status (0/1)
Cloudflaresmithers-orchestrator/cloudflarecloudflare-sandboxSandbox SDK containerSandbox filesystemDurable Object bindingnumeric
Per-provider setup, options, and cost live on each provider page: