Skip to content

feat(codex): pull an authenticated remote catalog into local Codex state - #4481

Merged
lidge-jun merged 2 commits into
devfrom
codex/260913-carry-4413-catalog-pull
Sep 13, 2026
Merged

feat(codex): pull an authenticated remote catalog into local Codex state#4481
lidge-jun merged 2 commits into
devfrom
codex/260913-carry-4413-catalog-pull

Conversation

@lidge-jun

Copy link
Copy Markdown
Owner

Summary

Carries #4413 by @rrmlima. Closes #3729.

Adds ocx catalog pull <https-url> [--auth-env <NAME>] [--json] [--restart-codex], which installs a complete catalog served by another OpenCodex instance's /v1/catalog endpoint and then synchronizes models_cache.json. ocx sync builds a catalog from locally configured providers and ocx sync-cache rebuilds the cache from the catalog already on disk; neither consumes a finished catalog from another server, so operators were reimplementing authentication, atomic replacement, cache rebuild and process handling per deployment.

Acquisition is fail-closed before any local write. HTTPS is required except on loopback, URL-embedded credentials, queries, fragments and redirects are refused, the body is bounded by size and inactivity, and slugs and input modalities are validated. The token is read only by environment-variable name, never from argv. Catalog and cache writes share the Codex catalog write lock and the atomic writer.

Review findings folded in

A cache rebuild that failed after the catalog write left a new catalog paired with a stale models_cache.json, while the caller was told catalogWritten: false. The write permit rolls back SQLite; replaceActiveCodexCatalog is an atomic file write that nothing else undid. The pull now restores the previous catalog bytes — or removes the file when the home had none — before throwing write_failed. Both cases have a regression test, and I confirmed both fail without the restore.

--restart-codex reported success when only some app-servers stopped. afterCatalogWriteHandleAppServers returns failed and surviving without throwing, so stopped.length > 0 produced ok: true and exit 0 while a stale app-server was still serving the previous catalog from memory. A restart now counts only when nothing failed, nothing survived, and every listed process stopped:

before: {"ok":true,"status":"updated","codexRestarted":true}   exit 0   # one survivor ignored
after:  {"ok":false,"status":"updated","catalogWritten":true,
         "cacheSynced":true,"codexRestarted":false,"code":"restart_incomplete"}   exit 1

The envelope keeps catalogWritten and cacheSynced true there, because those writes did land; reporting the pull as having written nothing would be a second false report.

The unused statSync import is removed.

Documentation now covers the full --json envelope, every failure code with its exit status, the host-root /v1/catalog path contract, and the two deliberate Phase 1 omissions: no ETag/If-None-Match conditional request and no Windows --restart-desktop-app. Identical bytes remain a complete no-op, so a home whose catalog is correct but whose cache is broken is repaired by ocx sync-cache rather than by this command — that limit is now written down instead of implied. The seven localized lifecycle pages carry the command.

This is the tip of lane L in devlog/_plan/260913_contributor_carry_train/, stacked on #4476. Its hosted run is the suite proof for both links.

Verification

  • bun test tests/codex-integration/catalog-remote-pull.test.ts — 30 pass, including the two new rollback regressions
  • Counterfactual: with the restore removed, both new rollback tests fail; with it, all 30 pass
  • bun test tests/ci-workflows/skill-ocx.test.ts tests/test-layout.test.ts tests/test-layout-tooling.test.ts — 33 pass
  • bun test tests/cli/cli-help.test.ts tests/cli/cli-dispatch.test.ts tests/cli/cli-registry.test.ts — 74 pass
  • bun run typecheck, bun run structure:check, bun run privacy:scan, bun run skill:surface + skill:surface:check — all clean
  • cd docs-site && bun run build — 441 pages, required by docs-site/AGENTS.md

The layout entries in scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json came over with the carry and are verified by the two layout guards above. Local full suite was not run, per the lane's instruction.

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.

lidge-jun and others added 2 commits September 13, 2026 15:56
`ocx status` on a connected client read the connection snapshot and the current
`service-api-token` independently, so a reconnect or a key rotation between the two reads
could send the new token to the snapshot's hub, or the snapshot's hub the new connection's
token. The window is real: `collectRemoteHubStatus` awaits a dynamic import before it
reads credentials.

It now rereads the persisted connection and passes a token only when that connection still
matches the snapshot's `serverUrl`, `apiKeyId` and `connectedAt` AND the token fingerprint
matches that connection's `tokenFingerprint`. Otherwise it skips the live request and falls
back to the snapshot owner's cache, or reports `unavailable`.

