Skip to content

Follow-up to #709: let a client credential read GET /api/catalog without management privileges #809

Description

@nbsp1221

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

  • I searched existing issues and documentation.
  • This request describes a concrete OpenCodex workflow rather than merely naming a desired technology.
  • I removed secrets and personal data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, planscatalogModel catalog, slugs, visibility, routed entriesenhancementNew feature or requestproxyHTTP proxy, routing, reverse-proxy / management authstreamingSSE, WebSocket, terminal stream frames

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions