Skip to content

Serve the generated Codex model catalog over the management API for remote clients #709

Closed

Description

@nbsp1221

Area

Proxy and routing

What are you trying to accomplish?

Run one opencodex instance centrally and let Codex clients on other machines see the routed model picker, without any client holding provider credentials. The server owns the providers and OAuth accounts; each client should only need base_url, the admission token, and the model catalog.

Everything except the catalog already works with the setup recommended in #95: a reverse proxy terminates TLS, injects x-opencodex-api-key, and forwards Authorization untouched, so ChatGPT passthrough keeps working per machine. Routed models are reachable by exact id (codex -m anthropic/claude-opus-5). What is missing is a supported way for a client to obtain the catalog that makes those models appear in the picker.

What prevents this today?

Codex builds its picker from files in the client's CODEX_HOME, not from a call to the proxy. The proxy already generates exactly those files on the server:

[opencodex] Codex runtime: /usr/local/bin/codex (version=0.146.0, source=configured)
   + 2 models appended to Codex catalog (/home/node/.opencodex/codex-home/opencodex-catalog.json)

There is no HTTP route that returns them. I went through the /api/* surface in 2.7.42 and there is no catalog route.

GET /v1/models is not a substitute. It returns identifiers only:

{ "id": "fakeprov/cool-model", "object": "model", "created": 0, "owned_by": "openai" }

A generated catalog entry carries roughly 29 fields, including base_instructions, comp_hash, context_window, max_context_window, supported_reasoning_levels, truncation_policy, and input_modalities. Reconstructing that client-side would mean reimplementing the catalog builder, and it still could not be done without provider credentials.

Running ocx on the client does not help either, and quietly makes things worse. On a credential-less client, ocx sync rebuilds routed entries from the local provider list and therefore deletes them. Measured in an isolated CODEX_HOME/OPENCODEX_HOME:

routed entries in catalog                      → ["fakeprov/cool-model"]
routed entries after a provider-less ocx sync  → []

The injector itself works well remotely — with hostname pointing at the server, ocx sync writes a correct provider table including env_http_headers — but injection and catalog rebuild are bundled in every command that exposes them (sync, init, ensure, restore back), so the injector cannot be used on its own. ocx ensure additionally tries to start a local proxy, which is wrong on a client.

The net effect is that every operator who follows the #95 recommendation has to build their own file distribution mechanism, which is also what the caveat in that thread currently advises.

What should OpenCodex do?

Expose the catalog the proxy already generates through the existing token-authenticated management API:

GET /api/catalog   →   the current opencodex-catalog.json content

Subject to the same admission rules as the rest of /api/*, so on a non-loopback bind it requires x-opencodex-api-key or Authorization: Bearer and nothing new is opened up.

It would help if the response let a client detect Codex version skew, since the catalog embeds base_instructions and comp_hash from the Codex build on the server, and a client running a different Codex version can end up with subtly wrong entries. A response header or a small JSON envelope would both work; I have no preference on the shape.

A second route for the stale-stamped models_cache.json variant would be convenient but is not strictly required, because ocx sync-cache already rebuilds it locally from the on-disk catalog without touching routed entries. Measured on the same isolated setup:

after sync-cache (no providers): ["fakeprov/cool-model"]
models_cache stamp: 2000-01-01T00:00:00Z 0.0.0 1 routed

So clients that have ocx installed need only the single catalog route. Clients that only have Codex would benefit from the second one.

This is deliberately read-only exposure of a file the proxy already writes. It does not touch tenant identity, per-user authorization, or attribution — the parts described as project-scale work in #95.

Example usage or interface

On a client machine, after the one-time config injection:

curl -fsS -H "x-opencodex-api-key: $OPENCODEX_API_AUTH_TOKEN" \
  https://proxy.example.com/api/catalog > "${CODEX_HOME:-$HOME/.codex}/opencodex-catalog.json"
ocx sync-cache

The client config.toml is unchanged from what non-loopback injection already produces, except that a reverse proxy allows an https base URL:

model_provider = "opencodex"
model_catalog_json = "/home/me/.codex/opencodex-catalog.json"

[model_providers.opencodex]
name = "OpenCodex Proxy"
base_url = "https://proxy.example.com/v1"
wire_api = "responses"
requires_openai_auth = true
env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" }

Before and after, for an operator adding a provider on the server:

today   add provider in the dashboard → resync → copy opencodex-catalog.json out of the
        server by whatever mechanism the operator built → distribute to every client
after   add provider in the dashboard → resync → each client runs one authenticated GET

Alternatives or workarounds

What I run today: a static file server sidecar that mounts the catalog directory read-only and is exposed under a path prefix on the same reverse-proxy host, plus a small client script that fetches opencodex-catalog.json and models_cache.json. It works, and it is a reasonable thing for an operator to build, but it is a second service to run and it is re-invented independently by everyone who hits this.

scp/rsync from the proxy host also works, but it requires SSH access to the server from every client machine, which is a much larger grant than an already-issued admission token.

Client-side ocx does not work, for the reasons measured above.

One small note in case it is useful: the guidance in #95 mentions that clients "can fetch it from the proxy's /v1/models". In practice that response carries identifiers only, so a catalog file is still required. Mentioning it only because that sentence is the natural first thing an operator tries.

Additional context

Measured on 2.7.42 running in Docker, with the Codex CLI installed in the image as the catalog source. Happy to open a PR against dev if the endpoint shape is agreed.

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.

Activity

  1. github-actions commented on Jul 29, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: unknown.

  2. Wibias commented on Jul 30, 2026

    @Wibias
    Contributor

    [shipping-github] Research review

    Claim: Expose the generated opencodex-catalog.json via authenticated GET /api/catalog for remote Codex clients.
    Checked against: dev@be177ea5

    Field Finding
    Still an issue on latest development? yes
    Exact issue No management route returns the full Codex catalog file; GET /v1/models / GET /api/models are not substitutes
    Fixed on development? no
    Open PR covering this? none
    Duplicate of? related to open #95 (hosting caveat), not a duplicate; #357 was the external aggregated model API
    Security relevance possible — auth-gated management read of catalog (includes base_instructions / model metadata); must stay behind existing /api/* admission
    Priority middle — blocks documented multi-machine #95 workflow; workarounds exist (sidecar/scp)
    Verdict Needs fix on development

    Evidence: Management route inventory under src/server/management*.ts has no /api/catalog. Catalog still written only to CODEX_HOME paths.
    Gaps: Shape of envelope/version-skew header not decided (reporter offered either).

  3. Wibias commented on Jul 30, 2026

    @Wibias
    Contributor

    [shipping-github] Security review

    Checked against: dev@be177ea5
    Scope: Proposed GET /api/catalog (and credential class for remote catalog distribution)

    Severity Area Finding Next
    medium authz / credential split Design risk: serving the full Codex catalog under /api/* must not silently widen what a data-plane admission credential can read. Today /api/* is management-plane only; the issue text describes clients using the same header pattern as data-plane admission. Collapsing those classes would expand management-adjacent disclosure (model metadata / base_instructions) to every inference credential holder. Keep management auth for mutations; 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 or quietly accept data-plane secrets on /api/*.
    low information disclosure Catalog JSON is not a secret store, but it is richer than GET /v1/models and should remain behind authenticated non-loopback admission; no credentials/tokens must ever be embedded in the payload. Auth-gate the route; add a regression that response bodies never contain apiKey / admission prefixes.

    Summary: No live vulnerability on tip (route does not exist yet). The security work is in the admission design of the feature.
    Residual: Version-skew / base_instructions trust is an operator concern, not an auth bypass.

  4. Wibias commented on Jul 30, 2026

    @Wibias
    Contributor

    [shipping-github] Opened PR #772 to address this.

  5. lidge-jun commented on Jul 31, 2026

    @lidge-jun
    Owner

    Implemented on dev (verified at bf9bc1ac8), landed in 5d7ab0cb7.

    GET /api/catalog now serves the generated Codex catalog to remote clients — src/server/management/model-routes.ts:101-112. It reads the configured catalog path, returns 404 when no catalog has been generated yet, and sets a version header when one is available. Management authentication applies before dispatch (src/server/index.ts:391-395), so it is not an unauthenticated route.

    Two notes on scope, so nothing is implied that was not built:

    • The response re-serializes parsed JSON rather than streaming the raw file bytes. Equivalent content, not a byte-for-byte copy.
    • The optional models_cache.json route mentioned in the thread was not implemented.

    If either matters for your use case, a follow-up issue is welcome. Closing the core request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions