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, andchanges.- Every
gitmethod. - Secret redaction and the
SandboxErrormessage format. - Get-or-create by key,
locator, and thesandbox-sdklabel.
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
createcan’t enforce a requestedsize,network,idleTimeoutMs, ortimeoutMs, throwSandboxErrorwith codeunsupported. Never create a sandbox that’s more open than requested. - Exec closes stdin unless
stdinis 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
SandboxErrorcodes such asauth,not_found,quota, andunavailable. 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
createSandboxwith codeprovider.
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:
gitPushskips the bare-repository clone, commit and push, for a runtime without git.httpProbeskips the real HTTP request to a preview URL, for previews no HTTP client can reach.previewsexpectspreview()to reject withunsupported, and skips the HTTP probe too.processesskips the background process checks.fileModesskips the permission-bit check forfiles.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.