Skip to content

[Bug]: native main pool does not refresh expired auth.json tokens #2221

Closed

Description

@MarcTCruz

Client or integration

Codex CLI

Area

Authentication and account pool

Summary

When the native Codex __main__ account has an expired access token in ~/.codex/auth.json but still has a valid refresh token, OpenCodex can select that account and forward the expired bearer to /v1/responses. The request returns 401 instead of refreshing the native credential first. Stored account-pool credentials have a refresh path, but the native main credential does not pass through it.

Expected behavior: OpenCodex refreshes the native main credential before upstream I/O. If upstream rejects that bearer once with 401, OpenCodex refreshes and replays the request once with the new bearer.

Reproduction

  1. Use OpenCodex at commit caf20353fa87bf14c5fef078e5d8b0692dd7c92d on the dev branch.
  2. Configure the Codex native main account in ~/.codex/auth.json with an expired access token and a non-empty valid refresh token. Ensure no other account-pool credential is currently usable.
  3. Start the local proxy with ocx start --port 10100.
  4. Send a normal Codex CLI request through http://127.0.0.1:10100/v1/responses.
  5. Observe that the native __main__ account is selected, but its expired access token is forwarded without first using the refresh token.
  6. Observe the 401 response shown below. The request succeeds only after credentials are refreshed outside this native-main request path.

The same missing native credential substitution affects /v1/responses/compact.

Version

