Skip to content

feat(server): add least-privilege data-plane catalog endpoint - #1557

Draft
LeoWang331 wants to merge 18 commits into
lidge-jun:devfrom
LeoWang331:feat/809-v1-catalog
Draft

feat(server): add least-privilege data-plane catalog endpoint#1557
LeoWang331 wants to merge 18 commits into
lidge-jun:devfrom
LeoWang331:feat/809-v1-catalog

Conversation

@LeoWang331

@LeoWang331 LeoWang331 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Closes #809

Draft, and not claiming completeness. The mechanical findings from review are fixed.
The distribution-safety policy is unresolved: the safety check in the tree is a
heuristic denylist with verified gaps, and choosing its replacement is a maintainer
decision (see Open maintainer decisions). This PR also touches
src/server/auth-cors.ts, which is on the sponsored authentication surface, so a
maintainer must complete security review and apply maintainer-sponsored. The author
cannot and will not self-apply it.

Summary

  • Adds GET/HEAD /v1/catalog, a read-only data-plane projection of the generated Codex
    catalog, so a remote client can fetch model metadata with the credential it already uses
    for inference. /api/* is untouched and gains no data-plane exception; the data-plane
    credential is still denied on every management route, including GET /api/catalog.
  • Puts one catalog materialization authority in src/codex/catalog/distribution.ts: a
    hard-bounded source read before JSON.parse, a safety verdict, and one serializer.
    Management GET /api/catalog and data-plane GET/HEAD /v1/catalog both go through it.

Safety check: what it is, and what it is not

It is a heuristic denylist, not a guarantee. It rejects a document (content-free
500 catalog_unsafe, both planes) when a key spelling names management state or a string
value matches a recognizable credential / identity / home-path shape. Representation
equivalence is enforced — Authorization scheme matching is case-insensitive, and key
normalization drops every non-alphanumeric character, so account.id, account_id,
account-id, accountId, and account id are one key.

Verified gaps, pinned as known behavior in the tests rather than papered over:

Not detected Why not widened
Raw provider base URL in an ordinary string field A blanket URL rule rejects every real catalog — instruction text contains URLs
Arbitrary-format token (e.g. bare hex) in an ordinary field No shape to match without also matching legitimate values
Arbitrary account identifier under an innocuous key Same
Non-home absolute path, e.g. D:\ocx\config.json A blanket absolute-path rule rejects legitimate instruction text

Additionally, key normalization strips all non-ASCII, so a non-ASCII key spelling (for
example a Cyrillic or fullwidth name) normalizes to the empty string and is accepted.

Widening the denylist is not the fix: it converts real catalogs into permanent 500s on
both routes. Closing these gaps needs a different strategy, which is a policy call.
The no-leakage invariant is therefore not fully enforced today — it is approximated by
this heuristic, and that is exactly what the open decision below is about.

Compatibility is therefore tested, not assumed. A rejected document takes both routes
down together, so the suite asserts that the pinned upstream snapshot
(src/codex/data/upstream-models.json) and the OpenCodex-owned extension fields a
generated catalog carries are safe to distribute. A future Codex schema addition that trips
a rule fails there instead of in production.

Open maintainer decisions (recorded, not resolved)

  1. Enforcement strategy. Candidates: heuristic rejection (in the tree today, and
    non-guaranteeing); a versioned canonical distribution DTO / strict field projection
    produced at the trusted writer boundary, which guarantees the field shape that
    leaves the boundary but not that an allowed field is free of secrets, since permitted
    strings such as base_instructions, description, display_name, model ids and
    ownership fields can still carry sensitive values; exact-value comparison against live
    configured secrets, account identities, provider base URLs/headers and filesystem paths,
    which is precise for known values but cannot decide unknown management-shaped
    fields; or a hybrid of the last two, which is what is required if the no-leakage
    invariant must actually hold for known live secrets. Provenance restriction is at best
    an auxiliary input constraint, not a guarantee — ocx sync deliberately preserves
    on-disk and user-authored rows (src/codex/catalog/sync.ts:1253-1257, :888-901) and
    replaces only catalog.models before serializing the whole document (:1392-1422), so
    an "OpenCodex-generated" file is not secret-free by construction.
  2. Shared rejection on the management plane. Because the verdict lives in the one
    shared materializer, GET /api/catalog now also refuses an unsafe or oversized-source
    document; it previously served any parseable file. This is a consequence of the single
    materialization step, not an approved policy, and the management-API reference says so.
  3. Statuses and public error codes for catalog_unsafe, catalog_too_large,
    catalog_source_too_large, plus the 8 MiB / 32 MiB thresholds, auth-before-method
    ordering, and byte-identical (vs merely equivalent) cross-route bodies.

structure/05_gui-and-management-api.md separates these tiers explicitly. Only the plane
split, the single materialization authority, GET/HEAD-only access, the no-leakage
requirement itself, and "data-plane credentials cannot reach /api/*" are presented as
maintainer-accepted; the enforcement strategy and detector rule set are marked
executor-selected. No option above is presented as chosen.

Bounded input and size errors

  • CATALOG_SOURCE_MAX_BYTES (32 MiB): size is checked on the open descriptor before any
    bytes are read, so an oversized file never reaches memory or JSON.parse.
  • DATA_PLANE_CATALOG_MAX_BYTES (8 MiB) measures serialized UTF-8 response bytes and
    refuses rather than truncating. Pinned at limit−1 / exact / limit+1, plus a multi-byte
    UTF-8 case proving bytes (not characters) are counted.
  • The two limits answer with distinct codes. Source refusal is
    catalog_source_too_large and says only that the source exceeded the safe read limit;
    reusing catalog_too_large there asserted a serialized size that was never computed (a
    33 MiB pretty-printed file can compact below 8 MiB). Route-level GET and HEAD tests cover
    it, including that no partial catalog or source content appears in the observable
    response and that no-store/nosniff still hold.
  • On the management plane the same two refusals surface in the ordinary management envelope
    ({ "error": "<message>" }), not the data-plane type/code envelope, and that route
    does not apply the 8 MiB serialized ceiling. Both are documented in the management-API
    reference in all six locales.

Method, header, and CORS semantics

Aspect Behavior
Methods GET/HEAD; POST/PUT/PATCH/DELETE answer 405 with Allow: GET, HEAD
Headers Every response the route itself generates — 200, HEAD, 401, 403, 404, 405, 500 — carries Cache-Control: no-store and X-Content-Type-Options: nosniff, so a cached 404 catalog_not_found cannot hide a catalog generated later. The global bodyless OPTIONS preflight is answered before the route runs and does not carry these two route headers; the docs state that exclusion explicitly
HEAD Same status/headers as GET plus exact Content-Length, no body
CORS Access-Control-Allow-Methods includes HEAD; a real OPTIONS preflight test carries Origin + Access-Control-Request-Method: HEAD + Access-Control-Request-Headers: x-opencodex-api-key, then proves the promised HEAD succeeds
OPTIONS Global CORS preflight answers a bodyless 204 before route authentication, here as everywhere. The docs no longer claim every anonymous non-read method returns 401
Version x-opencodex-codex-version when authoritative; omitted, never fabricated
Loopback GET/HEAD /v1/catalog pinned as 404 on the optional unauthenticated loopback listener, with the public remote bind still answering 401

Auth matrix

/v1/catalog is listed in the shipped AUTH_MATRIX (bearer / dedicated / x-api-key all
accepted, same admission as /v1/models) and the real-request matrix test drives all three
header forms against it as a GET route. This addresses @Wibias's requested change — that
review is still CHANGES_REQUESTED and needs re-review
, since a review cannot be
satisfied by the author asserting it was. The branch has also been rebased onto current
dev
as that review asked; history is linear with no merge commits.

Documentation

structure/05_gui-and-management-api.md plus 18 docs-site files (English +
ja/ko/ru/zh-cn/zh-tw), covering the data-plane reference, the management-plane reference,
and the Codex-integration guide.

  • Codex-integration guide, all six locales — least-privilege workflow. The remote-client
    catalog download no longer instructs operators to send OPENCODEX_ADMIN_AUTH_TOKEN to
    client machines and fetch GET /api/catalog — the exact management-credential
    distribution this issue exists to remove. Remote clients are now directed to
    GET /v1/catalog with x-opencodex-api-key: $DATA_PLANE_KEY, linked to the locale's
    reference/proxy-formats/ page for the canonical atomic download workflow rather than
    duplicating the shell snippet, followed by ocx sync-cache. GET /api/catalog is
    described only as the management-plane route for the dashboard and operator tooling on
    the trusted machine.
  • Codex-integration guide, all six locales — accuracy of the response description. The
    guides previously claimed the response contains "no provider credentials". That is
    stronger than the implementation can prove, given the verified false negatives listed
    above. They now state only observable behavior: the response is the generated
    opencodex-catalog.json document; the data-plane route applies the current
    catalog-distribution safety checks and refuses content it recognizes as credential-,
    identity-, or configuration-shaped; and the enforcement strategy remains subject to
    maintainer review. No absolute guarantee replaces the removed one. The
    x-opencodex-codex-version skew explanation is unchanged.
  • Reference pages: the /v1/catalog contract, its error table, the authentication
    matrix row, the credential-class table, and the multi-machine workflow, whose snippet is
    interruption-safe — mkdir -p and a bare mktemp with an explicit same-directory
    template compatible with GNU and macOS/BSD each fail fast, tmp is initialized before
    any trap, cleanup is bound to EXIT alone, HUP/INT/TERM handlers exit
    129/130/143 rather than only cleaning up, the previous catalog survives until both
    curl and the same-directory mv succeed, and every handler is cleared after a
    successful rename.

Verification

Runs on Windows with Bun 1.3.14 in this dedicated Issue #809 worktree. Commands 1–6 were run
against head 86c0636f2 (rebased onto upstream/dev = 570347304). The most recent commit
is documentation-only and touches no TypeScript; for it, only the two documentation checks
were re-run, per repository guidance not to rerun passing checks merely for confidence.

# Command Exit Duration Result
1 bun test tests/v1-catalog-route.test.ts 0 42.1s 65 pass, 0 fail
2 bun test tests/api-catalog-route.test.ts tests/api-key-attribution.test.ts 1 52.1s 20 pass, 3 fail — all three are timeouts; unresolved, see disclosure 3
3 bun run typecheck 0 5.3s clean
4 bun run privacy:scan 0 16.8s passed
5 git diff --check upstream/dev...HEAD 0 0.1s clean (re-run on the final head)
6 cd docs-site && bun install --frozen-lockfile && bun run build 0 12.2s + 38.7s no lockfile changes; 265 pages, [build] Complete! (re-run on the final head)

Rebase note. The branch was rebased onto current dev (previously it was brought
current with merge commits). The rebase was clean with no conflicts, and the PR diff was
byte-identical before and after (146,273 bytes both), so no content was lost or altered.
History is linear.

Upstream delta review. upstream/dev changed src/codex/catalog/parsing.ts and
sync.ts, which this PR's materializer imports from. Reviewed: the change only flips the
existing supports_search_tool boolean to !isCursorEntry and conditions
web_search_tool_type; it introduces no new catalog key names, and
parseCatalogJson/readCodexCatalogPath signatures are unchanged. supports_search_tool
normalizes to supportssearchtool, which matches no safety rule, so the safety verdict is
unaffected.

Disclosure 1 — the catalog test ran more than once across this PR's history. An earlier
batch's first attempt at command 1 hung for ~968s and was killed: that draft's email
detector used an unanchored regex that is quadratic under backtracking on the new 8 MiB
fixtures. It was rewritten as a linear @-anchored scan.

Disclosure 2 — tests/loopback-listener-integration.test.ts is not in the table. Its
one failure (Codex injection targets the loopback listener, failing with
CodexUserIdentityRefusal: Windows effective-account lookup returned an empty value) was
classified earlier by running the same command on clean upstream/dev in the same
environment, which failed identically. That comparison was performed by temporarily
checking out upstream/dev inside this dedicated Issue #809 worktree and returning to the
branch; no other checkout was used or modified.

Disclosure 3 — command 2 failed with three timeouts, and they remain UNRESOLVED. The
failures are attribution reaches usage.jsonl > the environment token records its own kind, … > search and realtime call-create each add an attributed row, and AUTH_MATRIX is true of the running server > every cell matches a real request. All three report
a beforeEach/afterEach hook timed out or this test timed out after 5000ms, with a
killed 1 dangling process notice, immediately after command 1 had spawned dozens of
servers in 42s. No assertion mismatch was reported, which is what an upstream
supports_search_tool regression would produce, and the same command passed 23/23 before
the rebase. Best available classification is local resource/port contention on this Windows
machine, but this was not re-run and no clean-upstream baseline was taken for it, so
it is not cleared. Repository CI must settle it.

Not run locally: the full suite. Cross-platform verification is repository CI's job,
and CI has not run — see blockers.

Changes

18 commits, linear (no merge commits), 0 behind / 18 ahead of upstream/dev
(570347304). 28 files, +2545 / −69.

Area Files
Shared authority src/codex/catalog/distribution.ts, src/codex/catalog.ts
Data-plane route src/server/data-plane-catalog.ts, src/server/index.ts
Management route src/server/management/model-routes.ts
Auth matrix / CORS src/server/auth-cors.ts
Tests tests/v1-catalog-route.test.ts, tests/api-key-attribution.test.ts, tests/loopback-listener-integration.test.ts
Docs structure/05_gui-and-management-api.md, 18 docs-site files

Current blockers

  • Missing sponsorship. maintainer-sponsored is not on this PR (Issue [Feature]: add least-privilege GET /v1/catalog for remote Codex clients #809 has it; the
    label does not transfer). hygiene and enforce-target have been failing on
    unsponsored_surface for src/server/auth-cors.ts, and the PR carries
    intake: hygiene-blocked. Requires maintainer security review.
  • Active CHANGES_REQUESTED review from @Wibias. The requested AUTH_MATRIX row is
    implemented and the branch is rebased onto current dev, but the review still stands and
    needs a maintainer to re-review the current head.
  • Three unresolved local timeout failures in tests/api-key-attribution.test.ts
    (disclosure 3). Not reproduced against a clean baseline and not re-run; CI is what would
    settle them.
  • Cross-platform CI and React Doctor are action_required and have never run on this
    branch; a maintainer must approve workflow runs for a fork PR.
  • CodeRabbit skipped the PR because it is a draft; no Codex review exists. Neither
    has produced findings yet, so "all findings resolved" cannot be asserted from evidence.
  • The enforcement-strategy decision above is unresolved.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.
    Admission helpers are unchanged; the auth-surface diff is one matrix row plus HEAD
    in Access-Control-Allow-Methods. The safety check's limits are documented above
    rather than overstated, and the user-facing guides no longer claim the response
    contains no provider credentials.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • All CI tests are green on my local testing.

  • I pushed my PR to the latest dev commit.

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a58ab4ca-0747-4629-9b72-da1012345fd8

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the enhancement New feature or request label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • hygiene: unsponsored_surface.

What to do

  • Fix unsponsored_surface — This changes an authentication, workflow, release-automation, or dependency surface. MAINTAINERS.md requires security review for these; ask a maintainer to apply maintainer-sponsored once they have reviewed it. Paths: src/server/auth-cors.ts.
  • Tick all four boxes in the PR description once you're done (currently 1/4).

Review readiness checklist

  • ⬜ All CI tests are green on my local testing.
  • ✅ I pushed my PR to the latest dev commit.
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

1/4 boxes ticked.

This pull request was already a draft. Its draft status will be preserved after every issue above is resolved.
@LeoWang331 Tick the boxes once your local CI is green, your branch is on the latest dev commit, and every correct Codex and CodeRabbit finding is resolved.

@github-actions github-actions Bot added intake: hygiene-blocked Deterministic PR hygiene checks failed and removed intake: hygiene-blocked Deterministic PR hygiene checks failed labels Aug 12, 2026

@Wibias Wibias left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The least-privilege route itself looks well designed and the negative management-plane coverage is strong, but I have one blocking contract issue on this head: /v1/catalog is deliberately omitted from the shipped AUTH_MATRIX in src/server/auth-cors.ts.

That matrix is explicitly the server-driven source of truth for which credential headers each data-plane endpoint accepts, is shipped to the GUI, and is backed by real-request matrix tests. Adding a new authenticated data-plane endpoint while documenting it only in prose leaves that machine-readable/user-facing contract incomplete. The PR body says the row was reverted to avoid putting the PR on the sponsored auth surface; that is not a good reason to let the source of truth drift. This issue is already maintainer-approved architecture, so please add /v1/catalog to AUTH_MATRIX with the behavior the route actually implements (bearer: accepted, dedicated: accepted, xApiKey: accepted) and extend the existing matrix/request coverage accordingly. Handle the maintainer-sponsored gate rather than working around it by omitting the contract row.

Separately, this branch is currently 15 commits behind dev (6c14e343), and Cross-platform CI is still running. After the matrix fix, rebase onto current dev and rerun exact-head CI.

@github-actions github-actions Bot added the intake: hygiene-blocked Deterministic PR hygiene checks failed label Aug 13, 2026
LeoWang331 and others added 17 commits August 13, 2026 05:23
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…lane

Co-authored-by: Cursor <cursoragent@cursor.com>
…tion

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…k probes

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…ed HEAD

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
… real-catalog compatibility

Co-authored-by: Cursor <cursoragent@cursor.com>
…ng refusal

Co-authored-by: Cursor <cursoragent@cursor.com>
…nippet

Co-authored-by: Cursor <cursoragent@cursor.com>
…wnload snippet

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@LeoWang331
LeoWang331 force-pushed the feat/809-v1-catalog branch from 93d83e3 to 86c0636 Compare August 13, 2026 09:39
Co-authored-by: Cursor <cursoragent@cursor.com>
@LeoWang331

Copy link
Copy Markdown
Contributor Author

@Wibias @Ingwannu — Issue #809 / PR #1557 now needs maintainer policy and security decisions before it can leave Draft.

Current head is 5586e4e01b63e5076bee9fedc3eb980b499c925a; the branch is 18 ahead and 4 behind dev (2cdbf66a23f9fd8f2f38dcc702ccd3f2e60ac535) as of this comment.

The mechanical requests from the existing review are implemented:

  • /v1/catalog is restored to the shipped AUTH_MATRIX with bearer, x-opencodex-api-key, and x-api-key accepted, and the real-request matrix test covers it.
  • The branch was rebased onto dev with linear history as requested.
  • All six Codex-integration guides now direct remote clients to GET /v1/catalog with a data-plane key instead of distributing OPENCODEX_ADMIN_AUTH_TOKEN to call /api/catalog.
  • The guides no longer claim the response is guaranteed to contain no provider credentials.
  • The PR remains Draft and does not claim the no-leakage invariant is fully enforced.

The remaining blocker is the catalog-distribution safety policy.

The current implementation is a name/shape-based heuristic denylist. It is explicitly non-guaranteeing:

  • it can miss arbitrary-format tokens, raw base URLs embedded in permitted strings, arbitrary account IDs, non-home paths, and non-ASCII key spellings;
  • it can also reject future legitimate catalog fields whose names look sensitive, returning 500 on both /api/catalog and /v1/catalog.

Our recommendation is:

  1. A versioned canonical distribution DTO / strict projection for the field-shape boundary.

    • This guarantees which fields leave the boundary, but not that free-form values inside permitted fields are secret-free.
    • The complete field set must be derived from the actual Codex schema, pinned snapshot, parser requirements, OpenCodex generation, and golden compatibility fixtures.
    • No schema-version field should be added to the payload without Codex compatibility evidence.
  2. Exact-value comparison as a required companion if protection against known configured secrets is required.

    • It must detect known sensitive values both as complete structured values and when embedded inside permitted free-form strings.
    • It should cover configured data/admin/provider credentials, OAuth values, account identity, provider base URLs/headers, and resolved local paths.
    • Refusals must remain content-free and must never log or identify the matched value.
  3. Provenance may be an auxiliary constraint only.

    • ocx sync preserves on-disk and user-authored rows and unknown top-level content, so an "OpenCodex-generated" file is not secret-free by construction.

If you prefer to keep the heuristic temporarily, we propose a schema- and type-aware fail-closed exception layer only for individually approved sensitive-looking fields. We would not broadly accept base_url or *_token, and we would record that no-leakage remains an approximation.

Please decide:

  1. Safety strategy:

    • A. versioned DTO/projection + exact-value comparison — recommended;
    • B. versioned DTO/projection only;
    • C. temporary heuristic with schema/type-aware exceptions;
    • D. another approach.
  2. Should unsafe/source-too-large refusal also apply to management GET /api/catalog, or should management remain file-faithful?

  3. Keep or change:

    • 8 MiB serialized / 32 MiB source limits;
    • status 500;
    • catalog_unsafe, catalog_too_large, catalog_source_too_large;
    • authentication-before-method ordering;
    • byte-identical versus equivalent cross-route content.
  4. Are the three inbound credential forms and the secondary-loopback 404 behavior accepted as implemented?

Process actions also needed:

  • Wibias's existing CHANGES_REQUESTED review is still active on the old head (a5737ffaa); please re-review the current head (5586e4e01).
  • src/server/auth-cors.ts is on the sponsored surface. After security review, please apply maintainer-sponsored to this PR if approved; the issue's label does not transfer.
  • Cross-platform CI and React Doctor are still awaiting fork-run approval and have not executed.
  • One targeted local command remains unresolved after three timeout-only failures with no assertion mismatch; repository CI must settle it.
  • CodeRabbit skipped the Draft and no Codex review exists yet.

Nothing further will be implemented until a maintainer chooses the policy. The PR will remain Draft.

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

Labels

enhancement New feature or request intake: hygiene-blocked Deterministic PR hygiene checks failed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants