Skip to content
Sandbox SDK
Esc
↑↓navigate↵open⌘Jpreview
On this page

Write a provider

Adapt any sandbox service to Sandbox SDK. Implement a small set of primitives from /provider, then check your adapter with runConformance from /testing.

Each built-in provider is a small adapter over a fixed set of primitives. Write the same adapter for another service and you get the whole API on top of it: runs, the $ tag, processes and logs, readiness checks, edit, upload, search, changes, git, redaction, and agent tools.

What the core does for you

You don’t implement any of these. The core builds them from your primitives:

  • run, $, timeouts, output caps, and streaming.
  • Background processes, their logs and cursors, and reattach by name.
  • files.edit, upload, download, search, watch, mark, and changes.
  • Every git method.
  • Secret redaction and the SandboxError message format.
  • Get-or-create by key, locator, and the sandbox-sdk label.

The contract

Import the types from @opencoredev/sandbox-sdk/provider. A Provider has:

Member What it does
name The provider name, shown in box.provider and errors
keyMode "exact" if the service enforces unique names, otherwise "best-effort"
capabilities The optional features you implement
create(input) Create a sandbox with the requested size, env, network, ports, limits, labels, and snapshot
find(key) Find a sandbox by key, or undefined
connect(id) Attach to a sandbox by ID. Throw not_found if it’s gone.
list(labels) List sandboxes matching labels
destroy(id) Delete a sandbox
exec(id, input) Run one command and stream its output
files read, write, list, stat, remove, mkdir, and optionally readRange
previewUrl(id, port) Return { url, headers?, expiresAt? } for a port
native(id) The service’s own sandbox object, exposed as box.native
pause, resume, snapshot, terminal Optional. Must match capabilities.
close, usage, startProcess Optional hooks

Paths are absolute POSIX paths and times are epoch milliseconds.

Rules an adapter must follow

  • Fail closed. If create can’t enforce a requested size, network, idleTimeoutMs, or timeoutMs, throw SandboxError with code unsupported. Never create a sandbox that’s more open than requested.
  • Exec closes stdin unless stdin is given, emits chunks in arrival order, and kills the entire remote process group before settling on timeout or abort.
  • Translate errors. Map the service’s failures to SandboxError codes such as auth, not_found, quota, and unavailable. Don’t attach the native error object, since it can hold credentials.
  • Keep policy out. Redaction, edit, git, process records, and readiness belong to the core.
  • Declare what you implement. A capability without its method fails at createSandbox with code provider.

Check it with the conformance suite

runConformance from @opencoredev/sandbox-sdk/testing runs the suite every built-in provider passes. It covers create and find by key, commands, $ escaping, processes, files, git, previews, and cleanup. It needs vitest 3 or later.

import { runConformance } from "@opencoredev/sandbox-sdk/testing";
import { myProvider } from "./my-provider";

runConformance(() => myProvider(), { live: true });

live: true is required for every provider except memory() and local(). With live: false the suite refuses any other provider, so a test run can’t reach a cloud account by accident. Live runs give each case 120 seconds, since real providers boot machines and cross the network. Pass timeoutMs to change that.

Every check runs unless you skip it. To skip a check your runtime can’t pass, give the reason under unsupported:

  • gitPush skips the bare-repository clone, commit and push, for a runtime without git.
  • httpProbe skips the real HTTP request to a preview URL, for previews no HTTP client can reach.
  • previews expects preview() to reject with unsupported, and skips the HTTP probe too.
  • processes skips the background process checks.
  • fileModes skips the permission-bit check for files.write.
import { runConformance } from "@opencoredev/sandbox-sdk/testing";
import { myProvider } from "./my-provider";

runConformance(() => myProvider(), {
  live: true,
  unsupported: { httpProbe: "Preview URLs are only reachable inside the VPC" },
});

The built-in adapters in packages/sdk/src/providers are working examples to copy from.

Avoiding extra calls

If an adapter can create and remove the core run activity record inside its native exec, set inlineActivity: true and honor ExecInput.activityPath for the entire run, including failure and cancellation. Otherwise the core maintains that record through file calls. Garbage collection uses the record to avoid reaping active work.

Set files.writeCreatesParents: true only when write() handles missing parent directories itself. The core then omits its unconditional mkdir. An adapter can try the native upload first, create missing parents on a confirmed not-found response, and retry the upload. Native whole-file and ranged reads should return the full requested data, up to EOF.

Was this page helpful?