Skip to content

Commit a8aec23

Browse files
committed
Merge branch 'git-plan/01-headless-auth' into git-plan/02-workspace-routing
2 parents b208746 + 463984b commit a8aec23

12 files changed

Lines changed: 348 additions & 52 deletions

File tree

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,21 @@ codex auth fix --dry-run
109109
codex auth doctor --fix
110110
```
111111

112+
If the shell should not launch a browser, use the manual callback flow:
113+
114+
```bash
115+
codex auth login --manual
116+
CODEX_AUTH_NO_BROWSER=1 codex auth login
117+
```
118+
119+
In non-TTY/manual shells, provide the full redirect URL on stdin instead of waiting for a browser callback:
120+
121+
```bash
122+
echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual
123+
```
124+
125+
No new npm scripts or storage migration steps are required for this login-flow update.
126+
112127
---
113128

114129
## Command Toolkit
@@ -234,6 +249,7 @@ codex auth login
234249
- `codex auth` unrecognized: run `where codex`, then follow `docs/troubleshooting.md` for routing fallback commands
235250
- Switch succeeds but wrong account appears active: run `codex auth switch <index>`, then restart session
236251
- OAuth callback on port `1455` fails: free the port and re-run `codex auth login`
252+
- Browser launch is blocked or you are in a headless shell: re-run `codex auth login --manual` or set `CODEX_AUTH_NO_BROWSER=1`
237253
- `missing field id_token` / `token_expired` / `refresh_token_reused`: re-login affected account
238254

239255
</details>

docs/features.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ User-facing capability map for `codex-multi-auth`.
5454
| Quick switch and search hotkeys | Faster navigation in the dashboard |
5555
| Account action hotkeys | Per-account set, refresh, toggle, and delete shortcuts |
5656
| In-dashboard settings hub | Runtime and display tuning without editing files directly |
57-
| Browser-first OAuth with manual fallback | Works in normal and constrained terminal environments |
57+
| Browser-first OAuth with manual fallback | `codex auth login` stays browser-first, while `--manual`, `--no-browser`, and `CODEX_AUTH_NO_BROWSER=1` keep login usable in browser-restricted shells |
58+
59+
Manual/non-TTY login accepts the full callback URL on stdin, so automation and host-managed shells can complete auth without relying on a local browser handoff.
5860

5961
---
6062

docs/getting-started.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ codex auth list
5656
codex auth check
5757
```
5858

59+
If browser launch is blocked or you want to handle the callback manually:
60+
61+
```bash
62+
codex auth login --manual
63+
CODEX_AUTH_NO_BROWSER=1 codex auth login
64+
```
65+
66+
In non-TTY/manual shells, provide the full redirect URL on stdin:
67+
68+
```bash
69+
echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual
70+
```
71+
72+
`codex auth login` remains browser-first by default. No new npm scripts or storage migration steps are required for this auth-flow update.
73+
5974
---
6075

6176
## Add More Accounts
@@ -111,6 +126,7 @@ If the OAuth callback on port `1455` fails:
111126

112127
- stop the process using port `1455`
113128
- rerun `codex auth login`
129+
- if browser launch is unavailable, rerun `codex auth login --manual`
114130

115131
If account state looks stale:
116132

docs/reference/commands.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,23 @@ Compatibility aliases are supported:
5656

5757
---
5858

59+
## Upgrade Notes
60+
61+
- `codex auth login` remains browser-first by default.
62+
- `codex auth login --manual` and `codex auth login --no-browser` force the manual callback flow instead of launching a browser.
63+
- `CODEX_AUTH_NO_BROWSER=1` suppresses browser launch for automation/headless sessions. False-like values such as `0` and `false` do not disable browser launch by themselves.
64+
- In non-TTY/manual shells, pass the full redirect URL on stdin, for example: `echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual`.
65+
- No new npm scripts or storage migration steps were introduced for this auth-flow update.
66+
67+
---
68+
5969
## Compatibility and Non-TTY Behavior
6070

