Skip to main content
This page is the contract a provider engineer builds against. For what a sandbox provider is and what Checkfu supplies around it, read Sandbox providers first. For what your provider may claim about isolation, read Provider grading and honest limits. The contract has seventeen members: one capability envelope, fifteen lifecycle operations, and openProcess. Everything below is the behavior the Runner validates today against its in-tree adapters. It is the same behavior a wire provider will be held to, because the validation lives on the Checkfu side of the seam and does not move.
The wire form of these members does not exist yet. The semantics on this page are stable — they are enforced today by the Runner’s sandbox runtime — but there is no published endpoint set, no JSON Schema, and no golden vector to decode against. See Machine-readable author kit for exactly what is pending and what the harness-side equivalent looks like.

Machine-readable author kit

The harness author path ships a checked-in kit generated from the same Effect Schemas the control plane decodes: an acp-v1 extension schema with golden wire vectors, and a oneshot-v1 authoring schema with its author-kit vectors. Revision literals in those files are compatibility identities: changing one requires an explicit review, not an in-place reinterpretation. The sandbox-provider equivalent is not published. When it lands it will follow exactly that shape — generated from SandboxProviderCapabilities, the request and result schemas below, and the openProcess frame vocabulary, with golden vectors covering the identity cross-checks and the failure dispositions. Until then, the authority is the schema definitions themselves and this page. Do not infer a wire encoding from the tables here; they describe semantics, not bytes.

From adapter to qualified provider

The harness path is self-serve end to end: checkfu harness init, then checkfu harness test oci against a build context, then the same command against a published immutable digest, then checkfu harness add oci and checkfu harness verify for server-issued, signature-verified evidence. No sandbox-provider equivalent of any of those commands exists. Concretely:
  • There is no enrollment path for an implementation. Be precise about which half is missing, because they are not the same. A capability declaration path already exists: a Runner declares RunnerProviderCapability entries in its claim request, and the control plane intersects them with a server-owned enrollment ceiling. What does not exist is any way to admit provider code Checkfu did not compile — each transport hands the sandbox runtime a compiled-in adapter, so the adapter set is fixed at build time. The wire seam’s whole purpose is to split those apart, and the missing enforcement and egress_certification ceilings are exactly the fields that declaration path does not yet carry.
  • There is no self-serve conformance command. The nine conformance cases exist as an in-tree test port (SandboxConformanceTarget) driven against a memory oracle and a real Docker daemon. A partner cannot run them without being a workspace package.
  • There is therefore no local diagnostic result and no signed evidence for a sandbox provider today.
What is stable is the content those commands will exercise — see Conformance — so building against this page is not wasted work.

Lifecycle and states

An instance has exactly two states: provisioning and active. Every operation names the states it accepts, and the Runner refuses a mismatch as SandboxLifecycleConflict{operation, expected, actual} before calling you.
resume produces an active instance directly — there is no provisioning step on the resume path.

The operations

Six operations return an allocation handoff; the rest return their value directly. The distinction is in Allocation handoff and it matters more than it looks. A provider that implements everything except the four optional members is a complete provider for one-shot harnesses with no memory writeback and no Project capture. beginActiveWrite has an explicit ownership handoff. A transaction remains caller-owned and abortable while its durable journal is open. Immediately before the provider changes that journal to dispatched, it invokes the transaction’s dispatch marker; a caller that stops observing after that marker must leave the command indeterminate and available for redelivery. Only dispatched journals are recovery work — another transaction’s merely staged open journal is never rolled back. A provider that pauses a workload records its own pause token before issuing the potentially ambiguous pause command, never resumes a pre-existing pause without such a token, transfers an inherited token before clearing its predecessor, and keeps the current token until unpause itself settles. Publication records a durable committed marker before deleting rollback evidence. Recovery rolls back an uncommitted dispatch and finishes cleanup forward after the committed marker; it never attempts to reconstruct old files from partially deleted evidence. The transaction resolves the deepest mount that owns every target; an ancestor writeable mount cannot authorize a path inside a nested read-only or separately owned mount. A provider whose active-write journal would place file bodies on durable host storage must reject ephemeral_zdr transactions before accepting a body.

Identity is cross-checked, not trusted

