DOCS

Harness identity

How gateway chats identify themselves to each harness: Codex records them as gatefox by default (subscription_gateway before 1.8.0, still read) and as openai on an opted-in Mac, Claude Code stores no provider, and the checklist for adding another harness.

About 15 min read · 4 sections

On this page

A gateway chat is a chat in some harness (Codex, Claude Code) whose inference the gateway serves. Each harness stores something about who served a chat. That stored identity decides which clients list the chat and which engine resumes it. This page is the contract for each harness, and the checklist for a new one.

Codex: gatefox by default, openai as an opt-in

Two ids for the gateway (1.8.0)

Since 1.8.0, new gateway chats are saved with the provider id gatefox. Chats saved before 1.8.0 carry subscription_gateway, and that id keeps working: every engine the gateway starts registers both ids, gateway desktop register publishes a row for each in the native config.toml, the router lists chats under both, and the patched SDK (gateway-executor-v4) treats both as the gateway. A chat is never rewritten by a resume.

Id Written by Read by
gatefox New gateway chats since 1.8.0 1.8.0 and later
subscription_gateway Every release before 1.8.0; also a new chat on an engine an older release started Every release

The executor marker is GATEFOX_EXECUTOR since 1.8.0. Executors also get the older HARNESS_GATEWAY_EXECUTOR, and the SDK reads either, so an SDK of either generation sees the same state. gatefox chats migrate (see the rename plan) moves saved chats from subscription_gateway to gatefox. Both ids stay registered, so a chat a Mac has not migrated yet, or one another device still records with the old id, keeps opening.

Account names and saved chats

Gateway subscription account IDs, such as codex-primary and codex-secondary, are separate from the harness provider identity described below. Renaming an account must preserve its credential binding, every existing model alias, and chatIdentity. Never rewrite saved chats or the native account configuration.

Keep existing picker aliases and their visibility unchanged when renaming account IDs. Running executors freeze their alias catalog, while the picker refreshes its catalog: exposing replacement aliases can make them selectable but unusable by loaded chats. Canonical duplicates may be added as hidden routes for new clients; keep every old and new alias routable indefinitely. Check the private alias reservation ledger for collisions and migrate only exact matching route identities, including retired aliases. Move the automatic-model-update scope with the ID.

Live configuration deliberately refuses provider-ID rebinding. Activation requires a fresh inference process. In a managed service, the inference child's verified SIGUSR2 handler restarts only when idle; do not restart the supervisor or chat executors to change names. A busy child leaves activation pending.

scripts/e2e-codex-account-names.mjs checks loaded-chat continuation, cold resume, account separation, and model switching with the real signed SDK and mock inference. Its adapter scenario reproduces a refreshed picker advertising an alias absent from a loaded executor, then verifies switching through the stable picker aliases.

Releases 0.4.7 to 1.7.x record new gateway chats with the provider id subscription_gateway; 1.8.0 records them as gatefox. A Mac can opt in to the native identity, openai, with "chatIdentity": "openai" in its gateway configuration.

The phone is why the choice matters. The ChatGPT app's Codex list, and a stock Codex engine's thread/list without a provider filter, show only chats saved with the engine's own provider, openai. With the native relay in the path the phone does not depend on that: the relay sends its requests through the adapter, the adapter lists the gateway's chats beside the native ones (see chat lists), and continuing a gateway chat goes to the gateway. No identity switch is needed then, and continuing is safe.

When to opt in. Only on a Mac whose native relay is verified: gateway native status shows it connected, and a gateway chat was listed and continued from the phone through it. Opting in only adds that a stock engine, such as the phone's when the relay is down, also lists the new chats. That is also the risk: without the relay, the phone continues a chat on the app's own engine, which uploads the chat's history to OpenAI under your account. A chat on a gateway alias is then refused, since OpenAI has no such model; a sub-agent on a native model, such as gpt-6.1-sol, runs on your own ChatGPT account. Leave the default on any Mac whose relay is not verified.

