> ## Documentation Index
> Fetch the complete documentation index at: https://checkfu.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Sandbox provider contract

> Every operation a sandbox provider implements, what each must do, what is optional, and what happens when an optional capability is absent versus wrong.

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](/concepts/sandbox-providers) first. For what your provider may claim about
isolation, read [Provider grading and honest limits](/concepts/sandbox-provider-grading).

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.

<Warning>
  **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](#machine-readable-author-kit) for exactly what is pending and
  what the harness-side equivalent looks like.
</Warning>

## 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](/harness-acp-v1-extension.schema.json)
with [golden wire vectors](/harness-acp-v1-extension-vectors.json), and a
[`oneshot-v1` authoring schema](/harness-oneshot-v1-authoring.schema.json) with its
[author-kit vectors](/harness-oneshot-v1-author-kit-vectors.json). 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](#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**.

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
provision ──▶ provisioning ──activate──▶ active ──release──▶ released (durable, with snapshot)
                    │                       │                └──▶ destroyed (ephemeral_zdr)
                 writeFile               writeFile (output tree only)
              (mounts + output)          execute / readFile / readFiles
                                         listDir / listProjectDir / openProcess
                                         snapshot

resume(request, snapshot) ──▶ active          cleanup(now, orphaned_before) ──▶ swept ids
```

`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](#allocation-handoff) and it matters more than it looks.

| Operation          | Required | Accepts                     | Must do                                                                                                                                                                    | Runtime rejects                                                                                                                  |
| ------------------ | -------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `capabilities`     | yes      | —                           | Advertise one honest envelope, unchanging for this revision                                                                                                                | a cross-field violation at decode                                                                                                |
| `provision`        | yes      | a `SandboxProvisionRequest` | Allocate the instance, materialize the derived image, prepare declared mounts; return it in `provisioning`                                                                 | any identity field disagreeing with the request                                                                                  |
| `writeFile`        | yes      | `provisioning` or `active`  | Write bytes at a path the Runner already admitted                                                                                                                          | a path outside every mount and the output tree; in `active`, any path outside the output tree                                    |
| `beginActiveWrite` | **no**   | `active`                    | Stage and atomically publish a bounded regular-file batch inside one read-write mount from provider custody; rollback or retain retry evidence before the workload resumes | a retained/non-current path, an ancestor conflict, a path outside a read-write mount, or a provider without `active_mount_write` |
| `activate`         | yes      | `provisioning`              | Seal staged mounts and make the instance runnable; return it in `active`                                                                                                   | the same identity fields, now with `state: "active"`                                                                             |
| `execute`          | yes      | `active`                    | Run one bounded command to completion and report its settlement                                                                                                            | a result with two exit causes or none; a non-monotonic interval; stdout or stderr over `output_limit_bytes`                      |
| `readFile`         | yes      | `active`                    | Read one file, enforcing **physical** containment under the supplied `allowed_root`, or fail closed on `physically_contained_file_read`                                    | content over the caller's `maximum_bytes`                                                                                        |
| `readFiles`        | **no**   | `active`                    | Native batch read within the exact supplied aggregate budget                                                                                                               | a result that is not the closed per-file union, or that overruns the group or request budget                                     |
| `listDir`          | **no**   | `active`                    | Enumerate regular files under a mount root with size and SHA-256                                                                                                           | more entries than `max_entries`                                                                                                  |
| `listProjectDir`   | **no**   | `active`                    | Complete leaf enumeration — special files named, never omitted                                                                                                             | a listing that is not `complete: true`, or over `max_entries`                                                                    |
| `snapshot`         | yes      | `active`, durable only      | Capture instance state under the supplied retention deadline                                                                                                               | a ref disagreeing on source instance, Session, profile, or `expires_at`                                                          |
| `cloneSnapshot`    | yes      | —                           | Physically clone into a distinct target Session with no dependency on the source                                                                                           | a clone that reuses the source id, moves the expiry, or predates the source                                                      |
| `release`          | yes      | `active`                    | Durable: settle a snapshot. ZDR: destroy every trace                                                                                                                       | a `released` disposition on a ZDR profile, or `destroyed` on a durable one                                                       |
| `resume`           | yes      | —                           | Rehydrate a snapshot into a fresh `active` instance                                                                                                                        | a snapshot/request mismatch on provider, placement, Workspace, Session, or profile                                               |
| `destroy`          | yes      | any                         | Idempotent teardown of one instance                                                                                                                                        | —                                                                                                                                |
| `destroySnapshot`  | yes      | any                         | Idempotent teardown of one snapshot                                                                                                                                        | —                                                                                                                                |
| `cleanup`          | yes      | —                           | Sweep instances and snapshots orphaned before a cutoff                                                                                                                     | a result that is not two id arrays                                                                                               |
| `openProcess`      | **no**   | `active`                    | Start a long-lived process and expose stdio as live streams                                                                                                                | evidence mismatch, or streams that are not real `WritableStream`/`ReadableStream`                                                |

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.

<Note>
  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.
</Note>

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.

<Warning>
  **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](#conformance).
</Warning>

* **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.

| Bound                                  | Value                                                       |
| -------------------------------------- | ----------------------------------------------------------- |
| Any file content                       | 16 MiB                                                      |
| `readFile.maximum_bytes`               | ≤ 16 MiB                                                    |
| `readFiles` files per request          | ≤ 1,024                                                     |
| `readFiles` aggregate output           | ≤ 128 MiB, and exactly the overflow-safe sum of the members |
| `execute.output_limit_bytes`           | ≤ 16 MiB, per stream                                        |
| Command arguments                      | ≤ 256, each ≤ 65,536 bytes, no NUL                          |
| Environment keys                       | `^[A-Za-z_][A-Za-z0-9_]*$`                                  |
| `listDir.max_entries` and listing size | ≤ 100,000                                                   |
| Mounts per instance                    | ≤ 64                                                        |
| `cleanup.pinned_snapshots`             | ≤ 1,024                                                     |
| Timestamps                             | canonical UTC ISO-8601 that round-trips byte-identically    |
| Digests                                | `^sha256:[0-9a-f]{64}$`                                     |

`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.

| Capability                      | Absent                                                                                                                                                                                                                                                        | Wrong                                                                                                                                                                                                                                                                                                      |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `list_dir: false`, no `listDir` | `SandboxLifecycleConflict{list_dir_unsupported}` at call time. Memory writeback records a **visible skip** rather than silently dropping the agent's edits                                                                                                    | `list_dir: true` with `listDir` undefined refuses identically — but selection already routed work to you that another provider could have served                                                                                                                                                           |
| `listProjectDir`                | same typed refusal; Project capture does not run                                                                                                                                                                                                              | a listing that omits special files instead of naming them `symlink`/`submodule`/`device`/`socket`/`fifo`/`other`, or one not marked `complete: true`, is a protocol failure — silently dropping a leaf is the failure this member exists to prevent                                                        |
| `readFiles`                     | the Runtime falls back to sequential `readFile` per member. Identical results, more round trips                                                                                                                                                               | out-of-order, short, or over-budget results fail the **whole batch** as `SandboxProtocolInvalid`                                                                                                                                                                                                           |
| Physically contained reads      | `readFile` fails closed on `physically_contained_file_read`. Nothing moves out of the sandbox through the trusted port — no memory writeback, no Project capture, no durable file sync — and every one of those gaps is typed and visible. E2B ships this way | a path-string comparison that *appears* to work leaves a check-to-read swap open on the one operation whose entire job is containment                                                                                                                                                                      |
| `snapshots: false`              | durable profiles never select you; a clone attempt fails `SandboxCapabilityUnavailable{requirement: "snapshots"}`                                                                                                                                             | `true` without working capture means a durable Run selects you and fails at Checkpoint time, mid-Session                                                                                                                                                                                                   |
| `snapshot_clone` omitted        | `false`: a clone attempt fails `SandboxCapabilityUnavailable{requirement: "snapshot_clone"}`. Capture still works, so durable profiles can still select you                                                                                                   | capture and clone are **different** provider operations — you may retain bytes you cannot re-hydrate. Declaring `true` without working clone means fork-from-Checkpoint selects you and fails. `true` with `snapshots: false` is refused by the envelope filter: a clone source is a snapshot you produced |
| `snapshot_custody` omitted      | `runner`: Checkpoint clone requires that exact Runner alive                                                                                                                                                                                                   | claiming `provider` without platform-side retention breaks fork-from-Checkpoint after Runner churn                                                                                                                                                                                                         |
| `openProcess`                   | ACP harnesses cannot run on your provider; the one-shot floor still can                                                                                                                                                                                       | streams that are not real `WritableStream`/`ReadableStream`, or evidence disagreeing with the instance, fail `open_process_identity`; the Runner then destroys the process under a 30 s bound and combines both causes                                                                                     |
| `egress_certification` omitted  | `unsupported`: an `allowlist` profile will not select you                                                                                                                                                                                                     | claiming `advisory_filter` while ignoring the destination set means the customer's declared restriction silently does nothing — the worst failure mode in this table                                                                                                                                       |
| `network_modes` overclaimed     | —                                                                                                                                                                                                                                                             | a `none` profile selects you and the workload reaches the internet under a policy that said it could not                                                                                                                                                                                                   |

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.

<Warning>
  **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](/concepts/sandbox-provider-grading#what-openprocess-costs-across-a-wire).
  The wire design is complete and spiked; two of its frames are specified but unbuilt.
</Warning>

## 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](#from-adapter-to-qualified-provider). 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:

| Case                  | Requires                | Proves                                                                   |
| --------------------- | ----------------------- | ------------------------------------------------------------------------ |
| `stage-seal-execute`  | `filesystem`, `process` | declared mounts stage, read-only data seals, command output is bounded   |
| Immutable Git staging | `filesystem`, `process` | an immutable Git blob stages **without a sandbox credential**            |
| Checkpoint clone      | `snapshot`              | a clone lands in an independent target-Session snapshot                  |
| Allowlist egress      | `network-allowlist`     | only the declared outbound hosts are reachable                           |
| Durable round trip    | `snapshot`              | files survive snapshot → release → resume                                |
| `none` egress         | `network-none`          | outbound traffic is denied for a `none`-network profile                  |
| ZDR release           | `zdr`                   | ZDR snapshots are forbidden and release destroys all state               |
| Orphan reclaim        | `cleanup`               | expired instances are reclaimed without a database reset                 |
| Pause pinning         | `snapshot`, `cleanup`   | a pause-pinned snapshot survives orphan cleanup until its **own** expiry |

A case whose tokens you do not declare is a **recorded skip**, never a silent pass — it shows
up in the evidence by name.

<Warning>
  **`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.
</Warning>

<Note>
  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.
</Note>

## A minimum provider

The smallest thing that is a real provider, and what it deliberately gives up:

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
capabilities:
  placement:            hosted
  tiers:                ["container"]
  platforms:            [{ os: "linux", architecture: "amd64" }]
  image_mode:           "profile"
  package_managers:     []                    # no materialization
  network_modes:        ["none"]              # plane route only, no workload destinations
  retention_modes:      ["ephemeral_zdr"]     # forced by snapshots: false
  snapshots:            false
  snapshot_custody:     "runner"              # forced by snapshots: false
  list_dir:             false
  egress_certification: "unsupported"         # honest at network_modes: ["none"]
  enforcement:          { ...nine axes, none of them runner_enforced }
  max_mounts:           0

implement:    provision, writeFile, activate, execute,
              release (always "destroyed"), destroy, destroySnapshot, cleanup
fail closed:  readFile → SandboxCapabilityUnavailable{physically_contained_file_read}
              unless your platform can bind resolution and consumption atomically
stub:         snapshot, cloneSnapshot, resume — unreachable while snapshots: false
omit:         readFiles, listDir, listProjectDir, openProcess
```

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](#conformance) before assuming it buys back both `filesystem` cases.

## Next steps

* [Provider grading and honest limits](/concepts/sandbox-provider-grading) — the grading rule,
  why three of D130's five facts cannot be self-attested, and the exact cost of the
  `openProcess` wire.
* [Sandbox providers](/concepts/sandbox-providers) — the concept page, the capability envelope
  in full, and the publication-readiness gaps.
* [Security](/concepts/security) — the threat model your provider is a component of.
