DOCS

Architecture

Account routing, provider execution, live configuration, and desktop isolation.

About 21 min read · 1 sections

On this page

The HTTP daemon accepts authenticated OpenAI Responses and Anthropic Messages requests. Configuration maps each public model alias to one provider, account, and upstream model, independently of the caller's wire protocol. Messages ingress compiles into the existing internal request contract; it does not force the upstream to use the Responses API. Provider adapters retain their Responses, Anthropic, Chat Completions, or subscription transport. Protocol writers produce the caller's native JSON or streamed output. Each client executes its file and command tools; providers receive their results through replay. See Messages compatibility for Claude Code setup and protocol limits.

For fixed aliases, the gateway does not assign agent jobs, select worker models, inject task instructions, or execute caller tools. Explicitly configured managed worker aliases opt into code-enforced provider selection, context budgets and durable provider affinity before inference. They do not inspect task prose to invent capability requirements. The client owns orchestration, permissions, chat naming, and conversation lifecycle. The desktop adapter forwards those client operations and isolates account configuration; its session ownership machinery prevents competing writers rather than deciding what work an agent performs. Added gateway model rows ignore task prompt text from older hosts and offline caches. The original native model catalog and explicit client instructions remain unchanged.

Provider admission

Each provider has an independent FIFO admission queue with a fixed waiting capacity of 32. Its running limit is provider.maxConcurrent, or the gateway-wide maxConcurrent when the provider does not set one, or 4 by default. A busy Codex provider therefore cannot delay a Claude or API-provider request. The reviewer route uses the queue of the provider that serves its route. The pre-body flood guard allows the larger of the gateway-wide limit and the sum of the configured provider limits, plus 32 requests, so a lower provider override does not reduce the previous capacity. /health reports the running and queued totals across all provider queues.

Both admission settings accept integers from 1 through 64. A local inference provider can opt into upstream capacity discovery once instead of maintaining a numeric worker count on every Gateway. Its explicit numeric override remains a ceiling; missing or stale discovery uses the static fallback. Other providers keep their existing limits. This is per-Gateway admission, not a shared reservation or measurement of farm load. Availability reports the effective local limit; an otherwise available provider becomes busy when it is reached or work is queued.

On a live configuration reload, existing queues keep their in-flight work, changed limits take effect immediately, and new providers receive new queues. A removed provider's queue remains until it is idle and no live request snapshot refers to it, so requests accepted before the reload can finish and a later re-add reuses that queue. Compatible hot-swapped handlers share the listener's admission queues; retired generations keep their accepted requests and release their snapshots once they finish. Discovered admission waits for an older incompatible generation to drain before increasing capacity.

Live configuration

Bare subagent model names can use an explicit account default. Set "defaultForBareModels": true on one codex-subscription provider and/or one claude-subscription provider. The default serves only models it offers; failure or a missing model never selects another account. Explicit configured aliases always retain precedence. Without a default, a bare name requires exactly one matching subscription account; duplicate aliases within that account resolve deterministically. The unnamed automatic reviewer uses the designated Codex account too, and its catalog advertisement and request route use the same rule. This setting does not infer the parent chat's account or select an agent model. Changes apply to later requests, while in-flight requests keep their original configuration and account.

An inference keeps the configuration snapshot with which it began. New requests load atomically replaced configuration. A change that cannot be applied live is refused, not served as an error: the last good configuration keeps serving and the refusal is recorded once per file version as config_rejected with a fixed reason, never a path, value or error text (see diagnostics). That covers an invalid or unreadable file (invalid, unreadable), a changed listener port, mode or state directory (restart_required), and an existing alias that would change account, provider, credential source, base URL, the tailnet setting or upstream model (rebind). The change takes effect only after the daemon restarts; to apply it live, add another alias instead. The next file that is valid and allowed is adopted at once and clears the refusal, and so does putting the applied file back.

A refused file that does validate still takes away what it removes. Every alias it no longer has stops being served, and a provider left with no alias stops with them, so a provider removed on purpose does not keep serving because another edit in the same file was refused. Those aliases stay stopped through later unreadable or invalid edits, until a file puts them back or the daemon restarts. Everything the refused file kept stays on its last good binding.

