Skip to content

Commit d36b04f

Browse files
ndycodecodex
andcommitted
feat: harden multi-auth update resilience and add tui version
- harden Windows shim recovery across codex.bat/codex.cmd/codex.ps1 and profile guard paths - improve synthetic-account backup promotion and codex auth token-shape persistence - add dashboard version label and release notes through v0.1.7 - validated with lint, typecheck, full test suite, and build Co-authored-by: Codex <noreply@openai.com>
1 parent 46510fe commit d36b04f

22 files changed

Lines changed: 1503 additions & 449 deletions

docs/releases/v0.1.6.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Release v0.1.6
2+
3+
Release date: 2026-03-03
4+
Channel: `latest`
5+
6+
## Highlights
7+
8+
- Fixed reset-on-update behavior by improving multi-auth runtime path selection when account storage is available only through recovery artifacts.
9+
- Added backup discovery recovery so non-standard backup files can restore `openai-codex-accounts.json` automatically.
10+
- Aligned Codex CLI sync default paths with `CODEX_HOME` to prevent auth writes from going to a different profile directory.
11+
- Hardened switch-sync reporting so account switches fail fast when required Codex auth persistence does not complete.
12+
13+
## Install
14+
15+
```bash
16+
npm i -g @openai/codex
17+
npm i -g codex-multi-auth
18+
```
19+
20+
## Core Operations
21+
22+
```bash
23+
codex auth login
24+
codex auth list
25+
codex auth switch 2
26+
codex auth status
27+
codex auth check
28+
codex auth forecast --live
29+
```
30+
31+
## Validation Snapshot
32+
33+
Release gate commands:
34+
35+
- `npm run lint`
36+
- `npm run typecheck`
37+
- `npm run build`
38+
- `npm test -- test/runtime-paths.test.ts test/storage-recovery-paths.test.ts test/codex-cli-state.test.ts`
39+
40+
## Merged PRs
41+
42+
- Follow-up patch release for storage durability and Codex sync path alignment.
43+
44+
## Commits
45+
46+
- Included in release tag `v0.1.6`.
47+
48+
## Notes
49+
50+
- Multi-auth now treats backup/WAL signals as valid storage indicators during runtime directory selection.
51+
- Codex auth sync now targets the same `CODEX_HOME` root used by the active Codex installation.
52+
- Canonical runtime paths remain under `~/.codex/multi-auth` unless overridden by environment.
53+
54+
## Related
55+
56+
- [../getting-started.md](../getting-started.md)
57+
- [../upgrade.md](../upgrade.md)
58+
- [../reference/commands.md](../reference/commands.md)

docs/releases/v0.1.7.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Release v0.1.7
2+
3+
Release date: 2026-03-03
4+
Channel: `latest`
5+
6+
## Highlights
7+
8+
- Hardened Windows global command routing so multi-auth survives Codex npm shim takeovers across `codex.bat`, `codex.cmd`, and `codex.ps1`.
9+
- Added stock-shim signature replacement and invocation-path-first shim resolution to avoid false PATH matches and stale launcher routing.
10+
- Added PowerShell profile guard installation so new PowerShell sessions keep resolving `codex` to the multi-auth wrapper.
11+
- Strengthened account recovery by auto-promoting discovered real backups when primary storage is synthetic fixture data (including missing `accountId` fixture rows).
12+
- Hardened Codex auth sync writes by enriching active account payloads with complete token shape (`access_token`, `refresh_token`, `id_token`) to prevent malformed auth fallbacks.
13+
- Added visible dashboard version label in TUI header (`Accounts Dashboard (vX.Y.Z)`).
14+
15+
## Install
16+
17+
```bash
18+
npm i -g @openai/codex
19+
npm i -g codex-multi-auth
20+
```
21+
22+
## Core Operations
23+
24+
```bash
25+
codex auth login
26+
codex auth list
27+
codex auth switch 2
28+
codex auth status
29+
codex auth check
30+
codex auth forecast --live
31+
```
32+
33+
## Validation Snapshot
34+
35+
Release gate commands:
36+
37+
- `npm run lint`
38+
- `npm run typecheck`
39+
- `npm run build`
40+
- `npm test`
41+
42+
Broad validation result:
43+
44+
- `100/100` test files passed
45+
- `2328/2328` tests passed
46+
47+
## Merged PRs
48+
49+
- Release hardening rollup for Windows shim resilience, storage recovery promotion, and TUI version visibility.
50+
51+
## Commits
52+
53+
- Included in release tag `v0.1.7`.
54+
55+
## Notes
56+
57+
- Existing account storage is auto-recovered from real backup artifacts when synthetic fixture resets are detected.
58+
- Windows command shims now self-heal from known stock Codex shim replacements during normal command execution.
59+
- TUI now shows the active package version directly in the dashboard title for fast runtime verification.
60+
61+
## Related
62+
63+
- [../getting-started.md](../getting-started.md)
64+
- [../upgrade.md](../upgrade.md)
65+
- [../reference/commands.md](../reference/commands.md)