A withheld token now carries its own cause. `resolveHubState` reported every null token as
"this client has no usable data-plane token", which is false for a client that reconnected
and holds a perfectly good token for a different hub - the operator would go re-enroll a
credential that is not the problem. The caller supplies the reason through the new
`withheldTokenReason`, so a changed connection, a missing token file and a fingerprint
mismatch are named separately. That folds the maintainer review finding on #4382; it is the
same misdiagnosis class as #4169 in the stop path.

Verification: bun test tests/cli/cli-status-hub-state.test.ts (20 pass),
tests/clients/client-hub-state.test.ts + tests/server/v1-hub-state.test.ts (35 pass),
bun run typecheck, bun run structure:check, bun run privacy:scan, and the docs-site
build required by docs-site/AGENTS.md (441 pages) - the one actionable CodeRabbit finding
on the source pull request.

Carried from #4382 by @luvs01.

Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>
Adds `ocx catalog pull <https-url> [--auth-env <NAME>] [--json] [--restart-codex]`, which
installs a complete catalog served by another OpenCodex instance's `/v1/catalog` endpoint and
then synchronizes `models_cache.json`. `ocx sync` builds a catalog from locally configured
providers and `ocx sync-cache` rebuilds the cache from the catalog already on disk; neither
consumes a finished catalog from another server, which is the gap #3729 has held open.

Acquisition is fail-closed before any local write: HTTPS is required except on loopback,
URL-embedded credentials, queries, fragments and redirects are refused, the body is bounded by
size and inactivity, and slugs and input modalities are validated. The token is read only by
environment-variable name and never from argv. Catalog and cache writes share the Codex catalog
write lock and the atomic writer.

Three review findings on the source pull request are folded in.

A cache rebuild that failed AFTER the catalog write left a new catalog paired with a stale
`models_cache.json` while reporting `catalogWritten: false`. The permit rolls back SQLite;
`replaceActiveCodexCatalog` is an atomic file write that nothing else undid. The pull now
restores the previous catalog bytes, or removes the file when the home had none, before it
throws. Both cases have a regression test, and both fail without the restore.

`--restart-codex` reported success when only some app-servers stopped, so `ok: true` and exit 0
could be returned while a stale app-server still served the previous catalog from memory. A
restart now counts only when nothing failed, nothing survived, and every listed process stopped.
An incomplete restart returns `code: "restart_incomplete"` and exit 1 while keeping
`catalogWritten` and `cacheSynced` true, because the writes did land.

The unused `statSync` import is removed.

Documentation now covers the full `--json` envelope, every failure code and its exit status, the
host-root `/v1/catalog` path contract, and the two deliberate Phase 1 omissions: no
`ETag`/`If-None-Match` conditional request, and no Windows `--restart-desktop-app`. Identical
bytes stay a complete no-op, so a home whose cache alone is broken is repaired by
`ocx sync-cache` rather than by this command. The seven localized lifecycle pages carry the
command too.

Closes #3729

Verification: bun test on catalog-remote-pull, skill-ocx, both test-layout guards, and the CLI
help/dispatch/registry suites (137 pass); bun run typecheck, structure:check, privacy:scan,
skill:surface:check; docs-site build (441 pages).

Carried from #4413 by @rrmlima.

Co-authored-by: rrmlima <137737127+rrmlima@users.noreply.github.com>
@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 13, 2026 07:03
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 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-13T07:10:30.617442Z 394b96d 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.

@coderabbitai

coderabbitai Bot commented Sep 13, 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: 33fc2aa4-f5f4-4c77-942d-6b1736e2da33

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 github-actions Bot added the enhancement New feature or request label Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 69 / 80

설명
이 PR은 @rrmlima의 #4413을 tip(b4dec3469, 2.53.0)으로 캐리하고 #3729를 닫는다. 운영자가 다른 OpenCodex의 완성 카탈로그를 받아 쓰려면 인증·원자적 교체·캐시 재빌드·프로세스 처리를 배포마다 다시 만들고 있었다. ocx sync는 로컬 provider로 카탈로그를 만들고, ocx sync-cache는 디스크 카탈로그로 캐시만 다시 짠다. 원격 /v1/catalog를 소비하는 공식 명령이 없었다.