The refusal is visible while it lasts. /health still answers 200, so the service does not restart-loop, and adds configRejected with the same fixed reason. gateway health reads that answer and warns "gateway.json change not applied (reason); fix it or restart the service" for as long as the file stays refused, and gateway discover --load checks it after writing and says when the daemon did not take the routes. The diagnostics problems list includes config_rejected. A release hot swap builds a fresh live configuration from the file in place, as a start does, so a file the release before it had refused and that loads is adopted by the new release, rebound aliases included. A gateway whose configuration is invalid at startup still fails to start. Retiring an alias does not release its identity until the daemon restarts. Labels and capability metadata can update live.

A macOS user LaunchAgent keeps the daemon alive across app and terminal exits. Its environment contains PATH, not provider credentials. Credentials and gateway client tokens are private files. Logs exclude request bodies, tool arguments, provider tokens, and environment values.

Existing installations can consolidate their gateway jobs into one supervised LaunchAgent. See service ownership and migration. Migration is explicit and requires a maintenance window; release installation preserves running chats.

Native executor sharing may instead be staged for a subsequent primary service start. The staging receipt is separate from the active plan, so clients keep their existing ownership mode until activation. Activation verifies the same plan and signed SDK again, and defers while old adapters or unknown writers remain. Private native starts matching the existing native relay service plan reserve the home before spawning, which prevents a concurrent shared host from creating another owner. See travel recovery.

gateway service restart asks the daemon to stop only while idle. The daemon checks all HTTP requests, including unfinished body uploads, before closing its listener. Active work refuses the restart and keeps its process. Older daemons without this capability refuse the command; upgrade those during an idle window. Explicit stop, gateway service restart --force or uninstall still stops the service and cancels active work; see services.

Desktop connections

The stdio adapter keeps one original native app-server connection. Account requests and native chats use that connection. Gateway chats use separate executors with private configuration homes. The adapter reads effective native settings for a private snapshot, excludes native provider authentication settings, and never copies the primary auth file. Automatic project-trust writes therefore land in the gateway executor's private configuration.

Before creating a gateway executor, the adapter rereads client settings. Native agent limits and user-authored roles pass through unchanged. The gateway does not generate agent profiles or replace the user's default worker or reasoning effort. Existing work keeps its loaded settings until its session reloads.

Executors share native chat storage and SQLite metadata so saved gateway chats remain discoverable through the desktop's normal thread list. User instructions, skills, and plugins keep one source through links. The global AGENTS.md link also follows a file created after an executor starts. Native configuration is never linked. All model preferences are saved in gateway-owned state.

The adapter fetches authenticated public model metadata from the inference host. A catalog change creates a fresh gateway executor for new chats. Loaded chats retain their original executor, including active turns and terminal state. Loaded gateway chats can retain as many catalog generations as they need; a fifth catalog does not require closing an older chat. Released and disconnected generations are checked in the background and periodically. Only an unleased generation proven to have no loaded chats is stopped. An older engine whose probe fails stays owned and does not block acquisition of a new catalog. A gateway executor failure does not stop the native connection; saved gateway history can be resumed.

Gateway-owned compaction metadata contains only current-window counters and SDK compaction lifecycle events, scoped to the native storage home. Recovery uses the loaded executor's frozen model limits and the next explicit user request. It preserves active steering, goals, native account ownership, and original rollouts. See compaction and model changes for completion, cancellation, and shared-ownership limits.

Each adapter keeps a separate private cache of public gateway model rows. Startup validates its source against the local port and client-token hash, then restores those names even if the inference host is offline. Cache files contain no native cache identity, provider authentication, or token values. Discovery can retain cached names; gateway work requires a successful fresh fetch and returns application error -32001 during an outage. Successful discovery atomically updates the cache. Recovery needs no app restart.

The unified service can own shared gateway executors. This is an explicit maintenance migration described in services. Each catalog and execution-settings generation gets one signed Codex app-server with a private Unix socket. Desktop and connected-machine adapters attach as independent clients to that process. Closing an adapter closes its connection and leaves the executor running. Reopening a loaded chat finds its owning generation before starting another executor.

Shared generation homes use the native home's writer-lock directory as well as its rollout storage. An unrelated writer therefore remains protected even across catalog generations. The execution host retains every generation still in use and only retires an engine with no client leases and no SDK-loaded chats. A missing or changed managed owner fails closed. It never deletes live locks or takes over an old executor. Existing per-connection executors must finish before migration.