Every returned instance is compared field by field against the request the Runner sent — seventeen top-level fields across twenty-one equality checks. All must be byte-exact, and a mismatch is a SandboxProtocolInvalid naming the instance_identity operation: provider, placement, state, workspace_id, session_id, run_id, attempt_id, lease_id, lease_epoch, target, profile, platform, root_image, mounts, output_path (always /mnt/session/outputs), expires_at, and materialization, which expands into five: profile_digest equal to the profile’s derived image digest, image_reference equal to root_image, image_digest equal to the digest suffix of root_image, and provider_revision / materialization_revision equal to your own advertised revisions.
Echoing this back correctly is where a naive implementation fails first, and the failure surfaces as an opaque identity protocol error rather than as a pointer to the offending field. Budget for it. Two details cause most of it: expires_at must come back unchanged rather than replaced by whatever deadline your platform actually assigned, and image_digest is derived from root_image rather than reported by your image store.
Snapshots get the same treatment. A snapshot result must agree with the source instance on provider, placement, source_instance_id, workspace_id, session_id, profile_id, profile_version, profile_digest, and must carry exactly the expires_at the request supplied — snapshot retention is governed independently of the instance’s hard expiry, so substituting your own deadline is a protocol failure, not a nicety. A cloneSnapshot result must additionally use a different snapshot_id, bind session_id to the clone request’s target Session, keep the source’s expires_at byte-identical, and have created_at at or after the source’s and strictly before its own expiry.

Allocation handoff

provision, activate, snapshot, cloneSnapshot, release, and resume return { value, rollback } rather than a bare value. rollback is a cold compensation for that exact allocation. The Runner invokes it at most once, and only when it rejects or is interrupted before taking ownership of value. This is what lets validation be strict without leaking: a snapshot whose identity check fails is not merely refused, it is unwound by the party that created it. Three rules follow:
  • You own every partial allocation until the handoff returns. Acquisition is interruption-restored; the moment you return, the Runner masks the transfer so an interruption cannot strand the allocation between acquisition and ownership.
  • rollback must not derive its authority from value. The Runner rejected value precisely because it could not be trusted; the compensation has to know what it allocated on its own.
  • rollback is bounded at 30 seconds. Exceeding it does not hang the Run: the Runner detaches the fiber, fails with a SandboxOperationFailed naming the operation’s _rollback suffix and a timed_out reason, combines it with the original rejection cause, and lets sweep reconciliation and hard expiry settle the orphan.
Delivery is at-least-once with idempotent effect. destroy, destroySnapshot, and rollback may all be invoked again after a lost response; each must settle the same way.

Paths, mounts, and what the Runner admits first

You never have to decide whether a path is legal. The Runner admits it before you are called:
  • Reserved roots. A mount plan overlapping /dev, /proc, /run, /sys, /tmp, /home/checkfu, /workspace/.checkfu, or /mnt/session/outputs is refused at selection as SandboxCapabilityUnavailable{requirement: "mounts"}, as are conflicting mount paths and a count over max_mounts.
  • /workspace/.checkfu is provider-owned. It is the control root for portable state; a profile may not shadow it.
  • /mnt/session/outputs is fixed. Every instance reports it verbatim as output_path.
  • Write scope narrows at activation. During provisioning, writeFile may target any declared mount or the output tree. Once active, only the output tree remains writable through this trusted port.
  • Mount staging is push, not pull. The Runner resolves every mount file from Projects or Git, verifies its digest, and writes it into you one file at a time through writeFile during provisioning. You never fetch from a Checkfu store and hold no credential for one — that is the point of the “stages an immutable Git blob without a sandbox credential” conformance case. Your job is to make the declared mount paths exist and accept the writes.
  • Reads carry their root. readFile and readFiles receive an allowed_root the Runtime derived from the instance’s own mounts. Enforce containment against that root inside the same operation — a symlink escape is yours to refuse, and outside_root is the closed reason for it.