caf20353fa87bf14c5fef078e5d8b0692dd7c92d (dev, before PR #2222)

Operating system

Ubuntu 24.04.4 LTS

Provider and model

OpenAI Codex native account; model-independent

Logs or error output

unexpected status 401 Unauthorized: OpenAI account pool has no usable account credential, url: http://127.0.0.1:10100/v1/responses

Screenshots and supporting files

PR #2222 contains focused regression tests for pre-request refresh, one-time 401 recovery, compact routing, refresh-token rotation, and native/stored credential convergence.

Redacted configuration

{
  "native_main": {
    "access_token": "<expired-redacted>",
    "refresh_token": "<valid-redacted>"
  },
  "other_usable_pool_accounts": 0
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. added
    bugSomething isn't working
    account-poolOAuth, credentials, Codex pool, quota, failover, plans
    on Aug 20, 2026
  2. github-actions commented on Aug 20, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  3. Ingwannu commented on Aug 21, 2026

    @Ingwannu
    Owner

    Confirmed against current dev (7881319e7df5cd21276d8fa2fc82d6739e3d23ab): this is still a real native-main authentication gap. src/codex/main-account.ts only reads and checks the stored access token, and src/codex/auth-context.ts materializes that token without a native refresh_token path. The stored account-pool path has refresh handling, but __main__ does not. /v1/responses/compact also has no bounded native-main 401 refresh/replay path.

    PR #2222 is the linked implementation direction, but it is not merge-ready on its current exact head f46deab84ba177a0a32354502f486192545fac64. I independently reviewed it and requested changes for the missing compact 401 replay, lack of coordination/CAS with an external Codex auth.json writer, pool-first persistence before the authoritative native file, stale reclaim-lock handling, unsafe test-home isolation, and the unrelated LEARNED_LESSONS.md addition. It is also Draft, conflicting, hygiene-blocked, and requires exact-head maintainer security review before sponsorship.

    Keeping this issue open as the bug contract. It should close only after a corrected current-dev implementation lands with pre-request refresh plus exactly one 401 replay for both Responses and compact, fail-closed persistence, external-writer race coverage, and green exact-head CI. The owner/Grok review diagnosis was treated as input and independently verified rather than assumed.

  4. lidge-jun commented on Aug 21, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 70 / 80

    지금 dev HEAD 7881319e7에서도 구멍 그대로임. src/codex/main-account.ts:33-51 getMainAccountToken() / isMainAccountTokenLive()가 ~/.codex/auth.json을 읽기만 함. JWT exp 지나면 사용 불가. 살아 있어도 리프레시 토큰은 안 봄. src/codex/auth-context.ts:485-506이 __main__을 main-pool로 고를 때 그 만료 베어러를 그대로 주입함. 풀 계정은 :510 getValidCodexToken()이 src/codex/account-store.ts에서 리프레시함. 메인만 Option A 주석(src/codex/account-usability.ts:35-36) 때문에 빠져 있음. materializeCodexUpstreamAuth (auth-context.ts:595-603)도 같은 읽기 전용 토큰임. 만료 액세스가 /v1/responses랑 /v1/responses/compact로 가서 401 남. 본문 맞음. Ingwannu가 같은 헤드에서 독립 확인함.

    재현도 이제 있음. 예전엔 봇이 재현 없어서 닫았음. 다시 열렸고 커밋 caf20353f + 만료 액세스/유효 리프레시 + 풀 계정 0개 + 401 로그가 박혀 있음. 그 자동 클로즈 다시 하지 말 것. 계약은 이 이슈가 소유자임.

    짝은 #2222임. fix(codex): refresh native main account tokens. draft. 이미 grok-bot 리뷰 있음(62). 랜덤 중복으로 닫지 말 것. 다만 그 헤드는 머지 준비 아님. Ingwannu CHANGES_REQUESTED — compact 401 리플레이 없음, 외부 Codex auth.json 라이터랑 CAS 없음, 풀 퍼시스트가 네이티브 파일보다 앞섬, stale reclaim-lock, 테스트 홈 격리, 무관한 LEARNED_LESSONS.md. hygiene-blocked. 충돌 중. 그 PR을 이 이슈에 맞춰 억지로 열지 말고, 이 이슈가 스펙임. 착지한 구현이 현재 dev에서 사전 리프레시 + Responses/compact 401 한 번 리플레이 + fail-closed 퍼시스트 + 외부 라이터 레이스를 잠그면 그때 이 이슈가 닫힘.

    #2188 L1–L9 사이드카는 이미 dev. x_search 넣지 말 것. Grok OAuth Chat 기본(#2255)/GUI 옵트인 Responses(#2266)랑 다른 레인임. 네이티브 Codex 메인 인증임. types.ts/config.ts 스플릿은 진행 중인데 이 이슈는 스키마 PR이 아님. 프리뷰 배포 아님. v2.28.0 이미 태그됨. 핫 크래시는 아닌데 메인만 있는 오퍼레이터는 프록시가 401로 죽음. 풀은 리프레시하고 메인만 안 함. 그래서 70.

    src/codex/account-store.ts:490-523 풀 리프레시가 이미 grant 핑거프린트랑 락이랑 원자 기록을 갖고 있음. 메인 경로를 그 계약에 수렴시키면 됨. 새 토큰 스토어를 만들지 말 것. auth.json을 쓰기 시작하면 Codex CLI랑 파일 레이스가 생김. 락이 그거 막아야 함. 리프레시만 있고 액세스가 빈 파일도 후보로 넣어야 함. 지금 getMainAccountToken()은 액세스 없으면 null임.

    해결방안: 이 이슈는 계약으로 열어 둬라. 구현은 현재 dev에 리베이스한 #2222 후속(또는 동급). 요청 전에 네이티브 메인 리프레시. Responses랑 compact 둘 다 401이면 한 번만 리플레이. 이미 바디가 나온 뒤엔 리플레이 금지. auth.json 쓰기 실패는 fail-closed. 외부 라이터 CAS. LEARNED_LESSONS.md 넣지 말 것. 스플릿이 main-account.ts/auth-context.ts를 다시 옮기면 리베이스하지 말고 닫고 다시 짜라. 지금은 그 파일 구조 그대로라 그 정도 아님.

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

  5. lidge-jun commented on Aug 25, 2026

    @lidge-jun
    Owner

    Investigated. Not fixed yet, and I want to be precise about why, because the safe-looking half is not actually separable.

    First, a correction to the reported symptom on current dev: an expired native-main bearer is not forwarded upstream. isMainAccountTokenLive() decodes the JWT exp and rejects an expired token (src/codex/main-account.ts:44-50), so the request fails locally rather than producing a 401 from the upstream. I verified that with a scratch regression that made zero network calls.

    So the remaining defect is narrower than the title but still real: native main never refreshes, while pool accounts do through a locked, generation-guarded path (src/codex/account-store.ts:486-530).

    The obvious minimal fix — refresh before upstream I/O, skip the 401-replay machinery — is what I set out to ship, and it does not work in isolation:

    • a successful refresh may rotate the refresh token (src/oauth/chatgpt.ts:146-160)
    • atomicWriteFile replaces the target unconditionally (src/config/atomic-write.ts:95-115); there is no compare-and-swap against an external writer
    • the Codex CLI writes the same auth.json

    So refreshing without a crash-safe, external-writer-aware publication protocol can strand the account: we obtain a rotated grant, another writer clobbers it or we crash mid-publish, and the old refresh token is already spent. That is a worse failure than the one being fixed — the current behavior fails a request; the naive fix can lose the login.

    #2497 proposes a publisher for exactly this, and it is held under security review rather than merged. Three blockers were verified by hand: the rename-before-link window leaves no auth.json on a crash with no startup recovery; a same-account fallback adopts a different pool refresh grant into native-main (account-id equality is not grant ownership — the hazard anthropic-routing.ts fails closed on); and the "exactly one" 401 replay expands to up to nine physical sends through fetchWithTransientRetry. Reusing its publisher would import the first blocker.

    What this needs is a designed and reviewed publication protocol for auth.json, not a smaller patch. The 401 replay and the cross-grant adoption can stay excluded from that scope. Leaving this open rather than shipping something that looks like a fix and can cost someone their credential.

  6. added a commit that references this issue on Aug 26, 2026
  7. MarcTCruz commented on Aug 28, 2026

    @MarcTCruz
    ContributorAuthor

    Status update: PR #2497 is not mergeable in its current form. Current dev is 80e251a4db5ab600ef0a0b44c9d24b326c37d40b; PR head 86a49e852581835253a06e40ebb7012215820757 is 419 commits behind and GitHub reports merge conflicts. The attempted rebase has semantic conflicts on the credential and entitlement path, so resolving it would require reauthoring authentication behavior and a new security review.

    Keep this issue open as the bug contract. PR #2497 remains Draft and unsponsored; it must not close this issue. A successor should be based on current dev, narrowly scoped, and reviewed for safe publication of ~/.codex/auth.json before the issue is closed.

  8. lidge-jun commented on Aug 28, 2026

    @lidge-jun
    Owner

    Fixed on dev by commit 182b356 (PR #2848), reimplementing @MarcTCruz's #2497 on the current tree.

    The native main pool now refreshes an expired auth.json token before the request instead of failing it. Previously src/codex/main-account.ts only read access tokens — there was no refresh or publish path at all.

    The write path got most of the review attention, since it touches the credential file the Codex CLI also owns:

    • The temp file is created with O_CREAT|O_EXCL and its 0600 mode is verified before any secret byte is written (Windows ACLs are applied pre-write too). The first implementation wrote the token first and hardened afterwards, so a pre-existing 0644 temp file stayed world-readable during the write.
    • A byte-hash revalidation immediately before the rename closes the TOCTOU window against a concurrent Codex CLI write. Advisory locking was rejected because an external Codex writer does not honour OpenCodex locks.
    • The authoritative auth.json is never truncated in place, and a refresh failure leaves it byte-identical.
    • Exactly one physical 401 replay, on both the Responses and compact paths.
    • A caller-supplied bearer never triggers a refresh of, or a write to, the file-backed auth.json — that credential is not ours to refresh.

    Verified with tests/codex-main-account-refresh.test.ts and tests/responses-native-main-refresh.test.ts driven RED then GREEN, plus the auth-context, server-auth, and replay-identity suites, tsc, and privacy:scan.

    This PR targeted dev, so GitHub did not auto-close the issue — closing manually.

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

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions