Area
Authentication and account pool
What are you trying to accomplish?
Let a remote Codex client pull the generated catalog with a credential scoped to what it actually does — inference — instead of an administrative one.
Follow-up to #709, as invited there: "If either matters for your use case, a follow-up issue is welcome." GET /api/catalog now exists on dev and solves the distribution half. The remaining piece is admission: the route sits behind the management plane, so the only way a client machine can call it is to hold the management credential.
What prevents this today?
src/server/index.ts gates the whole /api/* prefix before dispatch, and /api/catalog lives inside that surface:
if (url.pathname.startsWith("/api/")) {
const apiAuthError = requireManagementAuth(req, managementAuth, config);
if (apiAuthError) return withManagementCors(apiAuthError, req, config);
const mgmtResponse = await handleManagementAPI(req, url, config);
And the credential classes are mutually exclusive by design (structure/05_gui-and-management-api.md):
| Credential class |
Allowed surface |
| Data plane |
/v1/* HTTP endpoints and new data-plane WebSocket handshakes only |
| Management plane |
/api/* only |
| GUI session |
/api/* only, bound to the issuing origin |
So a client that already holds a data-plane credential for /v1/responses cannot read the catalog, and giving it the management credential hands it provider configuration, POST /api/oauth/login, POST /api/codex-auth/login, and POST /api/stop on the machine that stores every provider credential.
This is not a flaw in how #709 shipped — keeping the new route on the management plane was the conservative choice, and the security review on that issue argued for exactly that:
Collapsing those classes would expand management-adjacent disclosure (model metadata / base_instructions) to every inference credential holder. … if remote clients need catalog pull with a weaker credential, add an explicit read-only catalog grant and document it — do not reuse admin privileges
That recommendation is the part that has not been built yet, and without it the documented multi-machine workflow in #95 still cannot distribute the catalog using client credentials.
Measured on 2.7.43, which is what I run, with a correct data-plane token and Host as a reverse proxy sends it:
GET /v1/models data token -> 200
GET /api/config data token -> 401
GET /api/config admin token -> 200
The same split will apply to /api/catalog once it reaches a release, since it is dispatched from the same gate.
What should OpenCodex do?
Allow a least-privilege credential to read the catalog, while leaving every mutation on the management plane exactly as it is now.
Concretely, the observable behaviour I am asking for is that a credential which is already trusted for inference — a config.apiKeys entry or the data-plane token — can perform the catalog read and nothing else. A separate, explicitly named read-only catalog grant would serve equally well; I have no preference between the two as long as it is distinct from the management credential and documented in the credential table.
Mutations, provider management, account management, and POST /api/stop should stay management-only. Nothing about the existing management path needs to change, so the current dashboard flow is unaffected.
I noticed requireApiAuth(req, config, "data-plane") already takes a credential class, so the admission machinery to express this appears to exist; the route location is what forecloses it.
Example usage or interface
What a client machine does today to become usable, using the least-privilege credential it already needs for inference:
# Already works with a data-plane credential
curl -fsS -H "x-opencodex-api-key: $DATA_PLANE_KEY" https://proxy.example.com/v1/responses ...
# What this request asks for: the same credential can fetch the picker catalog
curl -fsS -H "x-opencodex-api-key: $DATA_PLANE_KEY" \
https://proxy.example.com/api/catalog > "${CODEX_HOME:-$HOME/.codex}/opencodex-catalog.json"
Before and after, for an operator running one proxy for several machines:
today every client that should see routed models in its picker must hold the
management credential, which also lets it reconfigure providers and stop the proxy
after clients hold only an inference credential; the management credential stays on the
operator's machine
Alternatives or workarounds
What I run today: a static file server sidecar sharing the catalog directory read-only, exposed under a path prefix on the same reverse-proxy host, plus a small client fetch script. It works and it is a reasonable thing for an operator to build, but it is a second service, and its only reason to exist is that the catalog cannot be read with a client credential.
Distributing the file with scp/rsync also works and requires SSH access to the proxy host from every client, which is a much larger grant than an inference credential.
Handing every client the management credential is the workaround this request exists to avoid.
Additional context
Two smaller observations from the same thread, mentioned only so they are on record rather than as part of this request:
- The
models_cache.json route was explicitly not implemented. That turns out to be fine for a client that has the catalog: the stale-cache wrapper opencodex writes is reproducible client-side, so one authenticated read is enough.
x-opencodex-codex-version comes from loadPersistedCodexRuntime()?.selectedVersion. In a deployment with no Codex CLI installed alongside the proxy there is no persisted runtime, so the header is absent and clients have no version signal from this route. Not a problem for me — I publish the build version separately — but worth knowing if the header is meant to be the skew signal.
Happy to open a PR against dev if you settle on which shape you want. src/server/index.ts and the admission helpers are in the CODEOWNERS authentication group, so I would rather agree on the credential class here first.
Checks
Area
Authentication and account pool
What are you trying to accomplish?
Let a remote Codex client pull the generated catalog with a credential scoped to what it actually does — inference — instead of an administrative one.
Follow-up to #709, as invited there: "If either matters for your use case, a follow-up issue is welcome."
GET /api/catalognow exists ondevand solves the distribution half. The remaining piece is admission: the route sits behind the management plane, so the only way a client machine can call it is to hold the management credential.What prevents this today?
src/server/index.tsgates the whole/api/*prefix before dispatch, and/api/cataloglives inside that surface:And the credential classes are mutually exclusive by design (
structure/05_gui-and-management-api.md):/v1/*HTTP endpoints and new data-plane WebSocket handshakes only/api/*only/api/*only, bound to the issuing originSo a client that already holds a data-plane credential for
/v1/responsescannot read the catalog, and giving it the management credential hands it provider configuration,POST /api/oauth/login,POST /api/codex-auth/login, andPOST /api/stopon the machine that stores every provider credential.This is not a flaw in how #709 shipped — keeping the new route on the management plane was the conservative choice, and the security review on that issue argued for exactly that:
That recommendation is the part that has not been built yet, and without it the documented multi-machine workflow in #95 still cannot distribute the catalog using client credentials.
Measured on 2.7.43, which is what I run, with a correct data-plane token and
Hostas a reverse proxy sends it:The same split will apply to
/api/catalogonce it reaches a release, since it is dispatched from the same gate.What should OpenCodex do?
Allow a least-privilege credential to read the catalog, while leaving every mutation on the management plane exactly as it is now.
Concretely, the observable behaviour I am asking for is that a credential which is already trusted for inference — a
config.apiKeysentry or the data-plane token — can perform the catalog read and nothing else. A separate, explicitly named read-only catalog grant would serve equally well; I have no preference between the two as long as it is distinct from the management credential and documented in the credential table.Mutations, provider management, account management, and
POST /api/stopshould stay management-only. Nothing about the existing management path needs to change, so the current dashboard flow is unaffected.I noticed
requireApiAuth(req, config, "data-plane")already takes a credential class, so the admission machinery to express this appears to exist; the route location is what forecloses it.Example usage or interface
What a client machine does today to become usable, using the least-privilege credential it already needs for inference:
Before and after, for an operator running one proxy for several machines:
Alternatives or workarounds
What I run today: a static file server sidecar sharing the catalog directory read-only, exposed under a path prefix on the same reverse-proxy host, plus a small client fetch script. It works and it is a reasonable thing for an operator to build, but it is a second service, and its only reason to exist is that the catalog cannot be read with a client credential.
Distributing the file with
scp/rsyncalso works and requires SSH access to the proxy host from every client, which is a much larger grant than an inference credential.Handing every client the management credential is the workaround this request exists to avoid.
Additional context
GET /api/catalog; this is the follow-up it invited.Two smaller observations from the same thread, mentioned only so they are on record rather than as part of this request:
models_cache.jsonroute was explicitly not implemented. That turns out to be fine for a client that has the catalog: the stale-cache wrapper opencodex writes is reproducible client-side, so one authenticated read is enough.x-opencodex-codex-versioncomes fromloadPersistedCodexRuntime()?.selectedVersion. In a deployment with no Codex CLI installed alongside the proxy there is no persisted runtime, so the header is absent and clients have no version signal from this route. Not a problem for me — I publish the build version separately — but worth knowing if the header is meant to be the skew signal.Happy to open a PR against
devif you settle on which shape you want.src/server/index.tsand the admission helpers are in the CODEOWNERS authentication group, so I would rather agree on the credential class here first.Checks