Where subscription_gateway (default) openai (opt-in)
Rollout session_meta.model_provider and state_5.sqlite threads.model_provider subscription_gateway openai
Model field the gateway alias, such as codex/gpt-6.1-sol the same alias
Sub-agent of the chat the parent's id, model often a native slug openai, model often a native slug
How the adapter recognizes it the provider id the registry, then the ancestors, then the alias
Listed by a stock engine with no filter no yes
Listed through the adapter with no filter, or with only the native default yes (0.4.7 adds it to the list) yes

Existing chats are never rewritten, whatever the setting. A resume names the provider id the chat was saved with, because the SDK replaces a chat's SQLite provider when a resume names another one. A fork is a new chat and gets the identity of the connection that makes it. Turning the opt-in off again (remove the key, or set "legacy", and reconnect the desktop) keeps every openai chat working: every patched executor serves both ids. Releases before 0.4.7 ignore the key.

Codex 0.166 refuses to resume a chat, or to take input on it, when its saved provider differs from the executor's configured one ("This chat uses subscription_gateway, but your current provider is openai"). Inside a marked gateway executor the executor patch treats the legacy id and the loopback openai alias as one provider in both checks (same_gateway_executor_provider), so a legacy chat still resumes under the id it was saved with. Every other pair, and every unmarked engine, keeps the SDK's strict check.

How an executor serves openai

A gateway executor is the pinned gateway SDK (gateway-sdk/gateway-codex) started by the adapter or by the shared execution host. It is not the built-in OpenAI provider. All of this ships and runs in 0.4.7 whatever the setting, so an opted-in Mac and one that opted out again serve the same chats.

  1. The adapter starts every executor with GATEFOX_EXECUTOR=1 and the older HARNESS_GATEWAY_EXECUTOR=1 in its environment. Native engines never get it: the adapter and the shared native host remove an inherited marker (either spelling) before starting one. Each executor sets both to 0 for the commands, hooks and shells it starts (shell_environment_policy.set), so nothing a chat runs counts as an executor.
  2. The executor's launch settings register the gateway's own provider under both ids, model_providers.gatefox and model_providers.subscription_gateway (loopback base URL, the gateway token command, supports_websockets = false, request_max_retries = 2, stream_idle_timeout_ms = 90000), and set model_provider to the identity for new chats.
  3. The gateway SDK patch (scripts/patches/gateway-executor.patch, capability retained in capability gateway-executor-v3) makes openai a second name for that provider, but only when the marker is 1 and the provider's base URL is plain HTTP on a loopback address with no credentials in it (a name such as 127.0.0.1.nip.io or a mapped IPv6 address does not count). Inference, authentication, retries and timeouts are therefore exactly the legacy provider's. The SDK keeps no ChatGPT or API-key credential, so nothing it sends elsewhere carries the gateway token.
  4. Because the alias is not named OpenAI, the SDK compacts locally (it never sends compaction_trigger items, which the gateway refuses), and does not send WebSocket requests or internal metadata. The patch also keeps context-limit compaction on the selected model inside the user's turn for both ids.
  5. Every executor sets analytics.enabled=false and openai_base_url to the gateway. If an executor ever runs without the patch, openai still reaches only the loopback gateway, which refuses the request; it never reaches OpenAI.

The adapter uses openai for new chats only when the Mac opted in and the package's gateway SDK declares gateway-executor-v4. Otherwise new chats are gatefox; a stock Codex executor (a checkout without the artifact) serves it too, because both ids are registered rows. A new chat on an engine this connection did not launch (a shared host's, found by its chat) keeps subscription_gateway, since that engine may belong to an older release that registers only the old id, and an executor that records a new chat with any provider but the requested one is refused.

An already-open adapter keeps the identity it selected for new chats. If its original managed package is pruned after an install, it may recover only to the verified SDK in that installation's current release. A verified replacement can serve an existing legacy connection without changing it to openai; an existing openai connection remains fail-closed unless that replacement proves gateway-executor-v4. Saved chats always retain their saved provider id.

