Skip to content

fix(client): refuse a hub catalog the local Codex CLI cannot parse - #4240

Merged
lidge-jun merged 2 commits into
devfrom
codex/260911-l4-client-catalog
Sep 11, 2026
Merged

fix(client): refuse a hub catalog the local Codex CLI cannot parse#4240
lidge-jun merged 2 commits into
devfrom
codex/260911-l4-client-catalog

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

A remote client reported connected, catalog present and freshly synced, token owned, rotation clean — and its installed Codex CLI still exited before making a single request:

failed to parse model_catalog_json <CODEX_HOME>/opencodex-catalog.json as JSON:
unknown variant `max`, expected one of `none`, `minimal`, `low`, `medium`, `high`, `xhigh`

The connection state answered a different question from the one the operator was asking. It proved the hub was reachable and the credential worked; nothing on that path proved the selected local runtime could consume what was written. validateRemoteCatalog checks JSON shape only, and connect writes the hub's bytes verbatim, so the effort clamp that already exists for local catalog sync never saw the downloaded file. That is why ocx status could report an active effort clamp for the older runtime while the materialized client catalog still failed to parse — the two paths never met.

The client path now establishes compatibility before it materializes anything, and fails closed, which is the decision recorded for this issue:

  • catalogEffortCompatibility reports which reasoning levels the observed local ladder would reject, without mutating the catalog. The clamp beside it is mutate-and-continue, which is right when this process owns the file it is about to write and wrong for a catalog that has to keep agreeing with hub truth.
  • Both hub-download writes — connectClient and syncConnectedClient — are gated on it. The download is refused before the write, so the previous known-good catalog is still on disk, no connection is committed, and no success is printed. Refusing before the write is stronger than writing and restoring: there is no window in which an unparseable catalog exists.
  • The two restore paths stay ungated. Refusing to restore a catalog this machine already accepted would strand the client with no catalog at all.

The refusal names the level, what was preserved, and both ways out — upgrade the Codex CLI, or point CODEX_CLI_PATH at a build that supports it and run ocx sync. It never suggests editing the hub catalog and it does not touch running Codex processes; the issue rules both out.

One decision the issue left open. An unobservable local ladder is reported as unverified rather than incompatible, and does not block. A client machine may legitimately have no Codex CLI to observe, and refusing on absent evidence would break a working configuration. I read "preserve the prior known-good catalog if compatibility cannot be established" as the incompatible case — the alternative to the compatible-projection branch offered in the same sentence — not as the inconclusive one. Flagging it because the other reading is defensible and would be a one-line change.

Verification

The local product suite, typecheck and GUI build were NOT RUN, by operator instruction for this dispatch round. bun test, bun run test, bun run test:changed, bun run typecheck, bun run build:gui and bun install were all NOT RUN. Hosted CI on this exact pushed head is the product evidence for this PR.

What was checked instead, by read-only review agents and deterministic source scans:

  • All four atomicWriteFile(DEFAULT_CATALOG_PATH, …) sites were enumerated and classified: the two hub-download writes are gated and the two restores are not. No other code in the repository writes a hub download to that path.
  • The gate precedes commitClientConnection, and throwing leaves writtenCatalogFingerprint null so the restore path is correctly skipped — the bytes are never written rather than written and rolled back.
  • Every new src/ line was reviewed against strict tsc rules by hand, since typecheck was not run: the RawEntry type-only import, the unknown narrowing on entry.slug and entry.default_reasoning_level, and the new optional dep field.
  • The new import does not create a cycle into src/client and adds no module-load side effect; src/client/connect.ts already reached src/codex/catalog/sync.ts, which already imports effort.ts. The Lab boundary files are untouched.
  • Every command named in the error message was verified to exist. An earlier draft recommended ocx codex-runtime, which does not exist; it was replaced with CODEX_CLI_PATH + ocx sync, both anchored in the CLI registry and in doctor.ts's existing advice.
  • Test non-vacuity was checked case by case, and one test title that overclaimed what it asserted was corrected.

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.

