fix(oauth): expose manual Codex callback entry (#9944)

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.
This commit is contained in:
Dmitry Kuznetsov
2026-09-17 01:59:25 +03:00
committed by GitHub
parent f1e7148c19
commit 2f0a01d75c
4 changed files with 35 additions and 6 deletions

View File

@@ -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.

View File

@@ -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({
<OAuthLoopbackMismatchPanel
providerName={providerInfo.name}
hint={loopbackHint}
onManualInput={() => void startOAuthFlow({ manualLoopback: true })}
onClose={handleClose}
/>
)}

View File

@@ -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({
<p className="text-xs text-text-muted">{t("loopbackMismatchAlternative")}</p>
</div>
<div className="flex gap-2">
<Button onClick={onManualInput} fullWidth>
{t("popupBlocked")}
</Button>
<Button onClick={onClose} variant="secondary" fullWidth>
{t("cancel")}
</Button>

View File

@@ -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,
/<Button onClick=\{onManualInput\} fullWidth>/,
"the mismatch panel must render a visible manual-entry action next to Cancel"
);
});
test("buildPkceLoopbackMismatchWarning mentions the fixed redirect and a way forward", () => {
const msg = buildPkceLoopbackMismatchWarning("codex");
assert.match(msg, /localhost:1455/);