lib/codex-cli/state.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { existsSync, promises as fs } from "node:fs";
2-
import { homedir } from "node:os";
32
import { join } from "node:path";
43
import { decodeJWT } from "../auth/auth.js";
54
import { extractAccountEmail, extractAccountId } from "../auth/token-utils.js";
65
import { createLogger } from "../logger.js";
6+
import { getCodexHomeDir } from "../runtime-paths.js";
77
import { sleep } from "../utils.js";
88
import {
99
incrementCodexCliMetric,
@@ -210,7 +210,7 @@ export function isCodexCliSyncEnabled(): boolean {
210210
export function getCodexCliAccountsPath(): string {
211211
const override = (process.env.CODEX_CLI_ACCOUNTS_PATH ?? "").trim();
212212
if (override.length > 0) return override;
213-
return join(homedir(), ".codex", "accounts.json");
213+
return join(getCodexHomeDir(), "accounts.json");
214214
}
215215

216216
/**
@@ -226,7 +226,7 @@ export function getCodexCliAccountsPath(): string {
226226
export function getCodexCliAuthPath(): string {
227227
const override = (process.env.CODEX_CLI_AUTH_PATH ?? "").trim();
228228
if (override.length > 0) return override;
229-
return join(homedir(), ".codex", "auth.json");
229+
return join(getCodexHomeDir(), "auth.json");
230230
}
231231

232232
/**
@@ -240,7 +240,7 @@ export function getCodexCliAuthPath(): string {
240240
export function getCodexCliConfigPath(): string {
241241
const override = (process.env.CODEX_CLI_CONFIG_PATH ?? "").trim();
242242
if (override.length > 0) return override;
243-
return join(homedir(), ".codex", "config.toml");
243+
return join(getCodexHomeDir(), "config.toml");
244244
}
245245

246246
/**

lib/codex-cli/writer.ts

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,73 @@ function extractSelectionFromAccountRecord(record: Record<string, unknown>): Act
108108
};
109109
}
110110

111+
function enrichActiveAccountRecordWithTokens(
112+
record: Record<string, unknown>,
113+
selection: ActiveSelection,
114+
): Record<string, unknown> {
115+
const auth = isRecord(record.auth) ? { ...record.auth } : {};
116+
const tokens = isRecord(auth.tokens) ? { ...auth.tokens } : {};
117+
118+
const currentAccess =
119+
extractTokenFromRecord(record, ["accessToken", "access_token"]) ??
120+
extractTokenFromRecord(tokens, ["access_token", "accessToken"]);
121+
const currentRefresh =
122+
extractTokenFromRecord(record, ["refreshToken", "refresh_token"]) ??
123+
extractTokenFromRecord(tokens, ["refresh_token", "refreshToken"]);
124+
const currentIdToken =
125+
extractTokenFromRecord(record, ["idToken", "id_token"]) ??
126+
extractTokenFromRecord(tokens, ["id_token", "idToken"]);
127+
128+
const selectedAccess = readTrimmedString(selection.accessToken);
129+
const selectedRefresh = readTrimmedString(selection.refreshToken);
130+
const selectedIdToken = readTrimmedString(selection.idToken);
131+
const accountId = selection.accountId?.trim() || readAccountId(record);
132+
const email = normalizeEmail(selection.email) ?? normalizeEmail(record.email);
133+
134+
const accessToken = selectedAccess ?? currentAccess;
135+
const refreshToken = selectedRefresh ?? currentRefresh;
136+
if (!accessToken || !refreshToken) {
137+
return record;
138+
}
139+
140+
const idToken = selectedIdToken ?? currentIdToken ?? accessToken;
141+
const next = { ...record } as Record<string, unknown>;
142+
143+
next.accessToken = accessToken;
144+
next.access_token = accessToken;
145+
next.refreshToken = refreshToken;
146+
next.refresh_token = refreshToken;
147+
if (idToken) {
148+
next.idToken = idToken;
149+
next.id_token = idToken;
150+
}
151+
if (accountId) {
152+
next.accountId = accountId;
153+
next.account_id = accountId;
154+
}
155+
if (email) {
156+
next.email = email;
157+
}
158+
159+
const nextTokens = { ...tokens } as Record<string, unknown>;
160+
nextTokens.access_token = accessToken;
161+
nextTokens.accessToken = accessToken;
162+
nextTokens.refresh_token = refreshToken;
163+
nextTokens.refreshToken = refreshToken;
164+
if (idToken) {
165+
nextTokens.id_token = idToken;
166+
nextTokens.idToken = idToken;
167+
}
168+
if (accountId) {
169+
nextTokens.account_id = accountId;
170+
nextTokens.accountId = accountId;
171+
}
172+
auth.tokens = nextTokens;
173+
next.auth = auth;
174+
175+
return next;
176+
}
177+
111178
function resolveMatchIndex(
112179
accounts: unknown[],
113180
selection: ActiveSelection,
@@ -353,6 +420,7 @@ export async function setCodexCliActiveSelection(
353420
selection.accessToken.trim().length > 0 &&
354421
typeof selection.refreshToken === "string" &&
355422
selection.refreshToken.trim().length > 0;
423+
const requireAuthWrite = hasAuthPath || selectionHasTokens;
356424

357425
if (!hasAccountsPath && !hasAuthPath && !selectionHasTokens) {
358426
incrementCodexCliMetric("writeFailures");
@@ -435,7 +503,10 @@ export async function setCodexCliActiveSelection(
435503

436504
next.accounts = parsed.accounts.map((entry, index) => {
437505
if (!isRecord(entry)) return entry;
438-
const updated = { ...entry };
506+
let updated = { ...entry } as Record<string, unknown>;
507+
if (index === matchIndex) {
508+
updated = enrichActiveAccountRecordWithTokens(updated, resolvedSelection);
509+
}
439510
updated.active = index === matchIndex;
440511
updated.isActive = index === matchIndex;
441512
updated.is_active = index === matchIndex;
@@ -463,10 +534,6 @@ export async function setCodexCliActiveSelection(
463534
if (hasAuthPath || wroteAccounts || selectionHasTokens) {
464535
wroteAuth = await writeCodexAuthState(authPath, resolvedSelection);
465536
if (!wroteAuth) {
466-
if (!wroteAccounts) {
467-
incrementCodexCliMetric("writeFailures");
468-
return false;
469-
}
470537
log.warn("Codex auth state update skipped after accounts selection update", {
471538
operation: "write-active-selection",
472539
outcome: "accounts-updated-auth-failed",
@@ -476,6 +543,10 @@ export async function setCodexCliActiveSelection(
476543
email: resolvedSelection.email,
477544
}),
478545
});
546+
if (requireAuthWrite || !wroteAccounts) {
547+
incrementCodexCliMetric("writeFailures");
548+
return false;
549+
}
479550
} else {
480551
log.debug("Persisted Codex auth active selection", {
481552
operation: "write-active-selection",

0 commit comments

Comments
 (0)