Machines call Core under /api/v1: sandbox nodes, Runtime daemons and the self-hosted installer. Each route accepts only the credential listed for it, never the Core key or a Project API key, and a console sign-in grants nothing here. The reverse proxy sends /api/v1 directly to Core; Web never serves these routes.
Routes
| Route | Caller | Credential | Contract |
|---|---|---|---|
GET sandbox-node/configuration | Node installer and node | Enrollment token, or node credential with X-OAC-Node-ID | Read the node configuration |
POST sandbox-node/enroll | Node installer | Enrollment token | Enroll a node |
GET sandbox-node/identity?node_id= | Node | Node credential | Recover a node's identity |
WebSocket GET sandbox-node/connect?node_id= | Node | Node credential | Node generation protocol |
GET agent-daemon/install/{version}/… | Self-hosted installer | None | Installation grant |
POST agent-daemon/installation, POST agent-daemon/installation/claim | Self-hosted installer | Installation grant | Installation grant |
POST agent-daemon/enroll | Self-hosted daemon | Executor credential | Enroll a self-hosted daemon |
GET agent-daemon/connection?environment_id= | Self-hosted installer | Executor credential | Private connection confirmation |
POST agent-daemon/bootstrap | Runtime daemon | Daemon credential | Daemon bootstrap |
GET agent-daemon/device-status?device_id= | Runtime daemon | Daemon credential | Device status |
WebSocket GET agent-daemon/ws?device_id=&version= | Runtime daemon | Daemon credential | Core–Runtime protocol |
Every credential travels in an Authorization: Bearer header, never in a URL.
The generated runtime.openapi.yaml describes only the sandbox-node configuration, enroll and identity routes and the two installation routes. The two WebSockets and the daemon bootstrap, device-status, enroll and connection routes are served outside the API router and have no generated schema; this document and the linked contracts are their only definition.
Credentials
| Credential | Issued by | Accepted on |
|---|---|---|
| Enrollment token | POST /core/v1/sandbox/enrollment-tokens (Web Add node), with the node's approved capacity. One use; it expires at the response's expires_at | sandbox-node/configuration without a node ID, sandbox-node/enroll |
| Node credential | The node itself: it generates a secret of 32 to 256 characters without whitespace and registers it at enrollment | sandbox-node/configuration with X-OAC-Node-ID, sandbox-node/identity, sandbox-node/connect |
| Installation grant | The x_agents_core.installation command of a self_hosted Session; short-lived | agent-daemon/installation and its claim |
| Executor credential | The installation claim, or the Core-key executor credential routes | agent-daemon/enroll and agent-daemon/connection; after enrollment it is also the daemon credential of the bound device |
| Daemon credential of a hosted sandbox | Core, for each managed allocation, delivered in the bootstrap file | agent-daemon/bootstrap, device-status and ws |
| Operator device profile | oac-core-device, run by an operator with database access | agent-daemon/bootstrap, device-status and ws |
Core keeps only a SHA-256 digest of each token and credential it stores; installation grants are signed and not stored. Credentials are not interchangeable: each works only on its own routes.
Operator device profile
An engine host for environment: none Sessions connects with a device profile that an operator provisions directly in the database:
umask 077
mkdir -p ~/.oac/daemon/default
OAC_DATABASE_URL=... oac-core-device --tenant <tenant-uuid> --name 'engine host' --url https://core.example > ~/.oac/daemon/default/auth.json
oac-daemon connect --profile default--tenant is the Project's execution tenant UUID and --url Core's origin without a path. The command prints the profile once: server_url (the origin plus /api/v1), runtime_id (the device ID), runner_credential and device_name. Use a new profile rather than overwriting another device's file, and copy it privately to the same path on a remote host. oac-core-device --tenant <tenant-uuid> --revoke <device-uuid> revokes the device: new connections are refused at once, and an open connection closes at its next heartbeat. The Worker binds each none Session to one connected device of its tenant that declares the required capabilities and keeps that binding across retries and restarts; self-hosted Sessions never use this path.
Node routes
Read the node configuration
GET /api/v1/sandbox-node/configuration returns the active deployment for node installation and recovery. It never consumes an enrollment token.
- A new node sends its enrollment token without
X-OAC-Node-ID. The token must be valid, unexpired, unconsumed and issued by this installation. An active reset refuses this read. - A registered node sends its node credential and its UUID in
X-OAC-Node-ID. Without a query it reads the current target.?generation=Nreads only a generation this node may still need: the current target, its serving pin, or one held by an unreleased allocation or placement on it; any other generation is refused. This read stays available during a reset, for owned recovery.
The response has installation_id, provider, core_url (the installation public URL), generation, specification, specification_digest, max_active and max_retained. It never contains an administrator, Project or E2B credential, and exists only for node-backed providers. The sandbox deployment contract defines the specification and its digest.
Enroll a node
POST /api/v1/sandbox-node/enroll registers a node and consumes the token. The body has exactly these fields:
| Field | Value |
|---|---|
node_id | A canonical UUID the node chose |
credential | The node's secret, 32 to 256 characters without whitespace |
name | Display name |
provider | The deployment's provider |
backend_fingerprint | The node's backend namespace digest |
deployment_generation, specification_digest | The configuration the node read |
core_url | The Core origin the node stores and connects to |
Core checks, in one transaction, that the token is valid, the deployment is initialized, node-backed and not resetting, the generation and digest match the current specification, core_url equals the installation public URL and the node ID is new. Only then does it register the node, with the capacity approved in the token, and consume the token. The 201 response is the node identity: node_id, installation_id, provider, deployment_generation, specification_digest, max_active and max_retained. The node cannot submit capacity; the enrolled generation and digest stay the node's immutable identity, and later generations use separate configurations.
Recover a node's identity
GET /api/v1/sandbox-node/identity?node_id= returns the same identity plus connected and provider_ready, as Core currently sees them.
Node route errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_request_error, param: "core_url" | Enrollment without core_url |
| 400 | invalid_request | A malformed body, node ID or generation, or a provider other than the deployment's |
| 401 | invalid_node_credential | A missing, invalid, expired, consumed or foreign token or node credential |
| 409 | sandbox_specification_mismatch | The node's generation or digest does not match |
| 409 | sandbox_node_address_mismatch | core_url is not the installation public URL; the token stays unused |
| 409 | idempotency_conflict | The node ID is already registered |
| 409 | sandbox_reset_in_progress | Enrollment or a new node's configuration read during a reset |
| 503 | runtime_node_unavailable | The deployment is not initialized, or storage is unavailable |
The credential is checked before any deployment state, so a rejected credential, including one issued for another installation, gets 401 even before initialization or under E2B. Until the deployment is initialized, the configuration read and enrollment answer an otherwise valid token with 503, and the identity read and node connection answer 401.
Daemon routes
Daemon bootstrap
POST /api/v1/agent-daemon/bootstrap with the daemon credential and {"device_id": "…"} returns device_id, workspace_id, ws_url (derived from OAC_PUBLIC_URL, never from request headers), heartbeat_seconds and protocol_version. The daemon then dials ws_url as the Core–Runtime protocol describes.
Device status
GET /api/v1/agent-daemon/device-status?device_id= with the daemon credential returns device_id, online and owner: the current connection owner's owner_pod_id, owner_url, generation, status and lease_expires_at, or null.
The bootstrap, device-status and WebSocket routes share one error body, {"error": code, "detail": text}: 400 missing_params, missing_device_id or bad_json; 401 missing_bearer, unknown_device or bad_credential; 403 wrong_runtime_type; 500 internal; and on the WebSocket 426 incompatible_version when version is not Core's exact Runtime protocol version.
Enroll a self-hosted daemon
POST /api/v1/agent-daemon/enroll with the executor credential and exactly {"environment_id": "…"} (no query) binds one dedicated device to the Environment's Session and returns device_id, session_id, environment_id and workspace_directory. It never returns another credential: the executor credential becomes the daemon credential of that device. A retry with the same credential returns the same binding. A successful response carries Cache-Control: no-store.
| HTTP | When |
|---|---|
| 400 | A malformed body or any query |
| 401 | An invalid, revoked or foreign credential, a deleted Session, or an Environment without current executor authority |
| 409 | The Environment is already bound to a different key or device |
| 503 | Storage is unavailable |
Enrollment creates no managed allocation and grants no Session API access. The daemon keeps the binding beside its credential and refuses another Environment's native history. The gateway and Worker recheck the credential's authority on every connection and dispatch, so rotation, revocation and Session deletion end further use. The self-hosted guide gives the operator steps, and the executor credential contract describes how the daemon handles a permanent rejection.