Containment must be physical, not textual. A read must bind resolution and consumption together — resolve to a file descriptor and compare path components as the filesystem sees them — so that nothing can be swapped between the check and the read. Comparing path strings is not sufficient and will not pass conformance.This is a real wall, not a formality. E2B fails this closed today: its SDK exposes no atomic physical-root containment primitive, so its readFile returns SandboxCapabilityUnavailable{requirement: "physically_contained_file_read"} rather than performing a path-based read. If your platform offers only path-based file APIs, you will hit the same wall — better to know now.Failing it closed is legitimate and costs less than it sounds — writeback, capture, and durable sync simply do not run — but it is not free at conformance: the stage-seal-execute case asserts a readFile round trip, so see the granularity warning under Conformance.
  • Roots are separate trust domains in a batch. readFiles requests are grouped by allowed_root and each group is called separately with its own aggregate budget. A batch spanning three mounts is three calls, never one.

Bounds

Enforce these, and expect the Runner to enforce them again on your results. execute additionally refuses a timeout_seconds above the profile’s max_duration_seconds — that check happens in the Runner, so a command reaching you is already inside the admitted envelope.

Cleanup and pinning

cleanup receives now, an orphaned_before cutoff no later than now, and an optional pinned_snapshots list. Sweep instances and snapshots orphaned before the cutoff and return the ids you destroyed. One rule is easy to get backwards: a pinned snapshot is exempt from the opportunistic cutoff but never from its own expires_at. Retention expiry always wins over the pin. A paused Session’s Checkpoint survives an orphan sweep; it does not survive its own deadline.

Optional capabilities: absent versus wrong

Every optional capability defaults to the weakest legal value, and the runtime fails closed on a missing member rather than substituting a permissive one. That makes absence safe and overclaiming dangerous — the two are not symmetric. The pattern: absent is a typed refusal at a known point; wrong is a Run routed to you that either fails late or succeeds while doing less than it claimed. Most of the “wrong” column fails loudly and late. The last two rows — a dishonest egress_certification and an overclaimed network_modes — are the only ones that fail silently, producing a Run that looks successful while a restriction the customer declared did nothing. That is why egress carries a grading rule and the rest does not.

Errors

The adapter error vocabulary is closed. Return one of these; anything else is a defect, not a failure: SandboxCapabilityUnavailable, SandboxFileTooLarge, SandboxInstanceNotFound, SandboxLifecycleConflict, SandboxOperationFailed, SandboxOutputLimitExceeded, SandboxSnapshotForbidden, SandboxSnapshotNotFound. The Runtime adds three of its own that you never author: SandboxAdapterUnavailable and SandboxAdapterAmbiguous (selection could not resolve exactly one adapter) and SandboxProtocolInvalid (your result failed decoding or an identity cross-check). SandboxCapabilityUnavailable.requirement is itself a closed set, and it is the vocabulary selection speaks: tier, platform, base_image, packages, network_policy, retention, snapshots, resources, mounts, physically_contained_file_read, active_mount_write. network_policy is the overloaded one, and it covers three distinct conditions — none of them your bug when you see it:
  1. you do not support the requested mode;
  2. your advertised egress grade does not meet what the policy requires;
  3. the retained profile’s own network policy does not satisfy the current policy shape at all, which the runtime re-checks on activate, execute, resume, and openProcess, not only at selection.
The failure always names the requested policy; your advertised grade names the reason.

openProcess

