From 2f0a01d75cedb3b04c8b880cbb9d778f46155e49 Mon Sep 17 00:00:00 2001 From: Dmitry Kuznetsov Date: Thu, 17 Sep 2026 01:59:25 +0300 Subject: [PATCH] fix(oauth): expose manual Codex callback entry (#9944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged after batch validation on a combined worktree cut from `release/v3.8.51` with #9908 and #7138. **Evidence** - Focused tests: 36/36 pass on the combined tree (`oauth-modal-codex-lan-ip-8046`, `antigravity-image-credential-retry`, `antigravity-usage-service`, `generic-quota-fetcher`), including the 2 pre-existing anchor tests that assert `codex` stays in `PKCE_CALLBACK_SERVER_PROVIDERS` and that the `localhost:1455` redirect URI is untouched. - Gates on the combined tree: `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check-changelog-integrity` PASS. - `check-file-size` is red, but reproduces byte-identical on the pure `release/v3.8.51` tip (`imageGeneration.ts`, `roundRobinCombo.ts`, `stream.ts`) — inherited base-red, not from this PR. **Reconciled** - `changelog.d/fixes/codex-manual-loopback-action.md` did not start with a markdown bullet, which is the one thing `check-changelog-integrity` failed on. Fixed in your branch (d3dbb5b) so the fragment convention holds; nothing else in your diff was touched. Thanks for this one, @Ardem2025 — exposing the manual callback entry that already existed in the code instead of adding a new flow is exactly the right shape for the LAN/remote case, and keeping every PKCE/state check untouched made it easy to verify. --- .../fixes/codex-manual-loopback-action.md | 1 + src/shared/components/OAuthModal.tsx | 10 ++++---- src/shared/components/OAuthModalPanels.tsx | 5 ++++ .../oauth-modal-codex-lan-ip-8046.test.ts | 25 ++++++++++++++++++- 4 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 changelog.d/fixes/codex-manual-loopback-action.md diff --git a/changelog.d/fixes/codex-manual-loopback-action.md b/changelog.d/fixes/codex-manual-loopback-action.md new file mode 100644 index 0000000000..98979c8ec6 --- /dev/null +++ b/changelog.d/fixes/codex-manual-loopback-action.md @@ -0,0 +1 @@ +- Add a visible manual callback-entry action to the Codex loopback warning so remote users can paste the authorization result instead of setting up an SSH tunnel. diff --git a/src/shared/components/OAuthModal.tsx b/src/shared/components/OAuthModal.tsx index bf6d47b57b..42c3884d65 100644 --- a/src/shared/components/OAuthModal.tsx +++ b/src/shared/components/OAuthModal.tsx @@ -374,11 +374,10 @@ export default function OAuthModal({ [provider, onSuccess, reauthConnection, t] ); - // Start OAuth flow. `opts.grokBrowser` lets the grok-cli method tabs force a - // specific branch synchronously (avoids reading a just-set state value through - // a stale closure); when omitted, falls back to the grokBrowserMode state. + // Start OAuth flow. Options let method buttons select a branch synchronously + // instead of reading a just-set state value through a stale closure. const startOAuthFlow = useCallback( - async (opts?: { grokBrowser?: boolean }) => { + async (opts?: { grokBrowser?: boolean; manualLoopback?: boolean }) => { if (!provider) return; try { setError(null); @@ -533,7 +532,7 @@ export default function OAuthModal({ setPolling(false); forceManual = true; } - } else if (isLocalhost) { + } else if (isLocalhost && !opts?.manualLoopback) { setLoopbackHint(buildPkceLoopbackMismatchHint(provider, loopbackLocation)); setStep("loopback-mismatch"); return; @@ -1160,6 +1159,7 @@ export default function OAuthModal({ void startOAuthFlow({ manualLoopback: true })} onClose={handleClose} /> )} diff --git a/src/shared/components/OAuthModalPanels.tsx b/src/shared/components/OAuthModalPanels.tsx index a691a20a39..c39a6d7941 100644 --- a/src/shared/components/OAuthModalPanels.tsx +++ b/src/shared/components/OAuthModalPanels.tsx @@ -89,6 +89,7 @@ export function OAuthDeviceCodePanel({ type OAuthLoopbackMismatchPanelProps = { providerName: string; hint: PkceLoopbackMismatchHint; + onManualInput: () => void; onClose: () => void; }; @@ -103,6 +104,7 @@ type OAuthLoopbackMismatchPanelProps = { export function OAuthLoopbackMismatchPanel({ providerName, hint, + onManualInput, onClose, }: OAuthLoopbackMismatchPanelProps) { const t = useTranslations("oauthModal"); @@ -194,6 +196,9 @@ export function OAuthLoopbackMismatchPanel({

{t("loopbackMismatchAlternative")}

+ diff --git a/tests/unit/oauth-modal-codex-lan-ip-8046.test.ts b/tests/unit/oauth-modal-codex-lan-ip-8046.test.ts index 6bc0cbc7f7..9add496077 100644 --- a/tests/unit/oauth-modal-codex-lan-ip-8046.test.ts +++ b/tests/unit/oauth-modal-codex-lan-ip-8046.test.ts @@ -41,7 +41,7 @@ test("PKCE callback-server providers now warn (not window.open) on a LAN-IP orig // that surfaces the loopback mismatch instead of falling through silently. // The follow-up swapped the flat warning string for the structured hint that feeds // the dedicated panel (buildPkceLoopbackMismatchHint) — the guard itself is unchanged. - const arm = modal.match(/else if \(isLocalhost\) \{[\s\S]{0,300}?\n {10}\}/); + const arm = modal.match(/else if \(isLocalhost(?: && [^)]+)?\) \{[\s\S]{0,300}?\n {10}\}/); assert.ok(arm, "expected an `else if (isLocalhost)` arm inside the callback-server branch"); assert.match( arm![0], @@ -57,6 +57,29 @@ test("PKCE callback-server providers now warn (not window.open) on a LAN-IP orig ); }); +test("loopback warning offers the existing manual callback flow", () => { + const panels = readFileSync( + resolve(here, "../../src/shared/components/OAuthModalPanels.tsx"), + "utf8" + ); + + assert.match( + modal, + /isLocalhost && !opts\?\.manualLoopback/, + "the warning guard must be bypassable only through an explicit manual-flow action" + ); + assert.match( + modal, + /onManualInput=\{\(\) => void startOAuthFlow\(\{ manualLoopback: true \}\)\}/, + "the mismatch panel action must initialize the existing authorization-code flow" + ); + assert.match( + panels, + /