The provider row in native configuration

Every engine the gateway starts registers gatefox and subscription_gateway with -c settings. The ChatGPT app's own engine reads only the native config.toml. ChatGPT is a login item and can start its engine before the gateway service sets CODEX_CLI_PATH. Without a row in config.toml, every saved gateway chat then fails to open with "Model provider gatefox not found" (or subscription_gateway for an older chat), whatever model the chat used.

gateway desktop register therefore publishes [model_providers.gatefox] and [model_providers.subscription_gateway] in the native config.toml, after it sets CODEX_CLI_PATH. It runs at installation and at every service start, so the row follows a changed port or configuration path. The row is the same table as the adapter's -c settings (src/native-provider-row.ts): the loopback gateway and the gateway token helper. The gateway writes it through the app's own engine, as API mode writes its settings, so comments and every other key stay as they are.

A chat opened by a stock engine this way gets its replies from the gateway, but without the adapter's extras, such as gateway tool registration and the compaction patch. The next app launch through the wrapper restores the full gateway path.

Routing follows the chat

The adapter decides a chat's engine from the chat, never from the provider id. The classifier (src/gateway-chat-classifier.ts) answers with the first rule that matches:

  1. legacy_provider: the chat is saved as gatefox or subscription_gateway.
  2. registry: the chat is in <state>/gateway-chats/<scope>.json.
  3. parent: a sub-agent whose ancestor (up to 8 levels) is registered, saved as gatefox or subscription_gateway, or on a gateway alias.
  4. alias: the chat's model is a gateway alias and not a native slug.
  5. native: anything else.

Before it classifies a chat, the adapter reads each of its ancestors that is neither in the registry nor already seen on this connection (thread/read without turns, up to 8 levels, each once per connection). So a sub-agent on a native model is found even when its parent's registry entry was lost, or when it was spawned two levels deep while no desktop was connected.

The registry is written before a new chat's reply, before a sub-agent's announcement is passed on, and, since 0.4.7, when a chat that only its ancestors or its alias identified is routed to an executor. Throwaway chats (composer predictions) are never written. A failed write is reported once as gateway_registry_write_failed. Such a chat is still found: a gatefox or subscription_gateway chat by its id, an openai chat on a gateway alias by its alias, and an openai sub-agent on a native model only through its ancestors.

Chat lists

When a thread/list asks only for the native engine's default provider, by naming it alone or by naming none (and no parent or ancestor filter), the adapter asks the engine for that provider, gatefox and subscription_gateway. So a client that lists the native identity through the adapter, such as the phone through the relay, also sees the gateway's own chats, old and new. An empty list (every provider), several providers, another provider, or a parent filter is sent unchanged. The native default is read from the engine's configuration once per connection, and again after a configuration write.

Runtime state on mobile lists

The native engine remains the source of list membership, ordering, cursors, titles, saved metadata, and turns. After it returns a page, the adapter considers only saved (notLoaded) row ids, intersects them with a bounded gateway loaded-id read, and may replace only those rows' runtime fields: status, model, reasoning effort, environments, and direct-input capability. A native row already live always wins. One 1.9-second list budget covers discovery and metadata reads; a host failure, timeout, or unavailable gateway SDK leaves the native page unchanged.

For a shared generation that no normal adapter connection already observes, the adapter opens a passive SDK connection solely to receive real thread/status/changed notifications. It never resumes a chat, answers approvals, forwards content, or owns a gateway engine. SDK new-thread subscriptions are released on that passive connection. Discovery runs about every five seconds after a client has listed chats, so it can miss transitions that happened before an observer attached; notifications do not replay state or history. The SDK has no unread field: clients own their unread bookkeeping.

Native chats and per-connection gateway chats on separate desktop and relay engines use a private status broker in each adapter. It exposes only loaded chat IDs, runtime status, model, effort, and direct-input capability through a user-owned Unix socket. Registrations are scoped to the native home and checked against their process and socket identities. The list client receives genuine native status events and enriches saved rows without resuming chats or taking over an engine. An unavailable owner contributes no runtime state. Existing native adapters start publishing after their normal upgrade or reconnect; the broker cannot observe an older adapter that lacks it.