openProcess is the one member that hands back a live object rather than a value: two readable byte streams, one writable byte sink, an exit that settles once, a control signal, and an idempotent teardown. It is optional and it is also the gate on the richest harness lane. ACP harnesses need streaming stdio — an ACP session is NDJSON frames over a live process — so a provider without openProcess can host the one-shot floor and nothing above it. The request is deliberately smaller than execute’s. A SandboxProcessRequest carries only executable, arguments (≤ 256, each ≤ 65,536 bytes, no NUL), working_directory, and environment. It has no stdin, no timeout_seconds, and no output_limit_bytes — unlike SandboxCommand, which has all three. Do not synthesize substitutes: stdin arrives on the input stream, the lifetime is bounded by the instance’s own expiry and the Run, and output is bounded by the consuming driver — ACP caps each NDJSON frame at 1 MiB, and the one-shot driver caps total collected stdout at 4 MiB. Neither is a cap on the stream you serve. A provider that invents its own idle timeout or output ceiling here will truncate a legitimate long-running session. Requirements:
  • input accepts ordered bytes and a half-close. The one-shot driver writes one prompt and closes; the child’s completion depends on observing EOF.
  • output delivers ordered bytes with a real EOF, nothing lost, nothing duplicated. Chunk boundaries are not observed — re-chunk freely.
  • error is diagnostic only. It is piped to the Runner’s own stderr under an operator debug flag, or discarded. It needs a bound and best-effort delivery; it needs neither exactly-once nor ordering relative to output.
  • wait settles exactly once with an exit code (0–255) xor a signal name, and must be re-readable — a Checkpoint release awaits it under a 5-second bound after sending SIGTERM.
  • signal delivers one control signal. SIGINT, SIGTERM, and SIGKILL are the vocabulary; only SIGTERM is sent today.
  • destroy is idempotent and either interruptible or internally bounded, so rejected-handoff cleanup can settle.
  • evidence is a SandboxProcessEvidence object. It is trusted provider evidence — workload output can never author it.
  • At most one live process per instance, which you enforce. No caller opens two, so there is no multiplexing to implement — but note the enforcement is adapter-side, not runtime-side: the Docker adapter keeps the reservation itself and refuses a second open as SandboxLifecycleConflict{operation: "open_process", actual: "process_already_open"}. The sandbox runtime does not check this for you, so a provider that quietly allows two gets no error, only two processes contending for one instance.
The Runner validates evidence against the instance and against your own capability envelope before transferring the handle, then re-decodes the exit when wait settles.
Putting this member behind a network hop costs three guarantees. Real stdin backpressure becomes an explicit credit window; stdout is lossless only inside a declared replay window, and a long enough disconnection kills a Run that would have survived in-process; and the evidence cross-check degenerates from an isolation check to an identity and self-consistency check. Each is stated exactly, with what fails closed and what does not, in Provider grading and honest limits. The wire design is complete and spiked; two of its frames are specified but unbuilt.

Conformance

Nine cases define a conforming provider. They exist today as an in-tree test port run against a deterministic memory oracle and a real Docker daemon; a partner-runnable command is pending. The behaviors are stable, so build to them. Each case names the capability tokens it requires, and a target declares the tokens it has, so you can predict your own skips from your envelope before running anything: A case whose tokens you do not declare is a recorded skip, never a silent pass — it shows up in the evidence by name.
filesystem is coarser than it looks, and it is the one place the token set will surprise you. It gates both filesystem cases together, but they need different things: stage-seal-execute asserts a readFile round trip through the output tree, while stage-git-project only uses execute.So a provider that accepts mounts but fails readFile closed on physically_contained_file_read has no honest option that costs nothing: declare filesystem and fail stage-seal-execute, or omit it and skip stage-git-project too — a case it would have passed. Plan for physically contained reads and mounts together, or expect one recorded skip you did not need.
Skips are normal, and the in-tree targets prove it. The memory oracle declares every token. Docker declares six and omits network-allowlist, so the allowlist-egress case is a recorded skip against the first-party production provider today. A partner recording skips is in the same position, not a worse one.

A minimum provider

The smallest thing that is a real provider, and what it deliberately gives up:
Failing readFile closed is a legitimate posture, not a shortcut: it is exactly what the shipped E2B provider does today. What that provider cannot do: durable Sessions, Checkpoints, forks from a Checkpoint, memory writeback, Project capture, durable file sync, ACP harnesses, and any profile naming a workload destination. It runs one-shot harnesses in a clean disposable machine, and the capability envelope says so before a Run is ever routed to it. Note this is narrower than the costed day-one tier, which keeps mounts. With max_mounts: 0 and readFile failing closed it declares neither filesystem nor snapshot nor network-allowlist, so it records six skips of nine rather than four. Accepting mounts is the cheapest step back up — it is writeFile into paths you already create — but read the granularity warning under Conformance before assuming it buys back both filesystem cases.

Next steps

  • Provider grading and honest limits — the grading rule, why three of D130’s five facts cannot be self-attested, and the exact cost of the openProcess wire.
  • Sandbox providers — the concept page, the capability envelope in full, and the publication-readiness gaps.
  • Security — the threat model your provider is a component of.