6171
- `codex` remains the primary wrapper entrypoint. It routes `codex auth ...` and the compatibility aliases to the multi-auth runtime, and forwards every other command to the official `@openai/codex` CLI.
6272
- In non-TTY or host-managed sessions, including `CODEX_TUI=1`, `CODEX_DESKTOP=1`, `TERM_PROGRAM=codex`, or `ELECTRON_RUN_AS_NODE=1`, auth flows degrade to deterministic text behavior.
6373
- The non-TTY fallback keeps `codex auth login` predictable: it defaults to add-account mode, skips the extra "add another account" prompt, and auto-picks the default workspace selection when a follow-up choice is needed.
6474
- `codex auth login --manual` keeps the login flow usable in browser-restricted shells by printing the OAuth URL and accepting manual callback input instead of trying to open a browser.
75+
- In non-TTY/manual shells, provide the full redirect URL on stdin, for example: `echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual`.
6576

6677
---
6778

docs/upgrade.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ codex auth forecast --live --model gpt-5-codex
4949

5050
---
5151

52+
## Login Flow Upgrade Notes
53+
54+
- `codex auth login` remains the default browser-first path.
55+
- `codex auth login --manual` and `codex auth login --no-browser` force manual callback handling for browser-restricted shells.
56+
- `CODEX_AUTH_NO_BROWSER=1` suppresses browser launch for automation/headless sessions. False-like values such as `0` and `false` no longer force manual mode.
57+
- In non-TTY/manual shells, provide the full redirect URL on stdin, for example: `echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual`.
58+
- No new npm scripts, storage migrations, or extra upgrade steps were introduced for this auth-flow change.
59+
60+
For the full command/behavior reference, see [reference/commands.md](reference/commands.md).
61+
62+
---
63+
5264
## Configuration Upgrade Notes
5365

5466
During upgrades, runtime config source precedence is:

lib/auth/browser.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import path from "node:path";
99
import { PLATFORM_OPENERS } from "../constants.js";
1010

1111
const BROWSER_DISABLED_VALUES = new Set(["0", "false", "no", "off", "none"]);
12+
const NO_BROWSER_TRUTHY_VALUES = new Set(["1", "true", "yes", "on"]);
1213

1314
/**
1415
* Gets the platform-specific command to open a URL in the default browser
@@ -23,8 +24,8 @@ export function getBrowserOpener(): string {
2324

2425
export function isBrowserLaunchSuppressed(): boolean {
2526
const explicitNoBrowser = (process.env.CODEX_AUTH_NO_BROWSER ?? "").trim().toLowerCase();
26-
if (explicitNoBrowser === "1" || BROWSER_DISABLED_VALUES.has(explicitNoBrowser)) {
27-
return true;
27+
if (explicitNoBrowser.length > 0) {
28+
return NO_BROWSER_TRUTHY_VALUES.has(explicitNoBrowser);
2829
}
2930

3031
const browserSetting = (process.env.BROWSER ?? "").trim().toLowerCase();

lib/codex-manager.ts

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
summarizeForecast,
3838
type ForecastAccountResult,
3939
} from "./forecast.js";
40+
import { createLogger } from "./logger.js";
4041
import { MODEL_FAMILIES, type ModelFamily } from "./prompts/codex.js";
4142
import {
4243
fetchCodexQuotaSnapshot,
@@ -87,6 +88,8 @@ type TokenSuccessWithAccount = TokenSuccess & {
8788
};
8889
type PromptTone = "accent" | "success" | "warning" | "danger" | "muted";
8990

91+
const log = createLogger("codex-manager");
92+
9093
function stylePromptText(text: string, tone: PromptTone): string {
9194
if (!output.isTTY) return text;
9295
const ui = getUiRuntimeOptions();
@@ -1141,7 +1144,50 @@ async function promptManualCallback(
11411144
console.log("");
11421145
console.log(stylePromptText(UI_COPY.oauth.pastePrompt, "accent"));
11431146
}
1144-
const answer = await rl.question(useInteractivePrompt ? "◆ " : "");
1147+
const answer = useInteractivePrompt
1148+
? await rl.question("◆ ")
1149+
: await new Promise<string | null>((resolve, reject) => {
1150+
if (input.readableEnded || input.destroyed) {
1151+
resolve(null);
1152+
return;
1153+
}
1154+
let settled = false;
1155+
const handleInputClosed = () => {
1156+
if (settled) return;
1157+
settled = true;
1158+
input.off("end", handleInputClosed);
1159+
input.off("close", handleInputClosed);
1160+
resolve(null);
1161+
};
1162+
const finish = (value: string) => {
1163+
if (settled) return;
1164+
settled = true;
1165+
input.off("end", handleInputClosed);
1166+
input.off("close", handleInputClosed);
1167+
resolve(value);
1168+
};
1169+
const fail = (error: unknown) => {
1170+
if (settled) return;
1171+
settled = true;
1172+
input.off("end", handleInputClosed);
1173+
input.off("close", handleInputClosed);
1174+
reject(error);
1175+
};
1176+
rl.question("")
1177+
.then((value) => finish(value))
1178+
.catch((error) => {
1179+
if (isAbortError(error) || isReadlineClosedError(error)) {
1180+
handleInputClosed();
1181+
return;
1182+
}
1183+
fail(error);
1184+
});
1185+
input.once("end", handleInputClosed);
1186+
input.once("close", handleInputClosed);
1187+
});
1188+
if (answer === null) {
1189+
return null;
1190+
}
11451191
if (answer.includes("\u001b")) {
11461192
return null;
11471193
}
@@ -1160,7 +1206,7 @@ async function promptManualCallback(
11601206
if (parsed.state && parsed.state !== state) return null;
11611207
return parsed.code;
11621208
} catch (error) {
1163-
if (isAbortError(error)) {
1209+
if (isAbortError(error) || isReadlineClosedError(error)) {
11641210
return null;
11651211
}
11661212
throw error;
@@ -1169,6 +1215,17 @@ async function promptManualCallback(
11691215
}
11701216
}
11711217

1218+
function isReadlineClosedError(error: unknown): boolean {
1219+
if (!(error instanceof Error)) {
1220+
return false;
1221+
}
1222+
const errorCode =
1223+
typeof error === "object" && error !== null && "code" in error
1224+
? String((error as { code?: unknown }).code)
1225+
: "";
1226+
return errorCode === "ERR_USE_AFTER_CLOSE" || /readline was closed/i.test(error.message);
1227+
}
1228+
11721229
type OAuthSignInMode = "browser" | "manual" | "cancel";
11731230

11741231
async function promptOAuthSignInMode(): Promise<OAuthSignInMode> {
@@ -1468,13 +1525,8 @@ async function runOAuthFlow(
14681525
): Promise<TokenResult> {
14691526
const { pkce, state, url } = await createAuthorizationFlow({ forceNewLogin });
14701527
const preferManualMode = options.manual || isBrowserLaunchSuppressed();
1471-
let oauthServer: Awaited<ReturnType<typeof startLocalOAuthServer>> | null = null;
1472-
try {
1473-
oauthServer = await startLocalOAuthServer({ state });
1474-
} catch {
1475-
oauthServer = null;
1476-
}
14771528
let code: string | null = null;
1529+
let oauthServer: Awaited<ReturnType<typeof startLocalOAuthServer>> | null = null;
14781530
try {
14791531
const signInMode = preferManualMode ? "manual" : await promptOAuthSignInMode();
14801532
if (signInMode === "cancel") {
@@ -1485,6 +1537,29 @@ async function runOAuthFlow(
14851537
};
14861538
}
14871539

1540+
if (signInMode === "browser") {
1541+
try {
1542+
oauthServer = await startLocalOAuthServer({ state });
1543+
} catch (serverError) {
1544+
log.warn(
1545+
"Local OAuth callback server unavailable; falling back to manual callback entry.",
1546+
serverError instanceof Error
1547+
? {
1548+
message: serverError.message,
1549+
stack: serverError.stack,
1550+
code:
1551+
typeof serverError === "object" &&
1552+
serverError !== null &&
1553+
"code" in serverError
1554+
? String(serverError.code)
1555+
: undefined,
1556+
}
1557+
: { error: String(serverError) },
1558+
);
1559+
oauthServer = null;
1560+
}
1561+
}
1562+
14881563
if (signInMode === "browser") {
14891564
const opened = openBrowserUrl(url);
14901565
if (opened) {
@@ -1523,7 +1598,9 @@ async function runOAuthFlow(
15231598
stylePromptText(
15241599
waitingForCallback
15251600
? UI_COPY.oauth.callbackMissed
1526-
: UI_COPY.oauth.callbackUnavailable,
1601+
: signInMode === "manual"
1602+
? UI_COPY.oauth.callbackBypassed
1603+
: UI_COPY.oauth.callbackUnavailable,
15271604
"warning",
15281605
),
15291606
);

0 commit comments

Comments
 (0)