The execution host belongs to the unified service and owns its SDK children. There is no separate per-generation LaunchAgent. The native account connection and provider credentials keep their current ownership.

Before a shared host creates or looks up a gateway generation, it verifies the selected SDK executable. If that executable's verified receipt names no adjacent Code Mode helper, the host changes every row in that generation's gateway catalog to tool_mode: "direct". This includes missing and unknown modes, while preserving the row's instructions, model, provider, permissions, review policy and every other catalog field. A changed catalog receives a deterministic replacement revision derived from the adapter revision and normalized catalog. It therefore cannot reuse a persistent generation directory created for the incompatible catalog. A catalog already entirely in direct mode keeps its adapter revision.

An owner is dead when nothing it names can still be running and nothing it advertised answers a connect. Its processes are gone when its receipt belongs to an earlier boot, or when every pid it names is gone, belongs to another user, is the claiming process itself, or belongs to a process that started at another time. Its sockets are the links its receipt records and every link its host folder holds (host.sock, native.sock and each e-*.sock generation link), each asked both through the link and at the SDK socket it names. An answering socket keeps an owner alive even when its receipt is from an earlier boot, so the two rules always agree. Only a socket can answer: a plain file, a folder or a pipe at a socket path, or at the SDK target a link names, never keeps an owner alive. A receipt this release cannot validate, such as a newer release's or a damaged one, is judged by whatever pids, stamps and socket path it still contains.

A dead owner never blocks a start. Its claim removes the socket links that are safe to remove (an SDK link to its own target that nothing answers; a direct socket that nobody recorded stays), moves the old receipt aside under the folder's stale/ directory, and records one execution_owner_reclaimed event with the scope and the reason. Archiving copies a plain receipt and moves anything else, such as a link or a pipe, aside whole without reading it; stale/ keeps the five newest receipts of each name. Receipts written before 0.4.4 carry no boot stamp and are judged by their file time. A receipt naming the claiming process itself is its own leftover from an attempt that never finished, so the claim goes on to take the home. A gateway generation whose record or catalog file is garbled is moved aside to its own stale/ folder and rewritten (an execution_owner_reclaimed event with the scope generation); a record that names another identity still refuses the generation. A host that died between creating its control socket at its SDK target and linking it leaves a socket nothing recorded at that configuration's own target; the next start removes it when it is a socket of this user, whatever its mode, that refuses a connect (ECONNREFUSED) twice, 200 ms apart (a live listener with a full accept queue also refuses on macOS), and any other file, or any socket that answers, still refuses the start as occupied and is never removed. The host creates its socket under a private umask (0077, restored right after the bind) so that a host killed before its chmod leaves a socket closed to others; the chmod stays as a second guard. Releases before 0.4.7 bound with the process umask and could leave a 0755 socket, which the next start now reclaims too. Rollback to legacy writers treats a receipt still marked running as stopped once its owner is proven dead by the same rule, so a crash cannot block it for good. It then clears that owner's links the way a claim does, including a link the owner recorded without its socket identity and every link it never recorded that nothing answers, and when the host and the native-home owner are both proven dead it no longer requires their native revisions to agree. A sharing-mode change that releases only the native-home receipt uses the same native rule: a running receipt passes once its owner is proven gone, with pids judged by their boot and start rather than by the pid alone.

An adapter whose required host is missing, not running, dead, or no longer matches its socket asks the service for an immediate retry by writing the private file kick in the host folder, and waits up to 20 seconds, looking every 250 ms, for a running, live host. The service watches that file once a second. A kick only brings a failed host's next start forward, never within five seconds of the previous start and never while a start or restart is running; a failure that retrying cannot fix (another live owner, a changed identity, an invalid engine or receipt) gets one kicked start per backoff step. A kick to a running host never stops it: the service probes it and restarts it only when the probe fails and the host is idle (see diagnostics). A host that stays down fails with the same error as before the wait; a live host that does not match fails at once.

Process start times come from the operating system (ps, read in UTC with the C locale) both for the stamp a process records about itself and for every check, so a long-running supervisor never drifts away from its own receipt, and a daylight-saving change cannot move a start by an hour. When ps does not answer, a process records no start at all rather than an estimate: a checker then skips the start comparison and keeps the owner while its pid answers, and the boot stamp still tells a reboot apart. Only an answer is remembered, so the next stamp asks ps again. Private file locks follow the same process rule but never the file-time boot rule, because a clock that steps forward would otherwise free a live lock. A lock has one name however its folder is spelled (/tmp and /private/tmp, or a linked folder), so two spellings never hold it together. Only the caller that created a lock removes it, and only while the path still names its file; one breaker at a time removes a stale lock.

A native launch that a dead host recorded as starting may have left an SDK child that is still coming up. Its receipt is reclaimed only when it is from an earlier boot, or when it is older than 60 seconds, and in both cases only when nothing answers a connect at the native socket or its SDK target. Until then the claim is refused as unconfirmed, so two writers never share a chat. A native-home receipt that names no process is given up only when no host folder's native socket (nor any socket it records) answers, and its file is from an earlier boot or older than ten minutes; the native SDK always listens while it runs, so ten quiet minutes prove its writer gone. A private, unshared native writer obeys the same verdict as a claim: a live host (even with its SDK idle), a launch inside its startup grace, or a receipt nothing can prove dead keeps it from starting.

A desktop adapter whose plan shares native chats falls back to its own native engine when its host stays down through the wait, so native chats keep working. Discovery must have failed because there is no host receipt, or because a valid receipt names no running, live host; a host that does not match, a changed host socket, an unreadable receipt and every other failure still fail closed. An adapter whose plan also shares gateway chats falls back the same way when the running host offers no native sharing (an app engine it could not verify). Either fallback needs the adapter's native home to be the plan's own: the same folder, spelled the same way. The host folder is keyed by that spelling, so an adapter that reaches the folder through a link or another path looked at a host folder the service never runs, and it is refused with a message saying so. The native-home verdict must also be clear: a live or unconfirmed owner, or anything answering at the host's native socket, keeps the adapter out. Before it spawns the engine, the adapter takes the per-native-home lease under the receipt's own lock. Its receipt names the host's configuration and native socket path, the adapter as the owner process and a launch marked starting, then the engine's pid once it runs. The marker it keeps in owner.json holds the engine identity and the whole receipt the first lease replaced. When the last lease's engine exits (the adapter stopping, or the engine leaving by itself), its adapter puts that receipt back with its file time, set before the atomic rename so no reader sees the restored receipt with the current time, or removes the file when there was none, so the host and rollback see the home as they did before the fallback. An adapter that fails to build after taking the lease spawned nothing and gives the lease straight back. An engine that still runs when the lease is released keeps the home: its lease's receipt then names only that engine (hostPid 0) until the dead owner rules release it.

Private leases share the native home with each other and never with the host's writer, the rule stock Codex follows when it runs one app-server for the app and another for an SSH remote on one home. Each lease keeps a receipt of its own beside owner.json, private-<adapter pid>-<lease id>.json, judged by the same liveness rules (boot, pid and start of the adapter and of its engine, the startup grace, the native socket); a dead one is moved to stale/ under one shared name, so the newest five are kept. Under the receipt's lock, a lease joins live ones of the same configuration and native home spelling; a live or unconfirmed host receipt, an unreadable or foreign private receipt, or anything answering at the host's native socket refuses it. The host's claim is refused while any private receipt may still be live, and archives the dead ones first. Releases 0.4.5 and earlier read only owner.json, so while any lease is live it holds a marker in the private receipt format of 0.4.4 and 0.4.5 (which 0.4.3 reads as a plain owner) with privateOwner.shared added. The marker keeps the receipt the first lease replaced and names live processes: the adapter and engine of a single lease, or the engines (the adapters before they run) of the two longest-running leases. It is rewritten whenever a lease comes, starts its engine or goes, and every lease looks at it again every 45 seconds, rewriting it only when something changed, and the last lease to go puts the kept receipt back. An older release therefore keeps its host's writer and its own private engine out while any named process runs. With three or more leases, an older release could miss the others for up to 45 seconds after both named leases crash; this release's host reads every receipt. The marker reads as absent to the private-writer check, so it keeps no other private engine out, while rollback still waits for it to stop; it reads as live while anything answers at the host's native socket. A private engine of release 0.4.5 or earlier keeps its own owner.json (no shared) and holds the home alone: a new adapter fails closed, saying such a release started that engine and naming its process, until that connection moves to this release. A lease that cannot write the marker removes its own receipt again and brings the marker back in line with the remaining leases. A release that fails (a busy lock, say) is retried by the lease's own timer until it succeeds, and once more when the adapter stops; the timer ends only after a release has succeeded, so a long-lived SSH listener never keeps the host out after its engine has gone. Rollback and shared-native --disable treat a live lease like any live native owner: they refuse (and the disable restores the previous plan) until every lease's engine has exited.

