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.
Machine-readable author kit
The harness author path ships a checked-in kit generated from the same Effect Schemas the control plane decodes: anacp-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
RunnerProviderCapabilityentries 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 missingenforcementandegress_certificationceilings 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.
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 aSandboxProtocolInvalid 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.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.
rollbackmust not derive its authority fromvalue. The Runner rejectedvalueprecisely because it could not be trusted; the compensation has to know what it allocated on its own.rollbackis bounded at 30 seconds. Exceeding it does not hang the Run: the Runner detaches the fiber, fails with aSandboxOperationFailednaming the operation’s_rollbacksuffix and atimed_outreason, combines it with the original rejection cause, and lets sweep reconciliation and hard expiry settle the orphan.
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/outputsis refused at selection asSandboxCapabilityUnavailable{requirement: "mounts"}, as are conflicting mount paths and a count overmax_mounts. /workspace/.checkfuis provider-owned. It is the control root for portable state; a profile may not shadow it./mnt/session/outputsis fixed. Every instance reports it verbatim asoutput_path.- Write scope narrows at activation. During
provisioning,writeFilemay target any declared mount or the output tree. Onceactive, 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
writeFileduringprovisioning. 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.
readFileandreadFilesreceive anallowed_rootthe Runtime derived from the instance’s own mounts. Enforce containment against that root inside the same operation — a symlink escape is yours to refuse, andoutside_rootis the closed reason for it.
- Roots are separate trust domains in a batch.
readFilesrequests are grouped byallowed_rootand 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:
- you do not support the requested mode;
- your advertised egress grade does not meet what the policy requires;
- 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, andopenProcess, not only at selection.
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:
inputaccepts ordered bytes and a half-close. The one-shot driver writes one prompt and closes; the child’s completion depends on observing EOF.outputdelivers ordered bytes with a real EOF, nothing lost, nothing duplicated. Chunk boundaries are not observed — re-chunk freely.erroris 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 tooutput.waitsettles 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.signaldelivers one control signal.SIGINT,SIGTERM, andSIGKILLare the vocabulary; only SIGTERM is sent today.destroyis idempotent and either interruptible or internally bounded, so rejected-handoff cleanup can settle.evidenceis aSandboxProcessEvidenceobject. 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.
evidence against the instance and against your own capability envelope
before transferring the handle, then re-decodes the exit when wait settles.
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.
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: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
openProcesswire. - 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.