feat(tui): native ChatGPT PKCE sign-in for openai-codex (#5778) - #5784
Conversation
Add a Codewhale-owned Sign in with ChatGPT adapter so the openai-codex route no longer requires Codex CLI. Browser PKCE uses the published auth.openai.com issuer, stores rotating refresh tokens in the existing credentials directory, and keeps external CLI import as an explicit alternative. Device-code is omitted because the issuer does not advertise device_authorization_endpoint. Signed-off-by: CodeWhale Bot <bot@codewhale.net>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Reviewing PR #5784 — native ChatGPT PKCE sign-in for openai-codex
Todo list
|
|
Claude finished @Hmbown's task in 3m 42s —— View job Reviewing PR #5784 — native ChatGPT PKCE sign-in for openai-codexTodo list
SummarySolid, well-scoped adapter that closely mirrors the existing xAI device-login pattern (shared Findings1. Blocking network I/O runs synchronously inside the async TUI event loop — 2. Doc claims OIDC-discovery-based revoke, but the endpoint is hardcoded — 3. Minor: unused localized message — Good choices worth calling out
|
There was a problem hiding this comment.
Codewhale review
Adds native Sign in with ChatGPT PKCE loopback flow for openai-codex, including config storage, CLI/TUI wiring, localization, tests, and docs. Overall solid but has several correctness concerns around loopback host resolution, hardcoded revocation endpoint versus the discovery claim, and opaque token expiry handling.
Findings
- [WARNING] Loopback callback uses localhost redirect while listener binds 127.0.0.1 (
crates/tui/src/chatgpt_oauth.rs)
bind_loopback_callback() binds only to 127.0.0.1, but redirect_uri_for_port() returns http://localhost:/auth/callback. On systems where localhost resolves to ::1 first (common on Linux with IPv6 enabled), the browser may connect to ::1 and be refused, breaking PKCE login. Bind to localhost (both v4/v6) or align the redirect URI to the bound address, and verify against OpenAI's registered redirect URIs. - [WARNING] Revocation endpoint hardcoded, not read from OIDC discovery (
crates/tui/src/chatgpt_oauth.rs)
PR claims to use the discovery revocation_endpoint, but revoke_endpoint() hardcodes /api/accounts/oauth/revoke and pkce_login() never fetches https://auth.openai.com/.well-known/openid-configuration. If OpenAI rotates endpoints, revoke will fail while the rest of the flow may still work. Fetch and use the advertised revocation_endpoint, or update docs to say it is hardcoded. - [INFO] select_entry prefers built-in client id even when custom client id is configured (
crates/tui/src/chatgpt_oauth.rs)
select_entry always prefers keys ending with ::app_EMo... (CHATGPT_OAUTH_CLIENT_ID). When CODEWHALE_CHATGPT_OAUTH_CLIENT_ID is set to a different client id, the current entry won't be preferred, and fallback may pick an older entry. Consider deriving the preferred suffix from the entry/config client id. - [WARNING] Opaque access tokens without expires_in are treated as expired and can fail closed incorrectly (
crates/tui/src/chatgpt_oauth.rs)
entry_access_token_is_fresh returns false when neither expires_at nor a JWT exp is present. For valid opaque tokens issued without expires_in (or with no JWT), get_owned_credentials_locked will attempt refresh and, if no refresh token is stored, error 'access token expired and no refresh_token is stored' even though the access token may be valid. Consider treating missing expiry as fresh until a 401 indicates otherwise, or require and document that the issuer always returns expires_in. - [WARNING] Local credential file removal errors are silently ignored during revoke (
crates/tui/src/chatgpt_oauth.rs)
In revoke_owned_login_locked, store.remove(&name) useslet _ = store.remove(&name);, so a failed deletion (permissions, I/O) leaves the token file on disk while the command reports success and the config pointer is unset. The user may believe credentials are revoked. Propagate or at least log the error. - [INFO] Logout clears only valid generation pointer, not legacy chatgpt pointer (
crates/cli/src/lib.rs)
run_logout_command_with_secrets_unlocked sets oauth_credential_generation to None only if is_valid_chatgpt_oauth_generation. If the config pointer somehow holds the legacy name chatgpt-oauth.json, logout will not clear it, then clear_all_chatgpt_oauth_credentials deletes that file, leaving a dangling config pointer. Use is_valid_chatgpt_oauth_generation or also check LEGACY_CHATGPT_OAUTH_FILE_NAME.
Suggestions
crates/tui/src/chatgpt_oauth.rs— Bind the callback listener tolocalhostas well as 127.0.0.1, or change redirect_uri_for_port to usehttp://127.0.0.1:<port>so the browser connects to the bound interface. Verify that the chosen URI is among the public client's registered redirect URIs.crates/tui/src/chatgpt_oauth.rs— Fetch the OIDC discovery document on first use and cache the advertised revocation_endpoint instead of hardcoding it, so remote revoke stays aligned with issuer configuration. If hardcoding is intentional, update docs and PR text accordingly.crates/tui/src/chatgpt_oauth.rs— Replacelet _ = store.remove(&name);with logging or propagate the error so users are not misled when local credential deletion fails.
Assessment
The PR is well-structured with good tests and documentation, but a few correctness issues should be addressed before merge: the localhost/IPv4 callback mismatch can break login on many systems, revocation endpoint is hardcoded despite the discovery claim, and opaque-token expiry handling may fail closed incorrectly. The code otherwise reuses patterns well and keeps token material redacted.
Advisory review by Codewhale (codewhale review --pr 5784 --post, head c65d0ea1bacfa54d0dc419985d082dadc3a2c9b8). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
The redirect URI has to keep saying `localhost` -- that exact string is what the public Codex client is registered against and redirect matching is exact -- but `localhost` resolves to `::1` as readily as to `127.0.0.1`, and an IPv6-first browser is free to prefer it. Binding only `127.0.0.1` sent that browser to a closed port: Happy Eyeballs turns a working sign-in into a slow one, and where that fallback is off, into a hang until the callback timeout. So bind both families on the chosen port and poll both; a host with only one stack binds that one and still signs in. The redirect URI is untouched. The module doc also claimed revoke used the discovery `revocation_endpoint` while revoke_endpoint() hardcodes `/api/accounts/oauth/revoke`. The hardcoded path is deliberate -- revoke has to clear local credentials even when the issuer is unreachable, and a discovery fetch would only add a failure mode to a cleanup path -- so the doc now says that instead of describing code that was never written. New test drives a real callback into the IPv6 listener with an idle IPv4 listener ahead of it in the poll order, so it fails if either the bind or the polling loop regresses to one family: cargo test -p codewhale-tui --lib chatgpt_oauth test result: ok. 11 passed; 0 failed; 0 ignored; 0 measured; 11659 filtered out Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_643a570a-e2d4-4e3d-adfc-32c4f47e765d) |
There was a problem hiding this comment.
🟡 Changes recommended
There are confirmed security/correctness issues in the loopback callback binding logic and /auth chatgpt-revoke config targeting that should be fixed before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces a native Sign in with ChatGPT flow for the first-class openai-codex route in the TUI/CLI, implementing an authorization-code PKCE (S256) browser login with a localhost callback and storing refreshable tokens in Codewhale-owned credentials storage, while keeping the Codex CLI import path as an explicit alternative.
Changes:
- Add a new
chatgpt_oauthadapter implementing PKCE loopback login, token persistence, refresh, and best-effort revoke foropenai-codex. - Wire new auth entrypoints into CLI + TUI (
codewhale auth chatgpt,/auth chatgpt, provider setup picker flow) and update credential resolution precedence. - Update docs, changelogs, provider guidance, and TUI locale packs to reflect the new ChatGPT subscription billing/auth path.
File summaries
| File | Description |
|---|---|
| docs/PROVIDERS.md | Documents native ChatGPT PKCE sign-in and clarifies billing-owner distinctions for openai-codex. |
| docs/CONFIGURATION.md | Updates credential precedence text for openai-codex to include Codewhale-owned ChatGPT tokens. |
| crates/tui/src/tui/views/mod.rs | Adds a new view event for ChatGPT PKCE sign-in requests from the provider picker. |
| crates/tui/src/tui/ui/handlers.rs | Handles the new provider picker ChatGPT PKCE event and triggers the TUI login flow. |
| crates/tui/src/tui/ui/event_loop.rs | Implements run_chatgpt_pkce_login_from_tui with terminal pause/resume around the login flow. |
| crates/tui/src/tui/ui/apply.rs | Adds an AppAction for starting ChatGPT PKCE login and applies activation + provider switching. |
| crates/tui/src/tui/setup/mod.rs | Updates setup guidance messaging for openai-codex to prefer ChatGPT sign-in. |
| crates/tui/src/tui/provider_picker.rs | Adds a ChatGPT/Codex auth-choice stage (ChatGPT PKCE vs Codex CLI import) plus tests. |
| crates/tui/src/tui/app/types.rs | Adds StartChatgptPkceLogin AppAction. |
| crates/tui/src/route_runtime.rs | Updates missing-auth “next step” guidance for openai-codex to prefer ChatGPT sign-in. |
| crates/tui/src/oauth.rs | Routes the openai-codex missing-auth message through the new chatgpt_oauth messaging. |
| crates/tui/src/localization.rs | Adds new MessageIds for the ChatGPT auth-choice UI strings. |
| crates/tui/src/lib.rs | Adds auth chatgpt / auth chatgpt-revoke CLI subcommands in the TUI binary. |
| crates/tui/src/config/credential_resolve.rs | Surfaces “Codewhale-owned ChatGPT sign-in” as an OAuth credential probe for openai-codex. |
| crates/tui/src/config.rs | Adds config helpers and codex_credentials() resolution support for owned ChatGPT OAuth credentials. |
| crates/tui/src/commands/mod.rs | Adds a test ensuring /auth chatgpt starts the login action. |
| crates/tui/src/commands/groups/core/core.rs | Updates provider help output tests to include codewhale auth chatgpt. |
| crates/tui/src/commands/groups/config/mod.rs | Adds /auth chatgpt and /auth chatgpt-revoke command routing. |
| crates/tui/src/chatgpt_oauth.rs | New module implementing ChatGPT/Codex subscription PKCE loopback login, storage, refresh, and revoke. |
| crates/tui/locales/en.json | Adds localized strings for ChatGPT auth-choice UI and actions (EN). |
| crates/tui/locales/de.json | Adds localized strings for ChatGPT auth-choice UI and actions (DE). |
| crates/tui/locales/ca.json | Adds localized strings for ChatGPT auth-choice UI and actions (CA). |
| crates/tui/locales/es-419.json | Adds localized strings for ChatGPT auth-choice UI and actions (es-419). |
| crates/tui/locales/fr.json | Adds localized strings for ChatGPT auth-choice UI and actions (FR). |
| crates/tui/locales/hi.json | Adds localized strings for ChatGPT auth-choice UI and actions (HI). |
| crates/tui/locales/id.json | Adds localized strings for ChatGPT auth-choice UI and actions (ID). |
| crates/tui/locales/ja.json | Adds localized strings for ChatGPT auth-choice UI and actions (JA). |
| crates/tui/locales/ko.json | Adds localized strings for ChatGPT auth-choice UI and actions (KO). |
| crates/tui/locales/pt-BR.json | Adds localized strings for ChatGPT auth-choice UI and actions (pt-BR). |
| crates/tui/locales/ru.json | Adds localized strings for ChatGPT auth-choice UI and actions (RU). |
| crates/tui/locales/uk.json | Adds localized strings for ChatGPT auth-choice UI and actions (UK). |
| crates/tui/locales/vi.json | Adds localized strings for ChatGPT auth-choice UI and actions (VI). |
| crates/tui/locales/zh-Hans.json | Adds localized strings for ChatGPT auth-choice UI and actions (zh-Hans). |
| crates/tui/locales/zh-Hant.json | Adds localized strings for ChatGPT auth-choice UI and actions (zh-Hant). |
| crates/tui/CHANGELOG.md | Notes the new native ChatGPT sign-in option for openai-codex. |
| crates/config/src/xai_credentials.rs | Extends owned-credential store infrastructure to support ChatGPT OAuth generations and cleanup. |
| crates/config/src/provider.rs | Updates openai-codex credential help guidance to prefer ChatGPT sign-in. |
| crates/config/src/lib.rs | Re-exports new ChatGPT OAuth credential store helpers and validation functions. |
| crates/cli/src/lib.rs | Adds codewhale auth chatgpt / chatgpt-revoke, updates logout cleanup and auth status output. |
| CHANGELOG.md | Notes the new native ChatGPT sign-in option for openai-codex. |
Review details
- Files reviewed: 40/40 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| let mut last_error = None; | ||
| for port in CHATGPT_OAUTH_LOOPBACK_PORTS { | ||
| let mut bound = Vec::new(); | ||
| for addr in [ | ||
| SocketAddr::from((Ipv4Addr::LOCALHOST, port)), | ||
| SocketAddr::from((Ipv6Addr::LOCALHOST, port)), | ||
| ] { | ||
| match TcpListener::bind(addr) { | ||
| Ok(listener) => { | ||
| listener | ||
| .set_nonblocking(true) | ||
| .context("ChatGPT OAuth callback listener could not be set non-blocking")?; | ||
| bound.push(listener); | ||
| } | ||
| Err(error) => last_error = Some(error), | ||
| } | ||
| } | ||
| if !bound.is_empty() { | ||
| return Ok(bound); | ||
| } | ||
| } |
| Some("chatgpt-revoke") | Some("chatgpt_revoke") => { | ||
| match crate::chatgpt_oauth::revoke_owned_login(None, None) { | ||
| Ok(()) => CommandResult::message( |
| Some("chatgpt-revoke") | Some("chatgpt_revoke") => { | ||
| match crate::chatgpt_oauth::revoke_owned_login(None, None) { | ||
| Ok(()) => CommandResult::message( | ||
| "Revoked Codewhale-owned ChatGPT tokens. Codex CLI consent is unchanged.", | ||
| ), |
…kce-5778-20260831 # Conflicts: # CHANGELOG.md # crates/tui/CHANGELOG.md # crates/tui/src/config.rs
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_9d2d4fe3-7469-4ffd-8566-e2fc96939816) |
|
Claude finished @Hmbown's task in 2m 2s —— View job Reviewing PR #5784 — native ChatGPT PKCE sign-in (re-review after push)Todo list
What changed since the last reviewCommit
Good fix, and the reasoning in the commit message (revoke must clear local state even if discovery/issuer is unreachable) is sound. Findings still open from prior reviews (unaddressed across this push)
New observation on the loopback fix itself
AssessmentThe loopback dual-stack fix and the revoke-endpoint doc correction in |
There was a problem hiding this comment.
Codewhale review
PR adds native ChatGPT PKCE sign-in for openai-codex with Codewhale-owned token storage, refresh, and revoke. The implementation is careful (PKCE S256, state checks, redacted Debug, mock issuer tests), but the revoke path has correctness gaps and the TUI slash-command revoke leaves in-memory state stale.
Findings
- [WARNING]
/auth chatgpt-revokedoes not update the live TUI config (crates/tui/src/commands/groups/config/mod.rs:192)
The slash-command handler callsrevoke_owned_login(None, None), which writes the config file but does not clear the in-memoryApp.config. After revoke, the TUI still has the ChatGPT generation pointer and can show stale auth status until restart/reload. Pass the live config as the second argument. - [WARNING] Remote revoke only revokes one token per entry and ignores local-removal errors (
crates/tui/src/chatgpt_oauth.rs)
Inrevoke_owned_login_locked, each entry revokesrefresh_token.as_deref().or(access_token.as_deref()); when both are present the access token is left valid server-side. Local removal useslet _ = store.remove(&name);, so a failed delete still returnsOk(()). Revoking is security-sensitive and should attempt to revoke both tokens and propagate or log local removal failures. - [INFO] Hard-coded revocation endpoint contradicts OIDC discovery and PR description (
crates/tui/src/chatgpt_oauth.rs)
The PR description states the discoveryrevocation_endpointis used for remote revoke, butrevoke_endpoint()hard-codes{issuer}/api/accounts/oauth/revokeand the comment says discovery is deliberately not consulted. If OpenAI moves that endpoint, local revoke still works but remote revoke silently breaks. Consider reading the advertised endpoint or documenting the pinned contract more clearly. - [INFO] Unused localization key
ProviderExternalHintChatgptReview(crates/tui/src/tui/provider_picker.rs)
The new message id and all locale translations are added, but the ChatGPT auth choice screen never rendersProviderExternalHintChatgptReview. The intended hint is therefore never shown. Use it inrender_chatgpt_auth_choiceor remove the key/translations to avoid dead UI copy. - [INFO] Global logout clears local ChatGPT tokens but does not call remote revoke (
crates/cli/src/lib.rs)
clear_all_chatgpt_oauth_credentials()only deletes local Codewhale-owned files. A user pressing global logout may expect revocation, but refresh/access tokens can remain valid server-side until expiry. Consider invoking best-effort remote revoke before clearing or explicitly documenting that logout only clears local state.
Suggestions
-
crates/tui/src/commands/groups/config/mod.rs:192— Pass the live app config to revoke_owned_login so the in-memory TUI state is cleared when the owned credential generation is removed.match crate::chatgpt_oauth::revoke_owned_login(None, Some(&mut app.config)) {
Assessment
The PKCE implementation is careful and mostly well-tested. The main gaps are revoke correctness (only one token revoked, local removal errors ignored) and stale in-memory TUI state after the slash-command revoke. The legal and endpoint assumptions are documented but should be explicitly accepted before merge.
Advisory review by Codewhale (codewhale review --pr 5784 --post, head f56fce794c0de72478048468b2ea830dd66ed0b3). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
| CommandResult::action(crate::tui::app::AppAction::StartChatgptPkceLogin) | ||
| } | ||
| Some("chatgpt-revoke") | Some("chatgpt_revoke") => { | ||
| match crate::chatgpt_oauth::revoke_owned_login(None, None) { |
There was a problem hiding this comment.
[WARNING] /auth chatgpt-revoke does not update the live TUI config
The slash-command handler calls revoke_owned_login(None, None), which writes the config file but does not clear the in-memory App.config. After revoke, the TUI still has the ChatGPT generation pointer and can show stale auth status until restart/reload. Pass the live config as the second argument.
| CommandResult::action(crate::tui::app::AppAction::StartChatgptPkceLogin) | ||
| } | ||
| Some("chatgpt-revoke") | Some("chatgpt_revoke") => { | ||
| match crate::chatgpt_oauth::revoke_owned_login(None, None) { |
There was a problem hiding this comment.
Pass the live app config to revoke_owned_login so the in-memory TUI state is cleared when the owned credential generation is removed.
| match crate::chatgpt_oauth::revoke_owned_login(None, None) { | |
| match crate::chatgpt_oauth::revoke_owned_login(None, Some(&mut app.config)) { |
…ersistent_service ordering fix)
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_02e261f5-9112-4c7e-ae41-f3bd3fdcd9c3) |
|
Claude finished @Hmbown's task in 1m 4s —— View job Reviewing PR #5784 — native ChatGPT PKCE sign-in (re-review after latest push)Todo list
What changed since the last reviewNothing in the ChatGPT OAuth surface. The rest of the delta since then is just a merge of Status of previously flagged findings — all still openVerified directly against current HEAD (
None of these are new; they were raised across the two prior review rounds on this same head and remain unaddressed since no commits touched the affected files. The Good design still holdsThe core PKCE flow itself (S256 challenge, state verification on both success/error callbacks, redacted |
There was a problem hiding this comment.
Stale comment
Not approving: Cursor Bugbot was present on this head but completed as skipped (usage limit / incomplete analysis), so the required automated-review signal is not clean. Human review is needed; no additional reviewers could be assigned.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
Codewhale review
PR adds native ChatGPT PKCE sign-in for openai-codex, with token storage, refresh, revoke, credential precedence, and UI integration. The implementation is mostly solid with good test coverage, but several issues around UI blocking, state consistency, and code duplication need attention.
Findings
- [WARNING] Slash command /auth chatgpt-revoke performs blocking network I/O and does not update live config (
crates/tui/src/commands/groups/config/mod.rs)
In crates/tui/src/commands/groups/config/mod.rs, the handler for 'chatgpt-revoke' directly calls chatgpt_oauth::revoke_owned_login(None, None). This executes synchronously in the event loop, potentially blocking the TUI for up to 20 seconds during the best-effort remote revoke. Additionally, it does not update the in-memory app Config, leaving stale oauth_credential_generation and auth_mode values that may mislead the UI until a reload. - [WARNING] Logout clears ChatGPT generation but not auth_mode for openai-codex (
crates/cli/src/lib.rs)
In run_logout_command_with_secrets_unlocked (crates/cli/src/lib.rs), after clearing xai.auth_mode, the code only clears openai_codex.oauth_credential_generation if it is a valid ChatGPT generation, but does not set openai_codex.auth_mode = None. This leaves auth_mode as 'oauth' after logout, inconsistent with the xai path and potentially confusing later credential resolution or UI. - [WARNING] clear_codewhale_owned_chatgpt_oauth does not clear auth_mode (
crates/tui/src/config.rs)
In crates/tui/src/config.rs, the method clear_codewhale_owned_chatgpt_oauth only clears oauth_credential_generation but leaves auth_mode set to 'oauth'. This is also used during revoke in the TUI, so after revoking, the provider's auth_mode remains 'oauth' even though no owned OAuth tokens exist. This may cause misleading status displays or behavior in other parts of the app that check auth_mode. - [WARNING] Dead code for ChatGPT auth in CLI dispatch (
crates/cli/src/lib.rs)
In crates/cli/src/lib.rs, the match on AuthCommand in run() already handles AuthCommand::Chatgpt and AuthCommand::ChatgptRevoke (around line 1996), returning early. However, the later match inside run_auth_command_with_secrets_and_runtime (around line 3925) also contains arms for these variants, which are unreachable. This redundancy is confusing and dead code. - [INFO] Revoke endpoint not using OIDC discovery (
crates/tui/src/chatgpt_oauth.rs)
The PR description states it uses the discovery 'revocation_endpoint' for remote revoke, but the code (revoke_endpoint in chatgpt_oauth.rs) hard-codes {issuer}/api/accounts/oauth/revoke and explicitly does not read the discovery document. This contradicts the description and could break if OpenAI changes the endpoint, but the code comment acknowledges the design choice. - [INFO] Duplicate credential presence functions (
crates/tui/src/chatgpt_oauth.rs)
In chatgpt_oauth.rs, credentials_present and credentials_valid have identical implementations; credentials_present simply delegates to credentials_valid. This is unnecessary duplication. - [INFO] Missing test for token refresh failure/rotation (
crates/tui/src/chatgpt_oauth.rs)
The PR tests the success path for token refresh, but there is no test covering permanent refresh failure (e.g., invalid_grant) or rotation behavior when the refresh token is updated or not returned. This is a security-sensitive area and would benefit from additional negative tests.
Suggestions
crates/cli/src/lib.rs— When clearing ChatGPT OAuth generation during logout, also set openai_codex.auth_mode = None to mirror the xAI cleanup and avoid stale auth_mode.crates/tui/src/config.rs— In clear_codewhale_owned_chatgpt_oauth, consider setting auth_mode = None as well (or preserving only if another OAuth source exists) to keep in-memory state consistent after revoke.crates/tui/src/commands/groups/config/mod.rs— Refactor the chatgpt-revoke command to dispatch an asynchronous action (similar to login) rather than calling revoke_owned_login synchronously. This prevents UI freezing and allows updating the live config immediately.crates/cli/src/lib.rs— Remove the redundant arms for AuthCommand::Chatgpt and AuthCommand::ChatgptRevoke in run_auth_command_with_secrets_and_runtime, as they are handled earlier in run() and never reached.
Assessment
The PR is generally well-implemented with strong test coverage for the PKCE flow, storage, and UI integration. The main concerns are the synchronous and state-inconsistent revoke slash command, the incomplete logout cleanup, and some dead code. These should be addressed before merging to avoid UI blocking and ensure consistent state management.
Advisory review by Codewhale (codewhale review --pr 5784 --post, head 0e51dd439e34fa59deb1757498967347a6f3b424). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
…ersistent_service staging deadline)
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_1526c2d4-7de3-4081-b671-c5437f1d49a1) |
|
Claude finished @Hmbown's task in 2m 7s —— View job Reviewing PR #5784 — native ChatGPT PKCE sign-in (re-review after latest push)Todo list
What changed since the last reviewNothing in the PR's own diff. The two new commits ( Status of previously flagged findings — all still openSince the feature files are unchanged since the last review round (head
None of these are new — they were raised across three prior review rounds on this same feature code and remain unaddressed since no commits have touched the affected files. Design that still holds up wellCore PKCE mechanics (S256 challenge/verifier, state checked on both success and error callbacks, redacted AssessmentThis push is CI/test-infrastructure maintenance only (picking up an upstream flaky-test fix) — it does not touch the ChatGPT OAuth feature. No new review is warranted for the feature itself; the |
There was a problem hiding this comment.
Codewhale review
Adds native ChatGPT PKCE sign-in for openai-codex with Codewhale-owned token storage, CLI/TUI commands, and configuration plumbing. The design handles OAuth security well (PKCE S256, redaction, fail-closed refresh), but state cleanup after logout/revoke and remote revocation of replaced tokens have gaps, and the live flow is untested.
Findings
- [WARNING] openai_codex auth_mode is not reset on logout or revoke (
crates/cli/src/lib.rs)
After a native ChatGPT login, Config::mark_codewhale_owned_chatgpt_oauth sets provider.auth_mode = "oauth". However, run_logout_command_with_secrets_unlocked only clears oauth_credential_generation and does not clear openai_codex.auth_mode; revoke_owned_login_locked/clear_codewhale_owned_chatgpt_oauth similarly only clear the generation. This leaves the provider configured for OAuth after credentials are removed, unlike the xAI path which resets both fields. Users may be unable to re-run setup cleanly or see stale auth-mode state. - [WARNING] Superseded ChatGPT tokens are removed without remote revocation (
crates/tui/src/chatgpt_oauth.rs)
activate_pkce_login_locked identifies previous_owned_name (legacy or prior generation) and removes it locally after installing the new generation, but never calls revoke_remote_token on the old refresh token/access token. If the previous refresh token was compromised, it remains valid remotely after the user re-authenticates. Best-effort remote revocation should happen before deleting the old file, or the old token should be explicitly revoked after successful activation. - [INFO] Callback HTTP server reads only a single 4096-byte chunk (
crates/tui/src/chatgpt_oauth.rs)
handle_callback_stream performs one read of a 4096-byte buffer and assumes the request line/headers are complete. Large error_description values or TCP segmentation can truncate the request and fail the callback. A loop reading until end-of-headers (or read_to_end) with the existing timeout would be more robust. - [INFO] Revoke path may report success while leaving remote tokens valid (
crates/tui/src/chatgpt_oauth.rs)
revoke_owned_login_locked parses the local file to revoke tokens; if parse_auth_file fails (corrupt file), it silently skips remote revocation, removes the local file, and returns Ok. The user sees success but the remote refresh token remains valid. At minimum it should warn/fail when it cannot revoke a credential it is deleting. - [INFO] Live OpenAI flow not actually exercised
PR checklist notes TUI browser login was not live-tested against OpenAI, and full workspace tests were not run locally. The mocked unit tests cover PKCE mechanics but not real issuer behavior, redirect ports, or token refresh rotation. Consider a manual/CI integration test before merge.
Suggestions
crates/cli/src/lib.rs— In run_logout_command_with_secrets_unlocked, addopenai_codex.auth_mode = None;after clearing the OAuth generation, and in Config::clear_codewhale_owned_chatgpt_oauth, also setentry.auth_mode = Nonewhen clearing generation so the provider does not remain in OAuth mode after logout/revoke.crates/tui/src/chatgpt_oauth.rs— Before removing previous_owned_name in activate_pkce_login_locked, read its entries and call revoke_remote_token (best-effort) for each refresh/access token, logging failures, so replaced credentials are not left valid remotely.crates/tui/src/chatgpt_oauth.rs— Modify handle_callback_stream to loop reads until the request headers are complete or a maximum size is reached, instead of a single read, to avoid truncation on split packets or large query strings.crates/tui/src/chatgpt_oauth.rs— In revoke_owned_login_locked, when parse_auth_file fails, return an error or at least tracing::warn before removing the local file, so the user is informed that remote revocation could not be performed.
Assessment
Well-implemented feature with careful redaction and separation of owned vs external credentials. Needs fixes for state cleanup after logout/revoke and remote revocation of superseded tokens before merge; live testing recommended.
Advisory review by Codewhale (codewhale review --pr 5784 --post, head b3d64ff3527f4e8ebbc8e4d38197cea9666fc08a). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
There was a problem hiding this comment.
Codewhale review
PR #5784 adds a native ChatGPT PKCE OAuth login for the openai-codex provider. The implementation is substantial and includes credential storage, refresh, and revoke logic, plus TUI/CLI wiring, documentation, and translations. Overall the approach is sound and reuses existing credential-store patterns, but there are a few security/behavioral edge cases and missing tests worth addressing before merge.
Findings
- [WARNING] chatgpt-revoke may leave orphaned credential files (
crates/tui/src/chatgpt_oauth.rs)
revoke_owned_login_locked removes only the generation referenced by the config (or the legacy file if no generation exists). If a previous activation's cleanup failed, or another chatgpt-auth-*.json file exists, it remains on disk after 'codewhale auth chatgpt-revoke' succeeds. Logout clears everything via clear_all_chatgpt_oauth_credentials, but the revoke command should arguably also remove all Codewhale-owned ChatGPT token files to honor the user's intent to revoke. - [INFO] credentials_valid performs file I/O on hot config checks (
crates/tui/src/config.rs)
active_provider_has_config_api_key now calls crate::chatgpt_oauth::credentials_valid(config) each time. This function reads and parses the credential file from disk. If this check runs during rendering or before each request, it could add latency. Consider caching the validity result or deferring the file read until credentials are actually needed. - [INFO] Auth status may report ChatGPT sign-in even when the credential file is missing (
crates/cli/src/lib.rs)
auth_status_lines_for_provider_with_runtime checks only that oauth_credential_generation is a valid ChatGPT generation string, not that the corresponding file exists. If the file is deleted out-of-band, status shows 'codewhale-owned ChatGPT sign-in (availability not probed)', which is misleading because the actual credential resolution will fall back to external consent or fail. - [INFO] No test covers the full PKCE flow with real loopback binding (
crates/tui/src/chatgpt_oauth.rs)
Unit tests exercise callback parsing, token exchange, persist/refresh/revoke, and the callback server, but there is no integration test that binds actual loopback ports, starts a mock issuer, and runs pkce_login end-to-end. The PR checklist also notes that live PKCE was not tested against OpenAI. Adding a test that uses ephemeral ports and a local HTTP server would improve confidence.
Suggestions
crates/tui/src/chatgpt_oauth.rs— In revoke_owned_login_locked, instead of removing only the current generation or legacy file, enumerate all chatgpt-auth-*.json files (using the existing chatgpt_auth_names_in_store helper) and revoke/remove each one. This ensures no refresh tokens remain on disk after a user-initiated revoke.
Assessment
The feature is well-structured and reuses existing patterns, but the revoke behavior and some status/performance edge cases should be reviewed before merge. The missing end-to-end PKCE test is a notable gap for a security-critical path.
Advisory review by Codewhale (codewhale review --pr 5784 --post, head 9749e41081ddd836fbe10939ca216c33df8ed67d). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
…kce-5778-20260831
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_a5175313-76c0-47c2-a286-3494ef05d371) |
|
Claude finished @Hmbown's task in 1m 36s —— View job Reviewing PR #5784 — native ChatGPT PKCE sign-in (re-review after latest push)Todo list
What changed since the last reviewTwo commits landed on top of
I verified this is infrastructure-only for this PR: Status of previously flagged findings — unaffected, still openVerified directly against current HEAD (e.g.
None of these are new — they've been raised consistently across the Design that still holds up wellCore PKCE mechanics (S256 challenge/verifier, AssessmentThis push is |
| for listener in listeners { | ||
| match listener.accept() { | ||
| Ok((stream, _)) => { | ||
| return handle_callback_stream(stream, expected_state); |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
wait_for_callback() returns on the first accepted loopback connection. Any GET that fails path/query/state checks, or an error= callback that omits state, then fails the entire ChatGPT sign-in. Ports 1455/1457 are well-known and the wait lasts up to 300s, so another local tab, process, or a page that navigates the browser to the loopback callback can abort login before the real redirect arrives.
accept_callback() also skips the state check when an error callback has no state, and interpolates error_description into the user-visible failure shown in the TUI. Success still requires matching state and PKCE, so this is login abort plus attacker-controlled error copy, not token theft.
Impact: During native ChatGPT PKCE sign-in, an unauthenticated request to the loopback callback can cancel the pending login and surface untrusted error text in the TUI.
Reviewed by Cursor Security Reviewer for commit 02e4122. Configure here.
… live route Review findings on #5784 that reviewers agreed should block merge: - The slash command called `revoke_owned_login(None, None)` inline from the async command dispatcher: one blocking reqwest POST per stored token under the OAuth lifecycle lock froze the TUI, `None` missed the session's own `app.config_path`, and `None` left `App.config`'s `oauth_credential_generation` and `auth_mode` stale. It now returns `AppAction::StartChatgptRevoke`; the loop runs the revoke on `spawn_blocking` against the session config path and clears the live route on success, mirroring the PKCE login path. - `auth_mode = "oauth"` is now reset with the generation pointer on revoke (persisted document and live config) and on CLI logout, so a revoked route does not keep claiming OAuth. Evidence: `cargo test -p codewhale-tui --lib chatgpt` test result: ok. 14 passed; 0 failed; 0 ignored (includes the new chatgpt_revoke_slash_command_defers_to_the_event_loop) `cargo check -p codewhale-cli` clean; `cargo fmt --all -- --check` clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdbuqwHAXSDcikPiS6L6Qw
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_c202dced-8c67-4baa-84a3-2f517ab781cb) |
|
Claude finished @Hmbown's task in 1m 29s —— View job Reviewing PR #5784 — native ChatGPT PKCE sign-in (re-review after latest push)Todo list
What changed since the last reviewOne commit landed on top of This resolves the item every reviewer agreed should block mergeVerified directly against current HEAD:
I was not able to run Remaining open items — unaffected by this push, still lower-severity follow-upsNone of these were in scope for this commit and none are newly introduced by it:
None of these individually block merge on their own — they're correctness/robustness gaps already tracked across prior rounds, mostly lower severity than the fixed issue (UI freeze + wrong-file + stale-state on a credential-revocation path). AssessmentThis push directly and correctly addresses the one finding that recurred across every review round on this PR and that reviewers agreed should block merge: |
| if let Some(config) = live_config { | ||
| config.clear_codewhale_owned_chatgpt_oauth(); | ||
| } | ||
| let names = match previous.as_deref() { |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
revoke_owned_login only remotely revokes and deletes the single oauth_credential_generation basename (or, if that pointer is missing/invalid, only the legacy chatgpt-oauth.json). Login already treats superseded-generation unlink as best-effort, and store.remove errors are swallowed here, so a prior chatgpt-auth-<hex>.json can keep a still-valid refresh token.
After /auth chatgpt-revoke / codewhale auth chatgpt-revoke, the config pointer is gone so the session looks signed out, while leftover files in $CODEWHALE_HOME/credentials can still mint access tokens. Logout already sweeps every ChatGPT generation via clear_all_chatgpt_oauth_credentials() / clear_chatgpt.
Impact: A user who revoked to invalidate ChatGPT credentials can leave live refresh tokens on disk that OpenAI will still honor.
Reviewed by Cursor Security Reviewer for commit 0198746. Configure here.
There was a problem hiding this comment.
Codewhale review
Adds native ChatGPT PKCE sign-in for openai-codex, storing refreshable tokens in Codewhale-owned credentials, with CLI/TUI wiring, config-layer support, locale strings, and documentation. The implementation is generally robust with redaction, state checks, loopback binding, and unit tests, but has concerns around blocking I/O in async contexts, the hardcoded public client ID / revoke endpoint, and legal ambiguity.
Findings
- [WARNING] Blocking OAuth refresh may stall the async event loop (
crates/tui/src/chatgpt_oauth.rs)
get_owned_credentialsusesReqwestTokenClient(a blocking reqwest client) and is invoked fromConfig::codex_credentialsduring credential resolution. If this resolution happens on the Tokio async runtime, the blocking refresh can block a worker thread and potentially starve other tasks. Consider using an async client or wrapping the refresh inspawn_blocking. - [WARNING] Hardcoded public Codex CLI client ID and ambiguous terms (
crates/tui/src/chatgpt_oauth.rs)
The code hardcodesapp_EMoamEEZ73f0CkXaXp7hrannwithoriginator=codewhale. OpenAI has not published a third-party client-registration path for this public Codex client, and the PR description itself labels this as 'ambiguous'. This introduces legal/ToS risk and could break if OpenAI revokes the client ID or enforces originator restrictions. - [WARNING] Remote revoke endpoint is hardcoded despite discovery advertising revocation_endpoint (
crates/tui/src/chatgpt_oauth.rs)
The PR summary and docs mention using the discoveryrevocation_endpoint, but the implementation hardcodes{issuer}/api/accounts/oauth/revoke. While the comment explains the rationale (cleanup should not depend on discovery), this will silently fail if OpenAI changes the path, and it contradicts the stated design. Consider making the endpoint configurable or using discovery with a fallback. - [INFO] Missing test for remote revoke failure still clearing local credentials (
crates/tui/src/chatgpt_oauth.rs)
The tests cover a successful remote revoke, but there is no test verifying that local credential files are removed even when the remote revoke returns an error. The code appears to handle this correctly, but a regression test would strengthen the guarantee. - [INFO] Synchronous revoke in CLI blocks the async runtime (
crates/tui/src/lib.rs)
run_chatgpt_pkce_revokeis a synchronous function that performs blocking network calls viarevoke_owned_login, and it is called directly from the asyncrun_async_main_dispatch. This can block the async event loop. The TUI path correctly usesspawn_blocking; the CLI path should follow the same pattern.
Assessment
The PR is well-structured with careful error handling, redaction, and test coverage. However, the blocking I/O in async paths, the hardcoded public client ID and revoke endpoint, and the legal ambiguity should be addressed before merging. The missing test for revoke failure cleanup is minor but valuable.
Advisory review by Codewhale (codewhale review --pr 5784 --post, head 019874602d922869f545085314c16941f00388d8). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdbuqwHAXSDcikPiS6L6Qw
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_5cd9d01e-bed1-4436-bb00-b64eb0e78584) |
|
Claude finished @Hmbown's task in 38s —— View job Reviewing PR #5784 — native ChatGPT PKCE sign-in (re-review after latest push)Todo list
What changed since the last reviewNothing. The only commit since Status of the last fix — confirmed still in placeThe prior review round's fix for the item every reviewer agreed should block merge (
Remaining open items — unaffected by this push, unchanged from prior roundsThese are pre-existing, lower-severity robustness/cleanup gaps already tracked across many review rounds on this PR, none touched by
AssessmentThis push is CI infrastructure only (an empty commit to retrigger a hung Buildkite build) — it does not touch the ChatGPT OAuth feature in any way. No new findings. The core PKCE design and the previously-blocking |
There was a problem hiding this comment.
Codewhale review
Adds native ChatGPT PKCE sign-in for the openai-codex provider, with browser loopback callback, Codewhale-owned token storage, refresh/revoke support, and provider picker update. The code is generally well-structured, security-conscious (redacted Debug, state validation, honest originator), and has substantial unit tests.
Findings
- [WARNING] Silent fallback to production issuer hides invalid CODEWHALE_CHATGPT_OAUTH_ISSUER (
crates/tui/src/chatgpt_oauth.rs:172)
In build_authorize_url, Url::parse of the configured issuer falls back to the hardcoded https://auth.openai.com/oauth/authorize on any parse error. If a user sets CODEWHALE_CHATGPT_OAUTH_ISSUER to a malformed or test issuer, the code silently redirects them to the production authorization endpoint instead of surfacing the misconfiguration. Prefer propagating the parse error so custom issuer mistakes fail loudly. - [INFO] Unused localization key ProviderExternalHintChatgptReview (
crates/tui/src/localization.rs:1674)
The new MessageId::ProviderExternalHintChatgptReview and its locale strings are never referenced in Rust code. The picker footer uses MessageId::ProviderExternalActionReuseCodex for the 'E' action hint, so the new review hint string is dead. Either remove the unused key from the enum and locale files, or wire it into the render path where it was intended. - [INFO] Standalone
auth chatgpt-revokeblocks the async main thread (crates/tui/src/lib.rs:8163)
run_chatgpt_pkce_revoke is a synchronous function that calls chatgpt_oauth::revoke_owned_login, which performs a blocking HTTP round trip under the OAuth lifecycle lock. In the async CLI entrypoint this blocks the executor thread. The TUI event-loop path avoids this by using spawn_blocking, but the standalonecodewhale-tui auth chatgpt-revokecommand still blocks. Consider wrapping this standalone revoke in spawn_blocking as well, or making the function async and awaiting a blocking task.
Assessment
The PR is well-tested and carefully implements the documented PKCE flow with fail-closed token refresh and local-first revoke. The issues found are minor and do not block merge, but the silent issuer fallback should be addressed to avoid surprising production calls during custom issuer testing.
Advisory review by Codewhale (codewhale review --pr 5784 --post, head 62cce8ff0c15dc59e9a52faf4f440b3dfba03d06). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
| let response = client | ||
| .post(url) | ||
| .form(form) | ||
| .send() |
There was a problem hiding this comment.
[WARNING] Silent fallback to production issuer hides invalid CODEWHALE_CHATGPT_OAUTH_ISSUER
In build_authorize_url, Url::parse of the configured issuer falls back to the hardcoded https://auth.openai.com/oauth/authorize on any parse error. If a user sets CODEWHALE_CHATGPT_OAUTH_ISSUER to a malformed or test issuer, the code silently redirects them to the production authorization endpoint instead of surfacing the misconfiguration. Prefer propagating the parse error so custom issuer mistakes fail loudly.
| ChatgptAuthChoiceIntro, | ||
| ChatgptAuthChoicePkceOption, | ||
| ChatgptAuthChoiceImportOption, | ||
| ProviderExternalHintChatgptReview, |
There was a problem hiding this comment.
[INFO] Unused localization key ProviderExternalHintChatgptReview
The new MessageId::ProviderExternalHintChatgptReview and its locale strings are never referenced in Rust code. The picker footer uses MessageId::ProviderExternalActionReuseCodex for the 'E' action hint, so the new review hint string is dead. Either remove the unused key from the enum and locale files, or wire it into the render path where it was intended.
|
|
||
| fn run_chatgpt_pkce_revoke(config_path: Option<&Path>) -> Result<()> { | ||
| chatgpt_oauth::revoke_owned_login(config_path, None)?; | ||
| println!("Revoked Codewhale-owned ChatGPT tokens. Codex CLI consent is unchanged."); |
There was a problem hiding this comment.
[INFO] Standalone auth chatgpt-revoke blocks the async main thread
run_chatgpt_pkce_revoke is a synchronous function that calls chatgpt_oauth::revoke_owned_login, which performs a blocking HTTP round trip under the OAuth lifecycle lock. In the async CLI entrypoint this blocks the executor thread. The TUI event-loop path avoids this by using spawn_blocking, but the standalone codewhale-tui auth chatgpt-revoke command still blocks. Consider wrapping this standalone revoke in spawn_blocking as well, or making the function async and awaiting a blocking task.
run_logout_command_with_secrets wraps everything in with_xai_oauth_revocation_transaction, which holds the non-reentrant lifecycle mutex; the new chatgpt clear inside it called clear_all_chatgpt_oauth_credentials, which takes the same mutex again. Every logout hung forever — the three logout_* tests spun >47m on buildkite and both Actions test jobs stuck in_progress for 75+ minutes, and a real 'codewhale auth logout' would hang the same way. Add clear_all_chatgpt_oauth_credentials_locked() for callers already inside the transaction (store open is lock-free; the fd lock is not re-taken) and use it from logout. Evidence: cargo test -p codewhale-cli --lib logout_ 'test result: ok. 5 passed; 0 failed' in 0.05s (was: infinite hang). Signed-off-by: CodeWhale Bot <bot@codewhale.net>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_31fa8335-0d6f-48dc-8c6e-9fd511393e73) |
|
Claude finished @Hmbown's task in 2m 7s —— View job Reviewing PR #5784 — native ChatGPT PKCE sign-in (re-review after latest push)Todo list
What changed since the last reviewOne commit landed on top of The fix is correct
The fix adds I checked for other latent instances of this same bug class — anything invoking I could not run Status of previously flagged findings — unaffected by this pushThis commit is scoped entirely to the logout deadlock; it doesn't touch AssessmentThis is a correct, well-isolated, well-evidenced fix for a real and severe bug (every |
There was a problem hiding this comment.
Agentic security review found one new medium issue: TUI logout does not destroy Codewhale-owned ChatGPT PKCE tokens, unlike CLI codewhale logout. Prior loopback-callback abort and incomplete chatgpt-revoke file sweep findings remain open and unacknowledged.
Sent by Cursor Security Agent: Security Reviewer
| entry.external_credentials = None; | ||
| } | ||
|
|
||
| pub(crate) fn clear_codewhale_owned_chatgpt_oauth(&mut self) { |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: MEDIUM
TUI /logout and codewhale-tui --logout never destroy Codewhale-owned ChatGPT PKCE tokens. CLI codewhale logout now unsets the openai-codex generation pointer and sweeps chatgpt-auth-*.json, but TUI logout still only special-cases xAI OAuth. This helper exists for in-memory revoke cleanup and is unused by those logout paths, so the UI can report credentials cleared while codex_credentials() still mints access tokens from leftover files.
Impact: After native ChatGPT sign-in, TUI logout leaves a live refresh token on disk and the next Codex request continues as signed in.
Reviewed by Cursor Security Reviewer for commit f59eb47. Configure here.
There was a problem hiding this comment.
Codewhale review
Adds native ChatGPT PKCE sign-in using the published OpenAI issuer and Codewhale-owned token storage. The core adapter is thorough and has good unit coverage, but several security and robustness concerns remain around loopback binding, revocation error handling, and blocking work on the async runtime.
Findings
- [WARNING] Loopback callback may bind only one address family (
crates/tui/src/chatgpt_oauth.rs)
bind_loopback_callbackreturns success as soon as at least one listener is bound. The redirect URI ishttp://localhost, which can resolve to::1on IPv6-first hosts. If Codewhale binds only127.0.0.1because another process already owns[::1]:1455, the browser can deliver the authorization code to that other local process instead of Codewhale. DistinguishAddrInUse/Accesson the alternate family fromAddrNotAvailable/unsupported and fail closed, or bind both stacks atomically before opening the browser. - [WARNING] Revoke ignores local credential deletion failures (
crates/tui/src/chatgpt_oauth.rs)
Inrevoke_owned_login_locked, eachstore.remove(&name)result is discarded withlet _ =. If removal fails due to permissions, directory handle failure, or other I/O errors, the function still returnsOk(()), so the TUI/CLI reports success while valid tokens remain on disk. At minimum log a warning on removal error, and consider propagating the first failure so callers do not claim revocation succeeded. - [WARNING] ChatGPT revoke blocks the async runtime (
crates/tui/src/lib.rs)
crates/tui/src/lib.rscallsrun_chatgpt_pkce_revokedirectly inrun_async_main_dispatch. That function performs at least one blocking HTTP round trip under the lifecycle lock, tying up a Tokio worker thread in CLI/TUI dispatchers. The interactiverun_chatgpt_revoke_from_tuipath already usestokio::task::spawn_blocking; the CLI/headless path should do the same. - [INFO] Unused localization key ProviderExternalHintChatgptReview (
crates/tui/src/localization.rs:1670)
MessageIdProviderExternalHintChatgptReviewand its translations are added in this PR but never referenced by any code path. The TUI footer usesProviderExternalActionReuseCodexinstead. This adds dead locale keys and can confuse future translators; either remove the unused key or use it in the appropriate hint. - [INFO] Missing test update for setup readiness copy (
crates/tui/src/tui/setup/mod.rs:355)
The user-facing message incrates/tui/src/tui/setup/mod.rschanged fromrun codex login...toSign in with ChatGPT..., but no corresponding test is updated in the diff. Existingtui/setupreadiness tests may assert the old string and fail under a full workspace test run. Confirm and update tests in that module before merge. - [INFO] Refresh network I/O holds global OAuth lifecycle lock (
crates/tui/src/chatgpt_oauth.rs)
get_owned_credentials_withacquireswith_xai_oauth_lifecycle_lockand callsrefresh_access_tokeninside the locked closure. While a refresh is waiting on the network, xAI/ChatGPT login, revoke, and logout operations are blocked. Consider performing the refresh outside the lock and reapplying the result under lock, or narrowing the lock to the shared credentials directory only.
Assessment
The implementation is comprehensive and unit-tested, but it should not merge without addressing the loopback family fallback and revoke deletion error swallowing. The dead localization key and likely missing setup test update also need cleanup, and the CLI revoke path should avoid blocking the async runtime.
Advisory review by Codewhale (codewhale review --pr 5784 --post, head f59eb47b79ca5209f88b6b2ceb63f9e2932af7e7). Line-specific findings are also posted as inline review comments; mechanical fixes arrive as committable suggestions you can apply from the Files tab. CODEOWNERS approval still governs merge.
| XaiAuthChoiceIntro, | ||
| XaiAuthChoiceApiKeyOption, | ||
| XaiAuthChoiceDeviceOAuthOption, | ||
| ChatgptAuthChoiceTitle, |
There was a problem hiding this comment.
[INFO] Unused localization key ProviderExternalHintChatgptReview
MessageId ProviderExternalHintChatgptReview and its translations are added in this PR but never referenced by any code path. The TUI footer uses ProviderExternalActionReuseCodex instead. This adds dead locale keys and can confuse future translators; either remove the unused key or use it in the appropriate hint.
| @@ -355,7 +355,7 @@ impl SetupRuntimeFacts { | |||
| format!("{}; retry or open /provider", readiness.label()) | |||
There was a problem hiding this comment.
[INFO] Missing test update for setup readiness copy
The user-facing message in crates/tui/src/tui/setup/mod.rs changed from run codex login... to Sign in with ChatGPT..., but no corresponding test is updated in the diff. Existing tui/setup readiness tests may assert the old string and fail under a full workspace test run. Confirm and update tests in that module before merge.
build_authorize_url silently fell back to the production authorize endpoint when CODEWHALE_CHATGPT_OAUTH_ISSUER failed to parse, sending the browser to a sign-in the user aimed somewhere else (review WARNING on #5784's final pass). The env's unset/empty default still resolves the production issuer; only a parse failure errors, naming the env var. cargo test -p codewhale-tui --lib -- chatgpt: 15 passed; 0 failed (includes malformed_issuer_fails_loudly_not_to_production). Signed-off-by: CodeWhale Bot <bot@codewhale.net>
…utomation slice 1 Signed-off-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-Authored-By: Hunter Bown <hmbown@gmail.com>





Summary
Native Sign in with ChatGPT for the first-class
openai-codexroute. Connecting a ChatGPT/Codex subscription no longer requires Codex CLI or~/.codex/auth.json. Browser PKCE with a localhost callback stores refreshable tokens in Codewhale-owned storage, matching the xAI device-login pattern.Closes #5778.
Terms research (ambiguous — implemented behind a documented boundary)
Public sources checked 2026-08-31:
localhost:1455) is the official Codex client flow. Device-code is documented as a Codex CLI beta (codex login --device-auth), not as a third-party API.https://auth.openai.com/.well-known/openid-configuration): advertisesauthorization_code+ PKCES256,refresh_token, scopesopenid profile email offline_access, andrevocation_endpoint. Does not advertisedevice_authorization_endpoint.app_EMoamEEZ73f0CkXaXp7hrann, Apache-2.0 Codex CLI).Conclusion: ambiguous. This PR implements the published authorization-code + PKCE S256 adapter and does not smuggle unpublished endpoints:
originator=codewhale(nevercodex_cli_rs)/oauth/authorizeand/oauth/tokenon the published issuerrevocation_endpointfor remote revoke/api/accounts/deviceauth/*(unpublished; issuer does not advertise device authorization)$CODEWHALE_HOME/credentials/chatgpt-auth-<hex>.json; Codex CLI files are never written or refreshedIf OpenAI later allocates a Codewhale-specific client id, swap
CODEWHALE_CHATGPT_OAUTH_CLIENT_ID.Behavior
/provider setup openai-codex(and missing-auth handoff) offers Sign in with ChatGPT first (subscription / ChatGPT billing) vs Import Codex CLI credentials (explicit read-only consent, Codex CLI remains owner). Copy distinguishes this from theopenaiAPI-key billing owner before any run.codewhale auth chatgpt//auth chatgptruns PKCE login.codewhale auth chatgpt-revoke//auth chatgpt-revokedeletes Codewhale-owned tokens (best-effort remote revoke) and does not touch Codex CLI consent.Testing
cargo fmt --all(clean)cargo clippy -p codewhale-config --tests -- -D warningscargo clippy -p codewhale-tui --tests -- -D warnings -A clippy::too_many_arguments(the allow is pre-existing onruntime_threads/underwater, not introduced here)cargo clippy -p codewhale-cli --tests -- -D warnings -A clippy::too_many_argumentscargo test -p codewhale-config --lib xai_credentialscargo test -p codewhale-tui --lib chatgpt_oauth(10 tests: PKCE S256, callback success/error/state, mock token exchange, persist/refresh/revoke, no live OpenAI)cargo test -p codewhale-tui --lib -- provider_picker::tests(126)cargo test -p codewhale-cli --lib -- parses_auth_subcommand_matrix auth_statuspython3 scripts/check-tui-locale-parity.pycargo test --workspace --all-features --lockednot run locally (TUI crate compile is large; CI will cover)Unit tests use a mock issuer. They do not hit live OpenAI.
Checklist
chatgpt_oauth.rs), not a product fork; it reuses the existing Codewhale-owned credentials directorydocs/PROVIDERS.md,docs/CONFIGURATION.md)Note
High Risk
Introduces OAuth token lifecycle, loopback callback handling, and global logout/revoke paths for a billing-critical provider; mistakes could leak credentials, deadlock logout, or break Codex access.
Overview
Adds Sign in with ChatGPT as a first-class way to use the
openai-codexroute: browser PKCE + localhost callback (ports 1455/1457), Codewhale-owned token files under$CODEWHALE_HOME/credentials, and honestoriginator=codewhale. Codex CLI import stays an explicit, read-only alternative—not a prerequisite.CLI/TUI:
codewhale auth chatgptandauth chatgpt-revoke(plus/auth chatgptand/auth chatgpt-revoke) wire through the same flows as xAI device login. Global logout clears ChatGPT OAuth config pointers and credential files via a non-reentrantclear_all_chatgpt_oauth_credentials_lockedso nested lifecycle locks do not hang.Runtime: OpenAI Codex credential order becomes env → Codewhale-owned ChatGPT sign-in (with refresh under lock) → consent-gated Codex CLI file.
auth status, credential resolution, setup copy, and provider picker onboarding reflect that order; the picker adds a ChatGPT vs Import Codex CLI stage before external consent.Config crate: Shared OAuth generation validation/paths/clear helpers for
chatgpt-auth-<hex>.jsonalongside existing xAI storage in the credentials directory.Reviewed by Cursor Bugbot for commit f59eb47. Bugbot is set up for automated code reviews on this repo. Configure here.