The console server (services/web, the oac-web process) serves the built console, signs the administrator in with the Core key and forwards the signed-in browser's /core/v1 requests to Core with that key. The browser never holds the Core key or any API key. Applications, nodes and self-hosted executors call Core directly; the console forwards none of their traffic.
Configuration owns its process settings and defaults.
Request boundary
flowchart LR
browser["Administrator browser"]
console["Console server"]
core["Core"]
database[("PostgreSQL")]
application["Application / official SDK"]
machine["Nodes and Runtime daemons"]
installer["Installer domain service"]
browser -->|"same origin: /console/*, /core/v1/*; session cookie"| console
console -->|"/core/v1/* with the Core key"| core
console -->|"domain setup, Unix socket"| installer
application -->|"/v1 with a Project API key"| core
machine -->|"/api/v1 with machine credentials"| core
core <--> database
The deployment's reverse proxy routes /v1 and /api/v1 to Core and every other path to the console; the installation options gives the routes. The console handles each path as follows:
| Path | Sign-in | Handling |
|---|---|---|
/healthz | No | GET or HEAD answers 200 ok |
/v1, /api/v1 and below | — | 404, whatever credential the request carries |
/node-install/* | No | The node installation payload (see Node installation payload) |
/console/auth, /console/auth/login, /console/auth/logout | No | Sign-in |
/, /index.html, /favicon.svg, /oac-mark.svg, /assets/* | No | Static console assets |
/console/config | Yes | Console configuration |
/console/installation/domain | Yes | Domain setup |
/core/v1/* | Yes | Forwarded to Core |
/core and other paths under /core/ | Yes | 404 |
| Any other path | Yes | Static assets; a path without a file extension falls back to index.html |
Every request except /healthz, /v1 and /api/v1 must pass these checks first:
- Host and origin. The
Hostheader must equal the host ofOAC_WEB_ORIGIN. AnOriginheader, when present, must equal that origin, andSec-Fetch-Sitemust besame-originornone. A write that carries neitherOriginnorSec-Fetch-Site: same-originneeds a same-originReferer. Otherwise the console answers 403./node-install/*checks only the host and the path. - Safe request. The path must start with
/and contain no%, backslash, NUL, dot segment or empty segment. Absolute-form request targets,CONNECT,TRACEand any request with anUpgradeheader get 400. A request can therefore never leave/core/v1on Core, and the console carries no WebSocket. - Sign-in. Paths that need sign-in answer 401 without a valid session cookie.
Under /core, these failures use the Core error envelope with the codes in console-generated failures; elsewhere they return {"error": "…"}, or plain text for an unsafe request. Every response carries Cache-Control: no-store, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer and Content-Security-Policy: frame-ancestors 'none'.
Forwarding to Core
The console forwards each signed-in /core/v1/* request by prefix to OAC_WEB_UPSTREAM, with its path and query unchanged. Core alone decides whether the route exists, and its responses and errors pass through unchanged. The console therefore needs no change when Core adds a /core/v1 route.
On the way to Core, the console:
- removes the browser's
Authorization,Proxy-Authorization,Cookie,OriginandRefererheaders; - sends
Authorization: Bearer <Core key>; - sets
X-Core-Console-Actor: console, replacing any value the browser sent. Core records it as a display-only audit label (administrator API); - ignores ambient HTTP proxy settings, so the Core key reaches only the configured Core;
- streams responses without buffering.
On the way back, it removes Set-Cookie, WWW-Authenticate, Location, Refresh and every Access-Control-* header. A redirect from Core, or a failed connection to Core, becomes 502 core_unreachable.
The console never retries a request. Browser code calls /core/v1 through the typed clients in packages/agents-client; console API usage lists what each page reads and writes.
Sign-in
| Method and route | Request | Result |
|---|---|---|
GET /console/auth | No body | 200 {"mode":"login"} or 200 {"mode":"authenticated"} |
POST /console/auth/login | Content-Type: application/json; body {"core_key":"…"} with no other member, at most 4 KiB | 200 {"mode":"authenticated"} and the session cookie |
POST /console/auth/logout | No body | 200 {"mode":"login"}; ends the session and clears the cookie |
The administrator signs in with the deployment's Core key. There are no console accounts, usernames or setup step, and signing in grants the whole console.
- The console compares SHA-256 digests of the submitted and configured keys in constant time. It never logs or returns the key.
- The session cookie
core_console_sessionis HttpOnly,SameSite=Strict, andSecurewhenOAC_WEB_ORIGINis HTTPS. It lasts 12 hours. - Sessions live only in the console's memory, at most 64 at a time; the oldest is dropped first. A console restart or a Core key rotation signs everyone out.
- At most two sign-in checks run at once; another attempt gets 429 with
Retry-After: 1. - Failed attempts share a budget of 10 per minute; beyond it, a wrong key gets 429 with
Retry-After: 60. The correct key always signs in, which is why the console refuses to start with a Core key shorter than 32 characters.
Sign-in errors: 400 for a malformed body, 401 Invalid Core key, 405 for a method other than POST, 415 for a body that is not JSON, 429 as above, and 503 when the console cannot create a session.
Console configuration
GET /console/config returns what the signed-in browser needs to add nodes:
| Field | Meaning |
|---|---|
node_installer | Whether the console serves a node installation payload |
node_installer_sha256 | SHA-256 of that payload's node-install.pyz; Add node commands verify it before running the installer |
node_artifacts | The providers (docker, microsandbox) whose node artifacts the payload holds, locally or as a pinned release download. Read on every request, so artifacts added by rerunning the installer appear without a restart |
Node installation payload
With OAC_WEB_NODE_PAYLOAD_DIR set, the console serves the matched distribution's node payload at /node-install/ without sign-in: node-install.pyz, manifest.json, SHA256SUMS, runtime/seccomp.json, and the node artifacts the manifest declares under artifacts/. An artifact missing locally redirects (307) to its pinned release download. Node install and uninstall commands download from <public_url>/node-install/, so the reverse proxy must send that path to the console. Nodes verify every checksum themselves.
Domain setup
GET and POST /console/installation/domain let System → Domain and HTTPS configure a managed installation's domain. They are console routes, not Core routes. After the same origin and sign-in checks, the console passes the request body (at most 2 KiB) to the installer's Unix socket at OAC_WEB_INSTALLATION_SOCKET, authenticated with the Core key, and returns the installer's JSON answer and status. The request times out after 20 seconds.
| Method | Request | Result |
|---|---|---|
GET | No body | The domain status |
POST | {"hostname":"core.example.com"}, optionally with "confirm_public_url_change":"https://core.example.com" | 202 and the status; the installer checks and applies the domain in the background |
The status has supported, state (unconfigured, checking, applying, ready or failed), and nullable public_url, target_url and message. Installer errors use {"error":{"code":"…","message":"…"}}. Changing an address that nodes or executors already use returns 409 public_url_confirmation_required until the request confirms the new URL; pending config.json edits, an installation that is not applied or not running, hand-edited generated files, and another installation operation holding the lock (installation_busy) also return 409.
Without OAC_WEB_INSTALLATION_SOCKET (external reverse proxy installations), GET reports supported: false and POST returns 400 domain_setup_unavailable. An unreachable installer or an invalid answer returns 502 installation_unreachable.
The System page submits a hostname once, polls the status every 2 seconds while it is checking or applying, and asks for confirmation when the installer requires it. During setup, network failures and HTTP 502/503/504 responses keep polling active. The page allows 30 seconds without a successful status response before showing the disconnected message, and recovers when a poll succeeds. It never retries a write. Applying the domain restarts the console, which ends every session; the page keeps a sign-in link to the new HTTPS address. Only the ready state confirms HTTPS; the browser does not probe the new origin. The installer owns certificates, locking and recovery (managed HTTPS).
OAC_WEB_BOOTSTRAP=1, which the installer sets while no public URL is configured, lets the console also accept plain HTTP requests addressed to a literal IP address, treating http://<that address> as the origin, so an operator can sign in through the server's IP address. Host names still require OAC_WEB_ORIGIN, so DNS rebinding cannot reach the console.
Verification
After installing or changing the console, check:
GET /healthzon the console and on Core. Each proves only that the process answers.- Sign in, then read
GET /core/v1/projectsin the browser. This proves the browser-to-console and console-to-Core path and the console's Core key. - A Project API key works on
/v1and fails on/core/v1. The Core key fails on/v1, and/v1sent to the console answers 404. - A cross-origin write to the console is rejected, and a forged
X-Core-Console-Actorheader does not change the audit label. - Neither sign-in nor the sandbox deployment read (
GET /core/v1/sandbox/deployment) proves that a model or a sandbox is ready. Runtime observations and history report execution separately.
A sign-in failure belongs to the console. A 401 from Core on a signed-in request means the console's Core key does not match Core's digest, or the console reaches the wrong Core. The troubleshooting table covers the common symptoms.