This targets codex/260911-l4-stop-refusal (PR #4237) as a stacked child and will be retargeted to dev once its parents land. #4204 was removed from this round by the feasibility audit, so the effort clamp is not bound to the Desktop runtime here; this PR only adds a read-only compatibility view of the existing observed ladder.

Closes #4207

A connected client reported connected, catalog present, token owned and
rotation clean, while its installed Codex CLI exited before making a single
request:

  failed to parse model_catalog_json ... unknown variant `max`,
  expected one of `none`, `minimal`, `low`, `medium`, `high`, `xhigh`

The connection state answered a different question from the one the operator
was asking. It proved the hub was reachable and the credential worked; nothing
on that path proved the selected local runtime could consume what was written.
validateRemoteCatalog checks JSON shape only, and connect wrote the hub's bytes
verbatim, so the effort clamp that already exists for local catalog sync never
saw the downloaded file.

The client path now establishes compatibility before it materialises anything.
catalogEffortCompatibility reports which reasoning levels the observed local
ladder would reject, without mutating the catalog - the clamp beside it is
mutate-and-continue, which is right when this process owns the file it is about
to write and wrong for a catalog that must keep agreeing with hub truth. Both
hub-download writes, connect and sync, are gated on it and fail closed: the
download is refused before the write, so the previous known-good catalog is
still there, no connection is committed, and no success is printed. The two
restore paths stay ungated, since refusing to restore a catalog this machine
already accepted would strand the client with none at all.

The refusal names the level, what was preserved, and both ways out - upgrade
the Codex CLI, or point CODEX_CLI_PATH at a build that supports it and run
`ocx sync`. It never suggests editing the hub catalog and it does not touch
running Codex processes, both of which the issue rules out.

An unobservable local ladder is reported as unverified rather than
incompatible, and does not block. A client machine may legitimately have no
Codex CLI to observe, and refusing on absent evidence would break a working
configuration; the issue asks to preserve the prior catalog when compatibility
cannot be established, which is the incompatible case, not the inconclusive
one. That call is mine and is recorded here.

Closes #4207
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 10, 2026 23:41
@coderabbitai

coderabbitai Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • ^dev$
  • ^preview$

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: Advanced

Run ID: 8396c9cf-86b6-43b9-9629-3060dee4f88d

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.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-10T23:44:37.106868Z 9627fe2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 10, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9627fe2d61

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/client/connect.ts
// Fail closed BEFORE the write (#4207). The hub being reachable and the credential working
// does not mean the selected local Codex runtime can consume what arrived: an older CLI
// exits on an unknown reasoning level before making a single request, while connect
// reports success. Refusing here leaves the previous catalog in place untouched, rather

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Skip Codex compatibility checks for Claude-only clients

When selectedClients is ["claude"], this unconditional check can reject the hub catalog solely because an installed, older Codex CLI does not support a level such as max, even though Codex will not consume the catalog and the Claude path only reads context-window metadata from it. The same problem occurs during syncConnectedClient, where the check also runs regardless of initial.connection.selectedClients; guard both checks with the corresponding selectedClients.includes("codex") condition and add a Claude-only regression case.

AGENTS.md reference: src/AGENTS.md:L10-L10

Useful? React with 👍 / 👎.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 71 / 80

이 PR은 지금 dev(HEAD df65a2587, 패키지 2.51.0) 위에서 도는 L4 서비스/CLI 레인의 WP3입니다. 베이스는 #4237(codex/260911-l4-stop-refusal)이고, 그 부모는 #4235(codex/260911-l4-service-cli)입니다. 부모가 dev에 안착하기 전에는 이 PR도 dev에 바로 못 붙습니다. 고치는 문제는 #4207입니다.

제보 장면은 이렇게 읽힙니다. 원격 클라이언트가 connected이고, 카탈로그도 있고, 토큰도 있고, 로테이션도 깨끗한데, 설치된 Codex CLI는 요청을 한 번도 못 보내고 죽습니다. 에러는 opencodex-catalog.json 안의 reasoning level max를 모른다는 파스 실패입니다. 연결 상태가 답한 질문은 “허브와 자격 증명이 되나?”였고, 운영자가 묻던 질문은 “지금 고른 로컬 런타임이 받은 카탈로그를 먹을 수 있나?”였습니다. 두 질문이 갈라져 있었습니다. 지금 devvalidateRemoteCatalog는 JSON 모양만 보고, connectClient/syncConnectedClient는 허브 바이트를 그대로 DEFAULT_CATALOG_PATH에 씁니다. 로컬 카탈로그 동기화에 이미 있는 effort clamp는 이 다운로드 경로를 보지 않습니다. 그래서 ocx status는 옛 CLI에 대한 clamp가 켜져 있다고 말하면서도, 디스크에 깔린 클라이언트 카탈로그는 파스가 깨질 수 있었습니다.

이 PR은 그 구멍을 닫습니다. src/codex/catalog/effort.tscatalogEffortCompatibility를 새로 둡니다. 이건 관찰된 로컬 사다리를 기준으로 거절될 reasoning level을 보고만 하고, 카탈로그를 고치지 않습니다. 옆에 있는 clamp는 “이 프로세스가 곧 쓸 파일을 소유할 때” 맞고, 허브 진실과 맞춰 둬야 하는 다운로드에는 틀립니다. src/client/catalog-compatibility.tsassertClientCatalogCompatible가 그 결과를 게이트로 쓰고, connect.ts의 허브 다운로드 쓰기 두 곳(connect·sync)만 막습니다. 쓰기기 전에 거절하므로, 이전 정상 카탈로그가 디스크에 남고, 연결 커밋도 안 되고, 성공 메시지도 안 나갑니다. 복원 경로 두 곳은 의도적으로 게이트 밖입니다. 이미 이 머신이 받아들인 카탈로그를 되살리는 걸 막으면 카탈로그가 아예 없어질 수 있습니다. 거절 메시지는 거절된 level, 이전 카탈로그 보존, 그리고 두 갈래 출구(Codex CLI 업그레이드, 또는 CODEX_CLI_PATH + ocx sync)를 말합니다. 허브 카탈로그를 손으로 고치라는 말은 없고, 돌아가는 Codex 프로세스도 건드리지 않습니다. #4207 Expected와 맞습니다. #4204 Desktop clamp는 이 라운드 feasibility에서 빠졌고, 이 PR은 관찰된 사다리의 읽기 전용 호환 뷰만 추가합니다.

테스트 tests/clients/client-catalog-compatibility.test.ts는 옛 CLI 사다리(이슈 파스 에러 그대로), default-only 불일치, 관측 불능=비차단, 깨진 JSON=unverified(#4169 반복 금지), 그리고 소스 오라클로 connect/sync만 게이트·restore는 비게이트임을 고정합니다. 로컬 제품 스위트·typecheck·GUI 빌드는 돌리지 않았고, 증거는 호스티드 CI로 두었습니다. 지금 이 시각 기준 hygiene/gates/docker smoke는 통과했고, ubuntu/macOS test shard와 npm-global windows는 아직 pending입니다. 본문에 Closes #4207이 있습니다.

src/client/catalog-compatibility.ts assessClientCatalogCompatibility / unverified - 로컬 사다리를 관측하지 못하면 차단하지 않고 통과시킵니다. PR 본문이 이 선택을 명시했고, “호환을 확정할 수 없으면 이전 카탈로그를 보존”을 불확실 케이스로 읽으면 한 줄로 막을 수도 있습니다. 지금은 증거 없음=차단 아님입니다. 클라이언트 머신에 Codex CLI가 아예 없는 정상 구성이 있다는 근거는 납득됩니다.

src/client/connect.ts assertClientCatalogCompatible (connect·sync) - 게이트가 lifecycle/config 락 에서 CLI 사다리를 본 뒤, 락 안에서 씁니다. 락을 잡은 채 서브프로세스를 안 돌리려는 선택으로 보이고 맞습니다. 관측과 쓰기 사이에 CODEX_CLI_PATH가 바뀌는 레이스는 사실상 운영 예외이니, 이번 범위에서 막을 필요는 없습니다.

src/codex/catalog/effort.ts catalogEffortCompatibility - supported === null이면 compatible: true를 돌려줍니다. 호출자는 assessClientCatalogCompatibility가 먼저 unverified로 갈라 주므로 지금 connect 경로는 안전합니다. 순수 함수만 따로 쓰면 “관측 실패=호환”으로 오해하기 쉬우니, 주석에 이미 있는 경고를 유지하세요.

베이스 codex/260911-l4-stop-refusal (#4237) / 그 부모 #4235 - 이 PR은 dev 직행이 아닙니다. 머지 순서는 #4235#4237 → 이 PR → dev 리타깃(또는 스택 머지)입니다. 부모 CI·정책이 흔들리면 자식도 같이 밀립니다.

메인테이너의 판단이 필요한 지점

너의 추천
부모 #4235·#4237을 먼저 랜딩한 뒤 이 PR을 스택으로 머지하세요. 제품 test shard와 npm-global windows가 초록인지 한 번 더 보고, unverified 통과 결정은 지금 값(증거 없음=비차단)을 유지해도 됩니다 — CLI 없는 클라이언트까지 막으면 #4207보다 큰 회귀가 납니다. 머지 커밋이 dev에 안착하면 #4207은 Closes로 닫히게 두세요. Desktop(#4204)은 이 PR에 섞지 말고 따로 두세요. types/config 분할에 치일 범위는 아닙니다.

이 댓글은 grok-bot이 작성했습니다

Base automatically changed from codex/260911-l4-stop-refusal to dev September 11, 2026 00:43
@lidge-jun
lidge-jun merged commit babb764 into dev Sep 11, 2026
32 of 33 checks passed
@lidge-jun
lidge-jun deleted the codex/260911-l4-client-catalog branch September 11, 2026 00:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant