fix(oauth): switch Codex OAuth to ChatGPT device-code flow (desktop + runtime-host) - #1950
Merged
Merged
Conversation
Astro-Han
added a commit
that referenced
this pull request
Aug 3, 2026
…ixes Addresses all 4 P2 findings from the codex review of PR #1950: 1. Desktop duplicated the device-auth protocol with unvalidated token exchange (a malformed 200 with missing refresh_token/expires_in was reported as success and persisted an unparseable credential). The desktop service now reuses @maka/runtime's codex-oauth-enrollment (start/poll/exchange) as the single protocol authority and keeps only product lifecycle: authRequestId/pending state, browser opening, account snapshots, credential persistence. openai-codex-helpers.ts keeps only JWT claims + experimental gate (endpoints live once in runtime contracts). 2. Both polls could issue a request after the device window elapsed and accept a late grant. codex-oauth-enrollment polls first, sleeps at most until expires_at, and re-checks on every loop; a 200 issued in-window is honored even if it arrives just past the boundary. 3. Codex poll passed the caller's AbortSignal into the admitted request, so a cancellation could discard a poll that consumed the one-time code. The admitted request now uses an independent signal (matching xAI), bounded by its own deadline. 4. A local window expiry was encoded as invalid_grant and surfaced as provider_rejected (coordinator), mislabeling a user timeout as a provider rejection; my own coordinator test had pinned the wrong mapping. New OAuthDeviceAuthorizationExpiredError is thrown by both codex and xAI polls on local expiry; the coordinator maps it to authorization_failed. oauth-login.ts codex loopback branches stay as a legacy public-library API, now marked LEGACY. Tests added: desktop malformed-200 no-write, expiry-stops-polling, and credential-write-failure; runtime expiry classification, no-fetch-after- expiry, and admission-cancel races (codex + xAI); coordinator codex cancellation-deferral. Desktop 1399, runtime 2712, runtime-host 568.
Replace the localhost:1455 loopback authorization with the official
codex CLI device-auth flow (auth.openai.com/api/accounts/deviceauth/*):
- getAuthorizationUrl POSTs {client_id} to /deviceauth/usercode and
surfaces the one-time user_code as stateHint.
- openAuthorizationUrl opens auth.openai.com/codex/device and starts
polling /deviceauth/token (403/404 = pending, 15 min TTL).
- completeAuthorization exchanges the returned authorization code at
/oauth/token with redirect_uri=auth.openai.com/deviceauth/callback.
Removes the loopback listener, PKCE authorize URL builder, and fixed
port 1455, eliminating the IPv4/IPv6 loopback mismatch, EADDRINUSE on
re-entry, wrong-state callback hangs, and port leaks. Refresh,
persistence, and JWT claim extraction are unchanged; requests still go
through proxiedFetch so Maka's proxy settings apply (verified the
deviceauth endpoints accept undici traffic).
Verified against the live endpoints (usercode returns 200, token poll
returns deviceauth_authorization_pending), 1397 desktop main tests,
renderer/main typecheck, and global lint.
Replace the localhost:1455 loopback listener in the Host OAuth coordinator with the official codex CLI device-auth flow, closing the same IPv4/IPv6 mismatch, port collision, and state-hang failures the desktop service had. - runtime: add codex-oauth-enrollment (startCodexDeviceAuthorization / pollCodexDeviceAuthorization / exchangeCodexDeviceAuthorizationCode) mirroring the xAI enrollment seam, and extend the openai-codex provider contract with deviceAuthBaseUrl / deviceVerifyUrl / deviceRedirectUri. - coordinator: route openai-codex through #runCodexDeviceLogin (present verification URL + user code as stateHint, poll until approval or expiry, exchange the grant); delete the loopback server, callback constants, and deadline helpers. #runAuthorizationCodeLogin is now claude-subscription only. 567 runtime-host tests, 2709 runtime tests, 1397 desktop tests, lint and typecheck pass.
…ixes Addresses all 4 P2 findings from the codex review of PR #1950: 1. Desktop duplicated the device-auth protocol with unvalidated token exchange (a malformed 200 with missing refresh_token/expires_in was reported as success and persisted an unparseable credential). The desktop service now reuses @maka/runtime's codex-oauth-enrollment (start/poll/exchange) as the single protocol authority and keeps only product lifecycle: authRequestId/pending state, browser opening, account snapshots, credential persistence. openai-codex-helpers.ts keeps only JWT claims + experimental gate (endpoints live once in runtime contracts). 2. Both polls could issue a request after the device window elapsed and accept a late grant. codex-oauth-enrollment polls first, sleeps at most until expires_at, and re-checks on every loop; a 200 issued in-window is honored even if it arrives just past the boundary. 3. Codex poll passed the caller's AbortSignal into the admitted request, so a cancellation could discard a poll that consumed the one-time code. The admitted request now uses an independent signal (matching xAI), bounded by its own deadline. 4. A local window expiry was encoded as invalid_grant and surfaced as provider_rejected (coordinator), mislabeling a user timeout as a provider rejection; my own coordinator test had pinned the wrong mapping. New OAuthDeviceAuthorizationExpiredError is thrown by both codex and xAI polls on local expiry; the coordinator maps it to authorization_failed. oauth-login.ts codex loopback branches stay as a legacy public-library API, now marked LEGACY. Tests added: desktop malformed-200 no-write, expiry-stops-polling, and credential-write-failure; runtime expiry classification, no-fetch-after- expiry, and admission-cancel races (codex + xAI); coordinator codex cancellation-deferral. Desktop 1399, runtime 2712, runtime-host 568.
Astro-Han
force-pushed
the
fix/desktop-codex-oauth-device-code
branch
from
August 3, 2026 07:18
ab130df to
2c9423d
Compare
Astro-Han
added a commit
that referenced
this pull request
Aug 3, 2026
Round-2 codex review of PR #1950 found 3 P2 + 1 P3 (verdict FAIL) plus a deletion/simplification list. All addressed: P2 fixes: - Desktop: the poll had already consumed the one-time device code when a cancellation arrived; the exchange reused the aborted signal and burned the code. Exchange now uses an independent signal so exchange + persistence complete even after cancel (deferred-poll test added). - xAI device poll (runtime-host path) could sleep past the window and still issue a request, and mapped local expiry to provider_rejected. Symmetric to the Codex poll: sleep min(interval, remaining), re-check after sleep, and throw OAuthDeviceAuthorizationExpiredError on both local expiry and server-side expired_token. - Codex enrollment now accepts the official CLI's alias in addition to (serde alias parity, test added). P3 fix: - After rebasing onto #1946 (which moved desktop xAI to a PKCE loopback), the renderer mislabeled xAI's random state prefix as a sign-in code. Codex shows the device code; xAI shows no code and its copy describes automatic browser write-back. Deletion/simplification (per review): - Removed the entire Codex loopback public path from oauth-login.ts (OAuthLoginProvider no longer includes openai-codex; buildCodex- AuthorizationUrl/CodexAuthorizationConfig deleted in favor of a private generic loopback builder; provider-config and contracts pruned of codex loopback fields; codex loopback tests deleted). Codex initial-token decoding stays via OAuthInitialTokenProvider. - Deleted apps/desktop/.../openai-codex-helpers.ts: JWT claim extraction merged into runtime subscription-auth.ts (extractCodexAccountClaims), with extractCodexAccountId kept sub-free for header routing per #1946; experimental gate now delegates to runtime isOAuthEnrollmentProvider- Enabled('openai-codex'); claims tests migrated to runtime. - Desktop service now persists runtime OAuthSubscriptionTokens directly (PersistedTokens removed). - Desktop tests tightened: protocol assertions (poll body, exchange form) and the mid-sleep poll-count assertion are owned by the runtime enrollment tests; desktop keeps lifecycle/UI-reason assertions. - Renamed stale "browser-loopback" wording in renderer + e2e fixture. Desktop 1403, runtime 2731, runtime-host 570; lint, format, typecheck, knip clean.
Round-2 codex review of PR #1950 found 3 P2 + 1 P3 (verdict FAIL) plus a deletion/simplification list. All addressed: P2 fixes: - Desktop: the poll had already consumed the one-time device code when a cancellation arrived; the exchange reused the aborted signal and burned the code. Exchange now uses an independent signal so exchange + persistence complete even after cancel (deferred-poll test added). - xAI device poll (runtime-host path) could sleep past the window and still issue a request, and mapped local expiry to provider_rejected. Symmetric to the Codex poll: sleep min(interval, remaining), re-check after sleep, and throw OAuthDeviceAuthorizationExpiredError on both local expiry and server-side expired_token. - Codex enrollment now accepts the official CLI's usercode alias in addition to user_code (serde alias parity, test added). P3 fix: - After rebasing onto #1946 (which moved desktop xAI to a PKCE loopback), the renderer mislabeled xAI's random state prefix as a sign-in code. Codex shows the device code; xAI shows no code and its copy describes automatic browser write-back. Deletion/simplification (per review): - Removed the entire Codex loopback public path from oauth-login.ts (OAuthLoginProvider no longer includes openai-codex; the codex-specific authorization builder and config types were deleted in favor of a private generic loopback builder; provider config and contracts pruned of codex loopback fields; codex loopback tests deleted). Codex initial-token decoding stays via OAuthInitialTokenProvider. - Deleted apps/desktop/src/main/oauth/openai-codex-helpers.ts: JWT claim extraction merged into runtime subscription-auth.ts (extractCodexAccountClaims), with extractCodexAccountId kept sub-free for header routing per #1946; the experimental gate now delegates to runtime isOAuthEnrollmentProviderEnabled('openai-codex'); claims tests migrated to runtime. - Desktop service now persists runtime OAuthSubscriptionTokens directly (PersistedTokens removed). - Desktop tests tightened: protocol assertions (poll body, exchange form) and the mid-sleep poll-count assertion are owned by the runtime enrollment tests; desktop keeps lifecycle/UI-reason assertions. - Renamed stale "browser-loopback" wording in renderer + e2e fixture. Desktop 1403, runtime 2731, runtime-host 570; lint, format, typecheck, knip clean.
Astro-Han
force-pushed
the
fix/desktop-codex-oauth-device-code
branch
from
August 3, 2026 07:50
5952871 to
4cb340e
Compare
Astro-Han
marked this pull request as ready for review
August 3, 2026 07:59
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.
背景
Maka 的 OpenAI Codex OAuth 登录不稳定,根因是
localhost:1455环回授权流程的 4 个缺陷(已用真实服务类复现):localhost,但回调服务器只监听127.0.0.1;macOS 上localhost优先解析为::1,Safari/部分浏览器直接连不上("时好时坏"主因)。EADDRINUSE失败。completeAuthorization永不返回,渲染层转圈卡死。官方 codex CLI 0.144.6(headless toolchain 钉的同一版本、同一 client_id)已改用 ChatGPT device-code 流程(
codex-rs login/src/device_code_auth.rs),环回是历史路径。本 PR 跟随上游,把 Maka 的两条 Codex 登录路径(桌面 service + runtime-host coordinator)都换成 device-code,环回机制整个删除。端点(官方协议,已实测)
POST /api/accounts/deviceauth/usercode{"client_id"}→{device_auth_id, user_code, interval, expires_at}(验证页固定auth.openai.com/codex/device)POST /api/accounts/deviceauth/token(403/404 = pending,200 ={authorization_code, code_challenge, code_verifier})POST /oauth/token交换(redirect_uri=auth.openai.com/deviceauth/callback)已用 Electron/undici 实测:usercode 返回 200、token 轮询返回
deviceauth_authorization_pending;deviceauth 端点对 undici 无 Cloudflare 挑战,因此可继续走 Maka 的proxiedFetch(代理兼容)。改动
commit 1 — 桌面 (
063ce461c)openai-codex-service.ts:授权重写为 device-auth(usercode → 轮询 → code 交换);删除 loopback server、端口 1455、PKCE URL 构建、callback HTML;刷新/持久化/claim 提取不变openai-codex-helpers.ts:端点常量换成 device-authcommit 2 — runtime-host (
5f8a6c8ab)codex-oauth-enrollment.ts(runtime):startCodexDeviceAuthorization/pollCodexDeviceAuthorization/exchangeCodexDeviceAuthorizationCode,镜像 xAI enrollment 接缝oauth-provider-contracts.ts:openai-codex 加deviceAuthBaseUrl/deviceVerifyUrl/deviceRedirectUrioauth-coordinator.ts:openai-codex 走#runCodexDeviceLogin(present 验证页 + stateHint=user_code → poll → exchange);删除 loopback server 与全套死代码(−214 行);#runAuthorizationCodeLogin收窄为 claude-only验证
1455//auth/callback生产代码零残留(cursor/antigravity 是独立 provider 的 loopback,不在本 PR 范围)有意保留
buildOAuthLoginAuthorization/exchangeOAuthAuthorizationCode的 codex 分支保留(公开 API + 测试钉住,无 Maka 生产调用者)