The native and gateway loaded IDs are combined before applying loaded-list pagination. Ordinary chat-list membership and pagination still come from the native engine. The checks in scripts/e2e-chat-state.mjs, including --native and --private, use two clients and the real SDK with a local mock provider. They establish running-state hydration and idle-event delivery, without paid inference. They do not establish the phone's unread rendering.

What the phone does

Phone continuation through the relay is implemented. On 23 Oct 2026 the isolated scripts/e2e-native-relay.mjs --expect-unfiltered-gateway-listing check passed with the real signed 0.163.0-alpha.11.1 app engine: it listed native and gateway chats, ran both, and continued the gateway chat with its history on a second stream. It also confirmed that a private engine refuses a second writer while the app holds a chat, then permits continuation after the app releases it. This tests the relay protocol with a mock phone and mock inference, not the phone's rendered UI.

Without the relay With the relay
List, default setting The app's own engine answers: no gateway chat is listed The adapter answers: every gateway chat is listed
List, opted in New openai gateway chats are listed, older ones are not Every gateway chat is listed
Open History is shown History is shown
Continue an openai chat on a gateway alias The app's engine uploads its history to OpenAI, which refuses the unknown model Routed to the gateway executor
Continue an openai sub-agent on a native model The app's engine uploads its history and runs it on your ChatGPT account Routed to the gateway executor
Continue a gatefox or subscription_gateway chat Not reachable from the list Routed to the gateway executor

Rollback

Only a Mac that opted in has openai gateway chats. Rolling it back:

Back to Top-level openai gateway chats openai sub-agents subscription_gateway chats
0.4.4, 0.4.5 or 0.4.6 Listed and continued: the registry routes them to the gateway. Those releases resume a gateway chat with subscription_gateway, so each one resumed there is saved as subscription_gateway in SQLite (the rollout keeps openai) and drops off a stock engine's list Their executor runs patch v1, so when a parent resumes an openai child it uses the built-in OpenAI provider (core/src/agent/control/spawn.rs keeps the child's saved provider), for which an executor has no credentials; such children do not continue on the gateway Unchanged
0.4.3 or older Listed, but they cannot be continued: those releases classify by provider id and send the alias to OpenAI Not continued on the gateway Unchanged

Claude Code: no provider id

Claude Code sessions store the model name, never a provider. There is nothing to rename, and no list filters by provider. A gateway session is one whose model is a gateway alias (see Messages). Remote Control needs api.anthropic.com and a claude.ai sign-in, so no naming change makes a gateway session reachable from the Claude app.

Copilot

Copilot models reach Codex as gateway aliases, so a Copilot chat in Codex follows the Codex contract above. The Copilot CLI's own sessions are the gateway's private state (live Copilot sessions) and are not listed by any harness.

Checklist for a new harness

  1. Find what the harness stores per chat that names who served it (provider id, base URL, account, model), and which clients filter their lists by it.
  2. Prefer the harness's own native identity for new chats, so its stock clients list them. Never rewrite chats that already exist; a resume must name what the chat was saved with.
  3. Keep routing independent of that identity: record each new chat, and its sub-agents, before the client sees it, and classify by chat, not by the stored id.
  4. Make sure the native identity cannot send a gateway chat to the vendor: mark the gateway's executors, check that the endpoint is loopback, and fail closed without them.
  5. Turn off the harness's analytics and any account-backed calls in executors, and check that no request outside the gateway carries the gateway token.
  6. Keep the harness's compaction and retry behavior the same as under the gateway's own provider, and test it with the real harness binary against a mock upstream.
  7. Put the switch behind a per-Mac setting, default it to the old identity until the client that reaches the chats (for Codex, the native relay) is verified, and write down what an older release does with the new chats before shipping.

The end-to-end check for Codex is scripts/e2e-identity.mjs.