While any lease is live, a host that starts claims everything but the native home: it records native_sharing_unavailable with reason: native_owner, serves gateway chats, and refuses native requests (native_acquire with refused_owner) until one finds the home free and claims it, so the next adapter start returns to sharing. An adapter whose native request the host refuses that way joins the private engines instead of failing (native_sharing_fallback with reason: native_owner). Its lease may only join: with no live private receipt it is refused as retryable, before anything is written, and the adapter asks the host once more (the refusal may have come from a busy lock, or the engines ended meanwhile), so it never starts a private engine beside a host that could share. It skips the check that keeps a private engine away from a live host with native sharing, since its lease is taken under the receipt's lock and is refused beside the host's writer, a private engine of 0.4.5 or earlier, or anything answering at the host's native socket. A receipt from a dead owner that names another configuration or another spelling of the native home is not the host's to take either: the host records native_sharing_unavailable with reason: native_identity, serves gateway chats, and refuses each native request (Native execution ownership changed, naming the receipt ~/.local/state/gw-native/<scope>/owner.json to move aside) until the receipt is moved aside. A native-only host still fails its start in both cases, as before. A host that never claimed the home claims it once when it stops, if the home is free by then (a fallback ended and put back, say, a crashed host's receipt that still says running), so its stop writes a clean stopped receipt and rollback is not blocked by a stale one. A fallback adapter that crashes leaves a receipt naming its own process and its engine; once both are gone, the host (or the next fallback, which keeps the receipt the crashed one had replaced) reclaims it as a dead owner.

The host pins the binary its gateway generations actually run: the release's own gateway SDK (gateway-sdk/gateway-codex, 0.162.0-alpha.17.2), not the app's native engine. An app update therefore neither stops the host from starting nor refuses new generations while the host runs; the runtime's checksum, carried in every generation descriptor, guards the binary. A host that pinned its runtime refuses a descriptor without that checksum instead of spawning the app's unchecked codex. Only a layout without a gateway SDK (or a runtime that cannot be resolved) pins the app's codex at start, as releases before 0.4.3 did; then that codex must report the pinned version and stay unchanged while the host runs. Because a gateway-only host runs the pinned runtime, adapters and rollback do not compare the app's codex with its receipt: an update that removes, moves or relinks that codex neither stops the host from starting nor makes adapters reject it. A host that shares native execution still has to match the app's codex.

Native ownership can also be shared independently of gateway generations. Its one original signed SDK keeps the original native home and normal authentication; private gateway homes never receive that authentication. A per-native-home lease prevents two managed configurations from creating separate native owners. Adapter disconnection releases its client, not the SDK. Compatible process arguments, actual signed executable identity, private socket identity, and exact launch death records govern reconnect and recovery. See native migration for maintenance requirements and the SDK's Unix capability limits.

Desktop SSH uses a private Unix socket carrying WebSocket JSON-RPC. The socket adapter reuses the same session router as stdio and bounds frames to 16 MiB, pending requests to 256 per connection, and simultaneous clients to eight. Without shared execution, disconnect stops that connection's executors. With shared execution enabled, disconnect closes only the SDK client connection. Other clients keep their sessions in both modes. The transport uses the MIT-licensed ws package, bundled in the installer.

Provider version maintenance runs on the build host, outside inference and session ownership. Stable update validation prepares isolated candidates and advances generated source pins only after compatibility checks pass. Installed engines continue through the normal gateway release path.

The app-owned supervisor can also replace its versioned controller in process. The root retains its lock, signal handlers, children and execution host; the new controller receives the same ownership broker. Child exit observers dispatch to the current controller, preserving backoff across a handover. See live supervisor updates for bootstrap, strict host-idle replacement and readiness limits.