Skip to content

fix(auth): keep fresher Codex re-auth state#53611

Merged
steipete merged 1 commit intoopenclaw:mainfrom
giulio-leone:fix/codex-reauth-overwrite-53466
Mar 24, 2026
Merged

fix(auth): keep fresher Codex re-auth state#53611
steipete merged 1 commit intoopenclaw:mainfrom
giulio-leone:fix/codex-reauth-overwrite-53466

Conversation

@giulio-leone
Copy link
Copy Markdown
Contributor

Summary

  • invalidate cached Codex CLI credentials as soon as ~/.codex/auth.json changes, even inside the normal TTL window
  • skip external CLI sync when the stored openai-codex:default OAuth credential is already newer than the imported one
  • cover both stale-cache and stale-overwrite paths with focused regression tests

Fixes #53466.

Root cause

readCodexCliCredentialsCached() only honored a time-based TTL, so a stale external Codex credential could survive in memory after auth.json changed.

Later, syncExternalCliCredentials() would blindly replace the stored openai-codex:default credential whenever the imported OAuth payload differed, even if the stored credential had a later expiry from a fresh openclaw models auth login re-auth flow.

What changed

  • src/agents/cli-credentials.ts
    • track Codex auth.json mtime alongside the cache entry
    • invalidate the cached entry immediately when the file mtime changes
    • only store a new cached entry when the file mtime stayed stable across the read
  • src/agents/auth-profiles/external-cli-sync.ts
    • keep the fresher stored OAuth credential when the imported external Codex credential is older
  • targeted regression coverage for both behaviors

Validation

Focused suite, run twice consecutively on the final code state:

pnpm exec vitest --run \
  src/agents/cli-credentials.test.ts \
  src/agents/auth-profiles.external-cli-sync.test.ts \
  src/agents/auth-profiles.store-cache.test.ts \
  src/agents/auth-profiles.markauthprofilefailure.test.ts \
  src/agents/auth-profiles/oauth.openai-codex-refresh-fallback.test.ts

Both consecutive runs passed: 27/27 tests each time.

Live proof 1 (changed auth.json invalidates the stale cached Codex credential inside the TTL window):

{
  "cacheProof": true,
  "firstRefresh": "stale-external-refresh",
  "secondRefresh": "fresh-external-refresh"
}

Live proof 2 (a warmed stale external Codex credential no longer overwrites a fresher stored openai-codex:default credential on the real auth-store load path):

{
  "overwriteProof": true,
  "finalRefresh": "fresh-store-refresh"
}

The repo commit hook also completed successfully and committed the change after running the project pnpm check stack.

@giulio-leone giulio-leone requested a review from a team as a code owner March 24, 2026 10:16
@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Mar 24, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 24, 2026

Greptile Summary

This PR fixes two related auth-state bugs in the Codex CLI credential pipeline: a stale in-memory cache surviving a ~/.codex/auth.json change within the TTL window, and a blind overwrite of a fresher stored openai-codex:default credential by an older external CLI import.

  • cli-credentials.ts: sourceMtimeMs is now tracked alongside each codexCliCache entry. A pre-read stat and a post-read stat bracket readCodexCliCredentials(); if the two mtimes differ the cache is cleared rather than populated, preventing a torn-read from being persisted.
  • external-cli-sync.ts: hasNewerStoredOAuthCredential is inserted before the overwrite in syncExternalCliCredentialsForProvider. It preserves the stored credential when its expires is finite and later than the incoming one. Note that this guard applies to all providers routed through syncExternalCliCredentialsForProvider (Qwen, MiniMax, and Codex), not only Codex as the PR description implies — this broader scope appears intentional and correct.
  • Both new behaviors are covered by focused regression tests that pass cleanly.

Confidence Score: 5/5

  • This PR is safe to merge; it fixes well-understood caching and overwrite bugs with targeted, well-tested logic.
  • The root causes are clearly identified and each fix is minimal and correctly scoped. The double-stat strategy handles the write-during-read race correctly, the freshness guard handles all expiry edge cases (non-finite, equal, greater), and the new tests directly exercise both failure paths. No regressions identified.
  • No files require special attention.

Reviews (1): Last reviewed commit: "fix(auth): protect fresher codex reauth ..." | Re-trigger Greptile

Copy link
Copy Markdown

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

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: 2075d9de2e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

if (options.log !== false) {
log.debug(`kept newer stored ${provider} credentials over external cli sync`, {
profileId,
storedExpires: new Date(existingOAuth!.expires).toISOString(),
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 Avoid throwing when logging newer stored expiry

If auth-profiles.json contains an out-of-range numeric expires value (which is currently accepted during store coercion), this debug branch can throw RangeError: Invalid time value at runtime because new Date(existingOAuth!.expires).toISOString() is not guarded. That exception aborts external CLI sync (and therefore auth-store load paths that call it) instead of safely keeping or replacing credentials. Please guard storedExpires the same way as externalExpires (or log the raw number) before calling toISOString().

Useful? React with 👍 / 👎.

- invalidate cached Codex CLI credentials when auth.json changes within the TTL window
- skip external CLI sync when the stored Codex OAuth credential is newer
- cover both behaviors with focused regression tests

Refs #53466

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@steipete steipete force-pushed the fix/codex-reauth-overwrite-53466 branch from 2075d9d to 7689d1d Compare March 24, 2026 16:45
Copy link
Copy Markdown

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

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: 7689d1dfa0

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

if (shallowEqualOAuthCredentials(existingOAuth, creds)) {
return false;
}
if (hasNewerStoredOAuthCredential(existingOAuth, creds)) {
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 Scope newer-token skip logic to Codex only

The new hasNewerStoredOAuthCredential short-circuit runs in the shared syncExternalCliCredentialsForProvider path, so it now changes behavior for Qwen and MiniMax as well as Codex. That means external sync can silently stop updating those profiles whenever the stored expires is higher than the incoming one, even if access/refresh tokens differ (for example after a CLI re-auth/account switch with a shorter lifetime), leaving stale credentials in auth-profiles.json instead of mirroring the external CLI state.

Useful? React with 👍 / 👎.

@steipete steipete merged commit bbe6f7f into openclaw:main Mar 24, 2026
37 checks passed
@steipete
Copy link
Copy Markdown
Contributor

Landed via temp rebase onto main.

  • Gate: reused green GitHub CI on rebased head; no extra local broad gate per operator request
  • Land commit: 7689d1d
  • Merge commit: bbe6f7f

Thanks @giulio-leone!

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

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Expired external Codex credentials overwrite fresh re-auth and cause repeated OAuth failures

2 participants