Repository navigation
fix(auth): align OAuth refresh policy with Codex - #185
Open
squarepots wants to merge 37 commits into
Open
squarepots wants to merge 37 commits into
squarepots wants to merge 37 commits into
Conversation
squarepots
force-pushed
the
fix/oauth-refresh-policy
branch
from
September 21, 2026 19:04
54fcfeb to
7aa083b
Compare
This was referenced Sep 21, 2026
This was referenced Sep 30, 2026
This was referenced Oct 28, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
依赖 #182,请先审阅该 PR。
本 PR 让 OAuth token refresh 与当前 Codex 的判断规则一致,并让 desktop 与
codex-web共享同一条跨进程 refresh 路径。主动 refresh 算法:
exp可以解析,只有在它已过期或将在 5 分钟内过期时 refresh。这个分支优先级高于 refresh age;新鲜 access token 不会因为旧的last_refresh或过期的 ID token 而 refresh。exp缺失或无法解析,回退到last_refresh,且只有超过 8 天才 refresh。auth.json.last_refresh;inactive profile 使用StoredAccount.last_refresh_at。该可选字段兼容旧数据,并且只在凭据实际创建、refresh 或从可信auth.jsonreconciliation 时更新;不会使用last_used_at,普通账号切换也不会伪造 refresh age。没有可信 refresh age 时不做主动 refresh。凭据 generation 的 reconciliation:
auth.json仍决定 active runtime identity,但其 token payload 只有在auth.json.last_refresh不早于StoredAccount.last_refresh_at时才能写回 profile store。已知的新 generation 不会被旧的或没有 generation 的 runtime snapshot 覆盖。refresh 在同一 account-store mutation 中持久化 token 和 generation;因此 rotated refresh token 即使在 ID token 无效或auth.json写入失败后,也能在下一次 reconciliation 保持可恢复。较新的 runtime generation 仍会正常投影到 active profile。收到 401 时如何处理? provider 401 可以触发一次受保护的 refresh 和一次 retry。操作会先获取
auth-operation.lock,然后重新读取当前凭据;如果另一个进程已经换入新的 access token,就直接复用它。403 和其他状态如何处理? 403、429 及其他非 401 响应不会触发 refresh,也不会消耗 rotating refresh token。
refresh、401 retry、账号切换和 OAuth 完成都使用同一个跨进程
auth-operation.lock。usage、warm-up、账号 metadata、profile usage 和 reset credits 使用同一套 401/retry 规则。refresh 响应缺少
id_token时保留 durable token 并返回错误;缺少refresh_token时保留旧 refresh token。验证
cargo test --locked rotated_refresh_token_survives_stale_runtime_reconciliation:通过cargo test --locked newer_runtime_generation_projects_tokens_to_the_active_store:通过rustfmt --edition 2021 --checkcargo check --manifest-path src-tauri/Cargo.toml --locked:通过cargo test --manifest-path src-tauri/Cargo.toml --locked:65 个 Rust 测试通过git diff --check:通过Closes #136
Depends on #182. Please review that PR first.
This PR aligns OAuth token refresh with the current Codex decision rules and gives desktop and
codex-webone shared cross-process refresh path.Proactive refresh algorithm:
exp, refresh only when it is expired or within five minutes of expiry. This branch takes precedence over refresh age: a fresh access token is not refreshed becauselast_refreshis old or the ID token is expired.expis missing or unparseable, fall back tolast_refreshand refresh only after eight days.auth.json.last_refresh; inactive profiles useStoredAccount.last_refresh_at. This optional field remains backward-compatible and changes only when credentials are created, refreshed, or reconciled from trustworthyauth.json; it never useslast_used_at, and ordinary account switches do not fabricate refresh age. No trustworthy refresh age means no proactive refresh.Credential-generation reconciliation:
auth.jsoncontinues to determine the active runtime identity, but its token payload can write back to the profile store only whenauth.json.last_refreshis not older thanStoredAccount.last_refresh_at. A known newer generation cannot be replaced by an older or unversioned runtime snapshot. Refresh persists the tokens and their generation in the same account-store mutation, so a rotated refresh token remains recoverable on the next reconciliation even when the ID token is invalid or writingauth.jsonfails. A newer runtime generation still projects normally to the active profile.What happens after a 401? A provider 401 may trigger one guarded refresh and one retry. The operation acquires
auth-operation.lockand reloads current credentials first; if another process has already installed a new access token, it reuses it.What happens after 403 and other statuses? A 403, 429, or other non-401 response never triggers refresh and never consumes a rotating refresh token.
Refresh, the 401 retry, account switching, and OAuth completion use the same cross-process
auth-operation.lock. Usage, warm-up, account metadata, profile usage, and reset credits share the same 401/retry policy.When a refresh response omits
id_token, durable tokens are preserved and the operation returns an error. When it omitsrefresh_token, the existing refresh token is retained.Validation
cargo test --locked rotated_refresh_token_survives_stale_runtime_reconciliation: passedcargo test --locked newer_runtime_generation_projects_tokens_to_the_active_store: passedrustfmt --edition 2021 --checkcargo check --manifest-path src-tauri/Cargo.toml --locked: passedcargo test --manifest-path src-tauri/Cargo.toml --locked: 65 Rust tests passedgit diff --check: passedCloses #136
Restores #172 after the source fork was accidentally deleted.
Maintenance triage — 2026-09-30
Prerequisite PRs: #182.
#136 also asks for human-readable authentication errors, which were implemented in the older #137. The refresh-policy work alone must not silently discard that UX acceptance concern. Preserve the error mapping or split a tracked follow-up before considering all of #136 complete.