추가되는 명령은 ocx catalog pull <https-url> [--auth-env <NAME>] [--json] [--restart-codex]다. 구현은 src/codex/catalog/remote.ts와 CLI wiring(catalog.ts/dispatch/help/registry). HTTPS만(루프백 HTTP 예외), URL 내장 자격·쿼리·fragment·리다이렉트·과대 응답·잘못된 카탈로그는 로컬 쓰기 전에 거절. 인증은 --auth-env로 환경변수 이름만 받고 argv로 비밀을 받지 않는다. 카탈로그·캐시는 공유 Codex catalog lock 아래 쓰고, 실패 시 last-good 유지, 동일 바이트면 mtime 보존 no-op. --restart-codex는 실제 쓰기 뒤에만. docs-site lifecycle 다국어와 tests/codex-integration/catalog-remote-pull.test.ts가 계약을 잠근다.

우선순위 69는 멀티 인스턴스 운영 통증을 직접 줄이고, 보안 경계(비밀을 argv에 안 둠·쓰기 전 검증)가 분명하기 때문이다. types/config 분할과 무관하다.

src/codex/catalog/remote.ts pullRemoteCatalog - 원격 취득·검증·원자 교체·에러 분류.

src/cli/catalog.ts / dispatch·help·registry - catalog pull UX와 exit 코드(usage/auth_env_missing 등).

tests/codex-integration/catalog-remote-pull.test.ts - HTTPS/거절/auth-env/원자성 회귀.

심볼 #4413 / #3729 - 캐리 머지 시 둘 다 닫히는 관계. ETag·Desktop 재시작은 범위 밖(본문도 명시).

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

너의 추천
보안·lock 테스트 초록이면 머지하세요. #4413 Landed via #4481, #3729 closes 확인하세요.

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

@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: 394b96dee1

ℹ️ 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".

// Re-check under K: another writer may have installed these bytes while the request was in flight.
const lockedCurrent = existsSync(catalogPath) ? readFileSync(catalogPath) : null;
if (lockedCurrent?.equals(candidate)) return { catalogWritten: false, cacheSynced: false };
replaceActiveCodexCatalog(permit, codexHome, { path: catalogPath, content: fetched.content });

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 Gate the pull on local Codex catalog compatibility

When the remote catalog advertises a reasoning effort unsupported by the selected local Codex CLI (for example, max on an older client), this path writes it without checking runtime compatibility, causing Codex to exit while parsing the catalog before its first request. The existing connect and refresh paths explicitly call assertClientCatalogCompatible before writing (src/client/connect.ts:549-554 and 671-674) to prevent this exact failure; apply the same pre-write gate here and return a typed failure.

Useful? React with 👍 / 👎.

}
if (slugs.has(slug)) invalid("Remote catalog contains duplicate model slugs");
slugs.add(slug);
if (Object.hasOwn(model, "input_modalities")) {

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 Reject rows missing Codex-required catalog fields

When /v1/catalog contains a row without input_modalities, this conditional skips validation and the pull reports success even though Codex's strict parser requires that field (src/codex/catalog/effort.ts:181-183). This can occur because the serving path's readCatalog accepts any object with a models array (src/codex/catalog/parsing.ts:287-298), including custom or older catalogs; validate all required strict fields before replacing the local last-known-good catalog.

Useful? React with 👍 / 👎.

resetCodexAppServerCatalogStateCache();
}

export async function pullRemoteCatalog(input: string, options: PullRemoteCatalogOptions = {}): Promise<PullRemoteCatalogResult> {

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 Update the owned structure contracts for the new writer

This adds a new catalog acquisition and coordinated-write transaction under both src/cli/ and src/codex/, but the commit updates none of the structure documents mapped to those areas in structure/INDEX.md:100-103. Document the remote pull's ownership, validation, locking, rollback, and CLI lifecycle contracts in every mapped structure document so the architecture source of truth does not omit this writer.

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

Useful? React with 👍 / 👎.

| `code` | Meaning | Exit |
| --- | --- | --- |
| `usage` | The arguments were not a valid `catalog pull` invocation | 2 |
| `auth_env_missing` | `--auth-env` named a variable that is not set | 1 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add credential_invalid to the documented failure codes

When the named environment variable is set to an empty, oversized, or control-character-containing token, validateToken emits credential_invalid with exit 1, but the documented JSON failure table omits that code. Scripts using the advertised stable envelope therefore cannot enumerate this real outcome; add the missing row and its exit status.

AGENTS.md reference: docs-site/AGENTS.md:L7-L10

Useful? React with 👍 / 👎.

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant