| title | Sandbox deployment |
|---|
The sandbox deployment selects the Sandbox Provider and the per-sandbox resources for Core-managed openai_hosted execution. PostgreSQL holds one active selection per installation; Web and the Core API write the same configuration. A node's files hold an installed copy of it plus host-specific paths and cannot override its resources. The Runtime is not part of the selection: each node runs the Runtime its own installation verified. The selection is independent of the Harness. A deployment can stay unconfigured, with no nodes; it then refuses hosted admission.
This contract owns the Core API routes below and their semantics. The nodes guide owns the operator workflow, the machine connection API the routes nodes call, and the sandbox node protocol the node connection.
Every route requires the Core key. Web's console server adds it on the server side for signed-in requests.
| Route | Effect |
|---|---|
GET /core/v1/sandbox/deployment |
Read the safe active configuration, rollout, reset and resource counts |
PATCH /core/v1/sandbox/deployment |
Update the deployment-wide idle timeout without changing the configuration generation |
POST /core/v1/sandbox/deployment |
Select the initial provider and resources |
PUT /core/v1/sandbox/deployment |
Change the same provider's target online |
POST /core/v1/sandbox/deployment/reset |
Start or escalate a durable clear of hosted resources |
DELETE /core/v1/sandbox/deployment/reset?expected_generation=N |
Cancel the remaining clear |
POST /core/v1/sandbox/providers/{provider}/discovery |
Query a provider's configuration catalog with a transient credential |
POST /core/v1/sandbox/enrollment-tokens |
Issue a one-use node enrollment token with optional node limits |
GET /core/v1/sandbox/nodes |
List registered nodes |
GET /core/v1/sandbox/nodes/{node_id} |
Read one node with its host observations and history |
PATCH /core/v1/sandbox/nodes/{node_id} |
Change a node's name and capacity |
DELETE /core/v1/sandbox/nodes/{node_id} |
Remove a node |
GET /core/v1/sandbox/nodes/{node_id}/allocations |
List a node's unreleased allocations |
GET /core/v1/sandbox/runtime-observations |
Current Runtime observations; see the Runtime telemetry API |
POST and PUT take the same complete selection and require expected_generation from a preceding GET. Zero is valid for the initial unconfigured deployment; omitted or null is invalid. A stale generation is checked before reset, provider, resource and same-selection conditions, even for a body identical to an earlier request.
| Field | Meaning |
|---|---|
expected_generation |
Required nonnegative integer from GET; never refresh and replay it automatically |
provider |
Exactly one of microsandbox, e2b, opensandbox |
resources |
Per-sandbox limits, below; required for microsandbox and OpenSandbox, optional for E2B |
network |
The egress cap, below. Omitted on POST means the default; omitted on PUT keeps the current cap |
configuration |
The provider's public selectors. E2B: the immutable template build and the optional paired api_url and domain. OpenSandbox: api_url and image. microsandbox accepts only {} or omission |
credential |
The provider's write-only credential. E2B and OpenSandbox: {api_key}, required at first setup and omitted on PUT to keep the current key; a null or empty key is invalid. microsandbox rejects it |
The request has no Core address. Core derives the deployment's core_url from the installation public URL, OAC_PUBLIC_URL: the origin nodes and sandbox guests use to reach Core. A request that contains core_url is rejected with 400 invalid_request like any other unknown member. E2B guests reach Core from E2B's cloud and OpenSandbox guests from the OpenSandbox server's hosts, so an E2B or OpenSandbox selection is rejected with 409 sandbox_configuration_error while the public URL is loopback. A microsandbox selection accepts a loopback public URL, which serves only local development because a guest's loopback address does not reach its host. Changing the public URL is an installation change; see Change the public URL.
| Field | Accepted value |
|---|---|
cpus |
Integer, 1 through 255 |
memory_mib |
Integer, 512 through 1048576 MiB |
root_disk_mib |
microsandbox: at least 1024 MiB; E2B and OpenSandbox: omitted or zero |
environment_disk_mib |
microsandbox: each new workspace's admitted capacity on workspace storage, where zero (the default) requests no quota and a positive value must be at least 1024 MiB and requires enforced quotas; E2B and OpenSandbox: omitted or zero |
These limits describe each sandbox. A node's max_active and max_retained are separate reservation limits, set as Node capacity describes; no host measurement permits exceeding either once set. Native providers may reject values that pass these bounds.
E2B CPU and memory must equal the exact ready template build, which Core validates through the pinned SDK before saving; disk capacity stays part of the template. E2B accepts no disk quota it cannot enforce. An E2B selection may omit resources: Core then stores the build's CPU count and memory as cpus and memory_mib, returned in specification.resources without disk fields. On restart Core loads the committed E2B selection without validating the template build again, so an E2B outage never blocks inspection or cleanup; new selections still require validation.
OpenSandbox applies cpus and memory_mib as the sandbox's cpu and memory resource limits; its runtime sets the disk.
microsandbox configures the CPUs, memory and a managed root disk. It runs every Environment on workspace storage mounted at /environment. The microsandbox helper describes how restore handles these limits.
The optional response specification.workspace is an immutable capability receipt derived from the selected workspace filesystem, not an independently writable setting. It contains only attachment, user_xattr and capacity_quota; filesystem configuration, paths and credentials stay outside the sandbox specification. Core validates the combined declarations before saving, and retained generation validation and node construction use this receipt. microsandbox always has the receipt, and a deployment saved before workspace storage is selected returns workspace_storage_required. E2B and OpenSandbox have no receipt and keep their own disks. Create and Resume provide a binding exactly when the frozen generation has the receipt.
Select the filesystem configuration before the microsandbox deployment. Selecting the filesystem does not alter existing generations or allocations; the next sandbox update derives the receipt. A filesystem selection must support the provider's declared requirements. An existing deployment requires the same attachment and user_xattr; the selection may change capacity_quota, after which new workspaces are refused until a sandbox update sets a matching environment_disk_mib. Existing workspaces keep the capacity they were admitted with.
network caps what every sandbox of the deployment may reach when its Environment's network.access is enabled or omitted. The connection to Core and the Session's model provider port on public addresses are always reachable, because the Harness runs inside the sandbox. To reach a model provider at a private address, list its host name, or a *. entry that covers it, in domains. MCP connections from the Environment follow the cap like any other traffic.
| Field | Accepted value |
|---|---|
public |
Boolean. true allows public addresses; false allows only Core, the model provider and domains |
domains |
At most 100 entries of at most 253 characters, each an exact host name or *. and a host name of at least two labels, in lowercase letters, digits, hyphens and dots, whose last label starts with a letter. *.example.com covers the subdomains of example.com at any depth but not example.com itself, which needs its own entry; other wildcards, IP literals, ports and URLs are rejected. Core stores them lowercase, deduplicated and sorted. Every address a name resolves to is allowed, private addresses included |
The default is {"public": true, "domains": []}. Under every cap, loopback, link-local and cloud-metadata addresses and all other private addresses stay blocked, except that E2B cannot block the metadata address 169.254.169.254 and its sandboxes cannot reach private networks at all. A sandbox keeps the cap of the generation that created it.
Each Provider declares what it enforces (Sandbox Provider protocol): microsandbox supports both fields, E2B supports public: false but not domains because E2B sandboxes cannot reach private networks, and OpenSandbox supports public: false but not domains because its deny rules for private ranges win over a domain's private answers. Those deny rules also win over the connection to Core, so OpenSandbox needs the installation public URL at global addresses. When it resolves to a private one, every OpenSandbox sandbox fails to start. A cap the selected Provider does not declare returns 400 invalid_sandbox_configuration with param network.public or network.domains.
E2B uses configuration.template in template-id:build-uuid form; the build UUID must be canonical and nonzero, and a mutable template alias alone is refused. The API key is encrypted in PostgreSQL and never appears in a response, bootstrap configuration, command argument or log. By default Core uses https://api.e2b.app and e2b.app. For a compatible service, set both configuration.api_url (an HTTPS origin without path, port, query, fragment or credentials) and configuration.domain (the sandbox data-plane DNS suffix); the API host must equal the domain or be a subdomain of it. Core rejects a sandbox whose data-plane domain lies outside the selected suffix before sending daemon credentials or using envd.
OpenSandbox uses configuration.api_url, the HTTP or HTTPS origin of the OpenSandbox server without path, query, fragment or credentials, and configuration.image, the Runtime image built for OpenSandbox as its adapter guide describes. The API key is encrypted in PostgreSQL and never appears in a response, bootstrap configuration, command argument or log. Core does not check the image. The initial selection does not contact the server, and a later change verifies the saved or new key as key replacement describes. Each Create fails when the image cannot start, the server's egress sidecar does not run in dns+nft mode or the installation public URL resolves to a non-global address.
POST /core/v1/sandbox/providers/{provider}/discovery takes exactly configuration, credential and query objects, at most 64 KiB, and runs for at most 30 seconds. Unknown members and null objects are rejected. The credential is used only for that request and is never stored or returned; discovery saves nothing, changes no deployment, allocates nothing and never proves that a selection will be admitted. microsandbox and OpenSandbox reject it with 400 sandbox_operation_unsupported.
Post {"configuration": {…}, "credential": {…}, "query": {"field": "…", "path": […]}}, where field is a field the provider declares as discovered and path holds the values selected at the earlier levels. The result is {"options": [{"value": "…", "label": "…", "resources": {"cpus": 2, "memory_mib": 2048}, "complete": true}]}, possibly empty. An option with complete: true can be saved as the field's value; the others open the next level. Any other field returns 400 invalid_request_error.
E2B declares template as discovered. An empty path lists the key's templates, and ["template-id"] lists that template's ready builds as template-id:build-uuid values with their CPU count and memory; the official endpoint may omit api_url and domain. The pinned SDK helper reads GET /v2/templates and returns at most 200 results; a limit or provider failure returns 503 with a generic message. The deployment write validates the selected build separately.
GET and successful writes return installation_id, provider, core_url (read-only: the installation public URL, present even before configuration), mode, generation, owner_epoch, reset, rollout, idle_seconds, suspension, resources and credential_configured. A configured deployment also returns specification, specification_digest, configuration and metadata: the adapter's public projection of its selectors and of the observations it recorded, never raw stored values or secrets. E2B returns configuration.template, configuration.api_url, configuration.domain and, once recorded, metadata.template_build. OpenSandbox returns configuration.api_url, configuration.image and an empty metadata object. microsandbox returns empty configuration and metadata objects and credential_configured: false; an unconfigured deployment has neither object.
metadata.template_buildis{status, resources: {cpus, memory_mib, root_disk_mib}}: the build as Core read it through the pinned SDK when the selection was saved. GET never calls E2B, so it stays cheap during an E2B outage. Validation admits only areadybuild whose CPU count and memory equal the selected values;root_disk_mibis the build's native disk size, which Core does not enforce. Unknown values are null,metadata: {}means no observation was recorded, and an identical PUT without a credential does not refresh it.idle_secondsis the deployment's idle timeout, for every Provider.suspensionis{retention_seconds}, Core's suspension policy, when the selected Provider declares checkpoint support; any other deployment, including an unconfigured one, returns null.- Request
resourcesand responsespecification.resourcesare per-sandbox limits. Responseresources.allocationsandresources.pendingcount unreleased allocations and pending hosted Environments without an allocation;resources.runningcounts the unreleased allocations whose compute is running, so it excludes suspended ones. - An unconfigured deployment has an empty provider and no specification. microsandbox uses
mode: nodes; E2B and OpenSandbox usemode: direct, without a synthetic node. generationidentifies the saved selection.owner_epochfences the execution owner and node connections; it does not replaceexpected_generation.specification_digestis the server's identity of the provider, limits and derived workspace capability receipt; enrollment echoes it unchanged.
The typed SandboxAdminClient in packages/agents-client checks the deployment, node list, node detail and allocation responses against exactly these shapes. An unknown or missing member, or a wrong type, rejects the whole response with a 502 invalid_admin_response error. A node's rollout.diagnostic is absent or a code, never empty, and the client reads an unknown code as provider_unavailable.
POST validates a candidate before persisting it and creates no compute, Session or model request. At the current generation an identical selection is a no-op; an old generation returns 409 sandbox_generation_stale, even for the same body. Missing prerequisites or a failed preparation leave the committed provider in place. A different backend requires a reset first. POST also initializes after a completed reset, using the reset's new generation.
PUT accepts the same provider while no reset is active. Send the observed generation once and never replay an uncertain write automatically. New allocations use the newly committed specification, and existing allocations keep their immutable deployment generation. A same-provider change retires no node, token or owner epoch and drains no execution: microsandbox nodes prepare the new target independently while serving their old pin, and E2B and OpenSandbox changes apply at once.
Omit credential on PUT to keep the current key; an identical selection without it is a no-op. Submitting a key, even the same one, always verifies it and advances the generation; a null or empty key is invalid. A key-only change uses the same full body (provider, existing configuration, optional resources, expected_generation and the new credential), with no separate route or implicit reset.
Initial setup requires the selected template to appear in the key's team-owned template listing; public readability is not enough. Before an online change, Core verifies that the committed key owns the current template, then requires the candidate key to own that exact template, which anchors team ownership. It also reads the candidate build and every retained build at its original endpoint with the candidate key, and confirms each settled live receipt in the installation-labelled sandbox listing.
| Result | Meaning |
|---|---|
409 sandbox_reset_required |
The current selection has no team ownership anchor, or the committed key no longer authenticates |
409 sandbox_credential_ownership |
The candidate key belongs to another team; reset before initializing another team |
400 sandbox_credential_invalid |
The candidate key gets 401 or 403 |
400 sandbox_configuration_invalid |
The candidate build is invalid or does not match the resources |
503 sandbox_verification_unconfirmed |
A receipt is missing or unsettled, or a read is unconfirmed |
No provider text or credential is returned. The write and its change or replace_credential audit entry share one transaction. A replacement briefly fences provider calls, waits for helper processes to actually exit even after the caller cancelled, and verifies again before committing; a helper's exit does not prove that a remote Create settled. The fence and reads are bounded, and a failure keeps the old key and lifecycles. After the successful response, all retained-generation management uses the committed key; only then revoke the old key in E2B, never before cleanup. Template and resource changes do not drain lifecycles.
The request rules are those of E2B key replacement. The candidate key must authenticate with the server of every retained generation and list the sandboxes of each unreleased allocation; an empty listing proves nothing. A 401 or 403 returns 400 sandbox_credential_invalid, and any other failure 503 sandbox_verification_unconfirmed, including an allocation whose sandboxes are gone. Server URL and image changes do not drain lifecycles.
PATCH /core/v1/sandbox/deployment accepts {"idle_seconds": 300}. idle_seconds is a required integer from 30 to 86400 seconds, defaulting to 300, stored in Core's database. Every configured Provider accepts this change; unconfigured or resetting deployments reject it. Success returns the deployment view with the saved idle_seconds.
With checkpoint support the idle timeout starts suspension. Without it, an allocation that a Session holds keeps running; Lifecycle without a Session owns what the timeout does to an unattached Environment.
Changes apply to existing and new running allocations on the next lifecycle pass, without a Core restart, generation change or node rollout. Suspension already in progress is not cancelled; existing snapshots and the 24-hour retention period are unchanged. Capacity-pressure reclamation can still suspend an idle allocation earlier and, when retained slots are full, end an eligible checkpoint's retention early. Web manages this deployment-wide setting in Settings → Sandbox and scheduling → Automatic suspension, or Idle timeout when the Provider has no checkpoint support; each node's capacity limits stay node-owned and are edited on Nodes with Edit node.
runtime_deployment holds the current specification. Superseded rows keep only immutable specification, build and endpoint metadata, never another provider key. A direct allocation (E2B, OpenSandbox) binds its generation at reservation; a node placement binds when the scheduler reserves compatible capacity, and its allocation copies that generation, even across later updates. Inspection, renewal, commands and cleanup use the allocation's original specification and endpoint with the current key; a missing generation never falls back to the current specification. Released generation identifiers stay reserved.
A generation is retained while it is current, referenced by an unreleased allocation or placement, or pinned by a node that is not removed. A node's durable serving pin survives offline periods and zero resources and is separate from its current readiness. Collection shares the deployment lock with updates and admission and deletes at most 32 eligible generation rows per pass. Reset retires pins and clears superseded rows only after confirmed release.
Every deployment response includes rollout:
"rollout": {"state": "settled", "previous_generation_sandboxes": 3, "nodes": null}previous_generation_sandboxes counts, from the same snapshot as the resource totals, unreleased allocations and allocation-less pending placements of older generations, never one resource twice. Direct (E2B, OpenSandbox) and unconfigured deployments return null nodes. A node deployment returns nodes as counts {ready, preparing, failed, unknown}, and every node that is not removed falls in exactly one bucket:
- a node that is offline on its current connection is
unknown; - otherwise the exact target observation on the current connection and owner epoch gives
ready,preparingorfailed, and a missing observation givesunknown.
flowchart TD
A{"Online on its current connection?"} -- no --> U["unknown"]
A -- yes --> C{"Target observation on this connection and epoch"}
C -- ready --> R["ready"]
C -- preparing --> P["preparing"]
C -- failed --> F["failed"]
C -- missing --> U
A node is online when it is connected under the current owner epoch with a fresh heartbeat. Target rollout is independent of serving readiness: unknown, preparing or failed does not remove an independently confirmed older serving generation. provider_ready requires online presence and an exact observation of the serving generation on that connection and epoch. Pins alone never imply readiness.
Each node adds rollout: {state, ready_generation, diagnostic?}, where ready_generation is the nullable durable serving pin and diagnostic a fixed code for the target generation; allocation items add deployment_generation. Poll every five seconds only while rollout.state is preparing or reset is not null; old Sessions and failed or offline nodes alone do not keep polling active.
Node-backed creation validates the target deployment and commits the Session, pending Environment and any initial input without reserving compute. A full, offline or preparing fleet leaves that accepted work waiting; an unconfigured deployment, reset or unsupported combination still rejects admission. The common scheduler uses bounded rotating scans of unplaced demand, with pages ordered by recorded time and Environment ID and a fixed time boundary for each scan so continuous arrivals cannot prevent it from revisiting older work. It checks online presence, exact serving-generation readiness, address, shared capacity and the selected generation's Harness/filesystem compatibility, then prefers the newest qualifying pin. Placement is immutable once reserved until confirmed release or a settled checkpoint transfer. A bounded scan skips temporarily unavailable demand and resumes after a restart. If preparation fails before a new allocation is reserved, Core releases only that attempt’s node reservation, retaining any filesystem identity. Its next placement demand is ordered no earlier than release time, behind already-waiting work; successful retries use the newly selected generation’s specification. An allocation with an unknown compute outcome keeps its reservation. Existing suspended allocations reserve eligible checkpoint capacity through the same lock under the checkpoint transfer contract; this is not a global fairness guarantee across hot restores and unplaced work. Input retains its original five-minute deadline, including time waiting for capacity.
To change backend, start a reset:
{"expected_generation": 7, "clear": "auto", "deadline_seconds": 3600}clear is required: auto or force. deadline_seconds applies to auto, defaults to 3600 and accepts 300 to 86400; force must omit it. Core persists an absolute deadline and the requester's audit provenance before it closes fresh hosted admission, and the execution owner advances the clear after the request ends and across restarts. auto archives idle hosted Sessions, including queued or pending work and suspended sandboxes, and waits for root and Subagent Turns that are in progress or waiting and for pending file writes, rechecking under the Session lock. At the persisted deadline it escalates to force durably. force uses the ordinary archive cancellation and cleanup path for every eligible hosted Session. Self-hosted Sessions are never touched. A reset also expires every unattached standalone Environment and requests its cleanup.
stateDiagram-v2
direction LR
[*] --> inactive
inactive --> auto: POST auto
inactive --> force: POST force
auto --> force: deadline or POST force
auto --> inactive: DELETE
force --> inactive: DELETE
auto --> unconfigured: no held resources
force --> unconfigured: no held resources
Starting again at the same generation and mode keeps the original deadline. auto can escalate to force, never the reverse. DELETE with the current expected_generation cancels the remaining work and reopens admission; it does not undo archives, revive expired Environments or cancel cleanup already requested. DELETE without an active reset is a no-op, and a stale request returns 409.
reset is null when inactive, otherwise:
{"clear": "auto", "requested_at": "2026-09-27T12:00:00Z",
"deadline_at": "2026-09-27T13:00:00Z", "forced_at": null,
"remaining": {"busy": 2, "idle": 1, "cleanup": 3, "on_offline_nodes": 2,
"offline_nodes": [{"node_id": "node-uuid", "name": "worker", "resources": 2}]}}One database snapshot partitions every unreleased allocation and every pending hosted Environment without an allocation into cleanup first, then busy or idle, so busy + idle + cleanup == resources.allocations + resources.pending. Deleted or expired Sessions with unreleased receipts count as cleanup. offline_nodes is the complete, ID-sorted list of offline nodes that hold resources by allocation or active placement, and its sum equals on_offline_nodes; presence is the online test, not provider readiness. Direct E2B and OpenSandbox resources have no node. Offline resources stay blockers until cleanup confirms their release.
When both held counts reach zero, the owner drains and atomically clears the provider, mode, specification, provider configuration, credential and metadata and provider policy, retires nodes and unused enrollment tokens, increments the generation and owner epoch, and records reset_complete. The installation identity and history remain. Core immediately publishes the unconfigured state and keeps the new generation even with no provider, so a delayed load cannot revive the old one. Configure again with POST and the returned generation; no restart is needed.
During a reset, fresh hosted admission returns 503 sandbox_reset_in_progress and leaves no provisional Session rows; live input, receipt retries, restoration and cleanup continue. Management writes and new enrollment return 409 sandbox_reset_in_progress, while registered nodes can still read their configuration to recover for cleanup. Per-Session archive needs only the current generation and works with or without a reset. It keeps history and persisted Files and Artifacts, discards the unpersisted workspace and prevents the Session from resuming; poll the archive GET for the actual release. A reset never fabricates a release receipt.
A force archive fences credentials and new work at once. When the hosted delivery of the Turn is still connected, Core keeps only that delivery's native cancellation and terminal receipt path open until the terminal commit, for at most 20 seconds from the original cancellation request; done does not end the bound while a cancellation acknowledgement or terminal commit is pending. This drain never authorizes reconnection, workspace or MCP access or further execution, and an explicit credential revocation ends it. Missing or failed receipts keep honest failure outcomes, and a disconnected, expired or restarted owner falls back to ordinary provider cleanup.
One mutation gate serializes setup, PUT, reset, cancellation and finalization. Archive locks the Session before the deployment, and finalization never reverses that order. Candidates bind to the reset request time, so a cancel followed by a new reset at the same generation cannot reuse old work. Never replay a rejected or uncertain write: read the current state and decide again.
Node capacity is set separately from the deployment specification. POST /core/v1/sandbox/enrollment-tokens accepts optional max_active and max_retained and returns {token, expires_at, enrollment_id}; enrollment_id is a public handle of that command, never a credential. Core stores the token with the limits as sent, and an omitted limit stays unset. When the node enrolls, Core copies each explicit limit to it unchanged and sets each unset limit to the recommended capacity of the host measurements in the enrollment request and the current specification; an unset limit with no host measurements fails with 409 sandbox_node_host_unmeasured and leaves the token unused. When only max_retained is set, the derived max_active never exceeds it; when only max_active is set, the derived max_retained is raised to at least max_active. microsandbox uses both limits. E2B and OpenSandbox have no nodes and answer 409 sandbox_deployment_conflict. A node cannot submit limits, but an unset limit follows the host measurements the node reports at enrollment, bounded by 1 and 1000000. Its local checks may refuse a deployment it cannot run but never raise the limits. Explicit values and later PATCH edits bind: once a node is enrolled, Core derives nothing again. Node capacity explains the limits for operators.
GET /core/v1/sandbox/nodes returns {data: [...]} with, for each node: id, name, provider, online, last_seen_at, created_at, max_active, max_retained, the counts active, reserved, running, retained, snapshots and cleanup_pending, provider_ready, the host measurements cpu_count, available_memory_bytes and available_disk_bytes, rollout, recommended_capacity, enrollment_id and core_url. recommended_capacity is {max_active, max_retained}, the recommended capacity of the node's latest health and the current specification, or null until the node has reported a CPU count or effective cores and its total memory, and while the current specification does not validate. enrollment_id is the handle of the command that registered the node, or null when Core has none. core_url is the installation public URL at enrollment; a node whose core_url differs from the current public URL receives no new placements. Work already placed on it finishes there, including a placed Environment that has no allocation yet, and its retained sandboxes can still resume while the old address reaches Core. See Change the public URL.
A node manages generations, so its provider_ready follows its serving generation and its fixed code for the target generation appears in rollout.diagnostic. The node classifies the first failed readiness check and sends only its code; a frame with any other value is invalid, and Core never stores or returns probe text or host paths. The codes are Provider-neutral classes: provider_unavailable, host_unsupported, artifacts_unavailable, runtime_image_unavailable and capacity_insufficient. Readiness codes gives causes and operator actions. Core and nodes must come from the same distribution.
PATCH /core/v1/sandbox/nodes/{node_id} takes {name, max_active, max_retained}; lowering a limit stops no running sandbox. DELETE /core/v1/sandbox/nodes/{node_id} refuses with 409 runtime_node_in_use while the node holds allocations, snapshots, reservations or pending cleanup, including while it is offline. Removal deletes no compute and retires the node's identity; the host can come back only as a new node. There is no node drain.
GET /core/v1/sandbox/nodes/{node_id}/allocations lists the node's unreleased allocations. Each item's session_id is the live Session attached to the allocation's Environment, or null when none is attached. Each item's compute_phase_changed_at is when the allocation entered its current compute_phase, or null when unknown. This is a phase transition time, not a snapshot expiry. The snapshot deadline is not exposed by this response and must not be inferred from suspension.retention_seconds.
Core derives the recommendation from the node's host and the size of one sandbox, in one rule. cpus is effective_cpu_cores when the node reports it, otherwise cpu_count.
max_activeis the smaller offloor(2 × cpus / specification cpus)andfloor((total_memory − 2 GiB) / specification memory), at least 1 and at most 1000000. CPU is committed at twice the cores because agent sandboxes mostly wait on the model. Memory is never overcommitted, and 2 GiB stays for the host and the node service.max_retainedismax_activefor a provider without checkpoint support. Otherwise it is the larger ofmax_activeandfloor(available_disk / 2 / root_disk), at most 1000000: suspended sandboxes keep their root disk checkpoints on the node, and half the free disk is for them.
The recommendation is absent unless the host reports a CPU count or effective cores and its total memory. Core applies it only when an enrollment token leaves a limit unset; Web shows it on Edit node and never applies it on its own.
Some fields keep one name across providers but differ in meaning, or do not apply. Deployment fields come from GET /core/v1/sandbox/deployment, node and allocation fields from the node routes, and Runtime fields from the Runtime telemetry API, where disk appears only in the observation list and history is described under Session Runtime history.
| Field | microsandbox | E2B | OpenSandbox |
|---|---|---|---|
| Independent workspace filesystem | Required: Kernel NFS adapter, configured separately from compute | Unsupported | Unsupported |
Deployment specification.network |
public and domains |
public only; domains must be empty and private networks are unreachable |
public only; domains must be empty |
Deployment specification.resources |
cpus, memory_mib, root_disk_mib and, as a quota request, environment_disk_mib |
cpus and memory_mib, equal to the ready template build's and taken from it when omitted; no disk fields |
cpus and memory_mib, applied as resource limits; no disk fields |
Deployment metadata.template_build |
Absent: metadata is empty |
The build as Core read it when the selection was saved | Absent: metadata is empty |
Deployment suspension |
{retention_seconds} |
{retention_seconds}; Core pauses an idle E2B sandbox with the SDK's native pause |
null: no checkpoint support; see Lifecycle without a Session |
Deployment resources.allocations, resources.pending |
Totals across all nodes | Core's unreleased E2B sandboxes, and hosted Environments waiting for one | Core's unreleased OpenSandbox sandboxes, and hosted Environments waiting for one |
Enrollment-token max_active, max_retained |
Both limits apply; an omitted limit is derived at enrollment | 409 sandbox_deployment_conflict, after the 400 capacity checks; E2B has no nodes |
409 sandbox_deployment_conflict, after the 400 capacity checks; OpenSandbox has no nodes |
| Node list and detail | Enrolled nodes | Empty list; detail returns 404 | Empty list; detail returns 404 |
Node retained, snapshots, max_retained |
Suspended sandboxes are retained minus active; snapshots also includes retained snapshots during restore or cleanup |
Not applicable | Not applicable |
Node host.available_disk_bytes |
Free space on the filesystem of the node state directory; sandbox disks have their own quotas | Not applicable | Not applicable |
Allocation compute_phase, compute_phase_changed_at |
Includes suspended; the timestamp describes the current phase, not snapshot expiry |
Not applicable: no node allocations | Not applicable: no node allocations |
Runtime observation memory |
From the VM: memory limit and usage | From E2B metrics: memory usage and limit | From execd metrics: memory usage and limit |
Runtime observation disk |
null |
E2B diskUsed and diskTotal; null when the template does not report them |
null |
Runtime observation lifecycle_state: sleeping |
While suspended | Never | Never |
| Runtime history CPU | Derived from cumulative CPU time | Mean of the utilization ratios E2B reported in each bucket | Mean of the utilization ratios execd reported in each bucket |
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request_error or invalid_request |
A malformed request |
| 400 | invalid_sandbox_configuration |
A validated configuration diagnostic |
| 409 | workspace_storage_required |
The provider runs every Environment on workspace storage and none is selected |
| 409 | sandbox_generation_stale |
expected_generation is not current; current_generation gives the current one |
| 409 | sandbox_reset_required |
A different backend; the details name the current and requested providers |
| 409 | sandbox_not_configured |
A change to an unconfigured deployment |
| 409 | sandbox_reset_in_progress |
A management write or new enrollment during a reset |
| 409 | sandbox_deployment_conflict |
Another state the change cannot apply to |
| 409 | runtime_node_in_use |
Node removal while it holds resources |
| 503 | execution_unavailable |
Provider preparation is unavailable |
Storage and credential failures stay errors: an empty or failed read never proves cleanup. The machine connection API lists the errors of the node routes.
sandbox/deployment_contract.go owns the resource bounds and canonical field order, and each registered Provider's sandbox.DeploymentPolicy declares its requirements; sandbox/deployment.go applies them in Core. The installer consumes the generated declaration in deploy/node/node_spec.py, and the TypeScript client and Web the generated bounds, Provider declarations and each Provider's setup declaration (sandbox.SetupDeclaration) in packages/agents-client/src/deployment-contract.ts, so there is no second set of limits or providers. Regenerate both from the repository root with go run ./services/core/cmd/specification-contract -write; the sandbox Go tests, part of make check, reject a stale projection.
The specification digest is the SHA-256 of compact UTF-8 JSON with provider first, then resources, then workspace when external storage is selected. Workspace fields follow the filesystem declaration order (attachment, user_xattr, capacity_quota); its Boolean fields remain present. Resource fields follow the contract's declaration order; zero optional disk fields are omitted and required fields stay present. The digest never depends on the incoming field order or whitespace. services/core/internal/sandbox/testdata/deployment-contract.json holds shared acceptance cases, exact canonical bytes and digests that both the Go and Python tests consume.