codex-multi-auth 2.7.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +2 -1
- package/dist/lib/codex-cli/writer.d.ts +51 -0
- package/dist/lib/codex-cli/writer.d.ts.map +1 -1
- package/dist/lib/codex-cli/writer.js +89 -8
- package/dist/lib/codex-manager/repair-commands.d.ts.map +1 -1
- package/dist/lib/codex-manager/repair-commands.js +57 -8
- package/dist/lib/runtime/first-run.d.ts +12 -6
- package/dist/lib/runtime/first-run.d.ts.map +1 -1
- package/dist/lib/runtime/first-run.js +140 -9
- package/package.json +1 -1
- package/scripts/codex.js +126 -13
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.",
|
|
5
5
|
"interface": {
|
|
6
6
|
"composerIcon": "./assets/codex-multi-auth-icon.svg"
|
package/README.md
CHANGED
|
@@ -391,7 +391,8 @@ codex-multi-auth doctor --json
|
|
|
391
391
|
|
|
392
392
|
## Release Notes
|
|
393
393
|
|
|
394
|
-
- Current stable: [docs/releases/v2.
|
|
394
|
+
- Current stable: [docs/releases/v2.8.0.md](docs/releases/v2.8.0.md) — install via `npm i -g codex-multi-auth`
|
|
395
|
+
- Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
|
|
395
396
|
- Previous stable: [docs/releases/v2.7.0.md](docs/releases/v2.7.0.md)
|
|
396
397
|
- Previous stable: [docs/releases/v2.6.0.md](docs/releases/v2.6.0.md)
|
|
397
398
|
- Previous stable: [docs/releases/v2.5.0.md](docs/releases/v2.5.0.md)
|
|
@@ -6,6 +6,57 @@ interface ActiveSelection {
|
|
|
6
6
|
expiresAt?: number;
|
|
7
7
|
idToken?: string;
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Whether `config.toml` may be rewritten to pin the credential store.
|
|
11
|
+
*
|
|
12
|
+
* Exported so callers that only *plan* a rewrite — `doctor --fix --dry-run` —
|
|
13
|
+
* can report the same outcome the real run would produce, instead of promising
|
|
14
|
+
* a change the opt-out would suppress.
|
|
15
|
+
*/
|
|
16
|
+
export declare function shouldEnforceCodexCliFileAuthStore(): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Reads the top-level `cli_auth_credentials_store` value out of a raw
|
|
19
|
+
* `config.toml`.
|
|
20
|
+
*
|
|
21
|
+
* Mirrors the scoping rules of {@link ensureCodexCliFileAuthStore}: assignments
|
|
22
|
+
* inside a `[table]` belong to that table, so only lines before the first table
|
|
23
|
+
* header describe the official CLI's default credential store. Reporting a
|
|
24
|
+
* profile-scoped value as the effective one would let `doctor` call a config
|
|
25
|
+
* healthy while the CLI still reaches for the keychain.
|
|
26
|
+
*
|
|
27
|
+
* Both TOML string forms count. A literal string (`'file'`) is as valid as a
|
|
28
|
+
* basic string (`"file"`), so treating only the latter as real would make
|
|
29
|
+
* `doctor` warn about a perfectly healthy config. Literal strings have no
|
|
30
|
+
* escape sequences, and the values in play here are bare identifiers, so a
|
|
31
|
+
* non-greedy character class is an exact match for both forms.
|
|
32
|
+
*
|
|
33
|
+
* @param rawConfig - Full text of `config.toml`.
|
|
34
|
+
* @returns The declared mode, or `null` when no top-level assignment exists.
|
|
35
|
+
*/
|
|
36
|
+
export declare function readTopLevelCodexCliAuthStoreMode(rawConfig: string): string | null;
|
|
37
|
+
/**
|
|
38
|
+
* Persists `cli_auth_credentials_store = "file"` as a top-level key in the
|
|
39
|
+
* official Codex CLI `config.toml`.
|
|
40
|
+
*
|
|
41
|
+
* The wrapper already appends `-c cli_auth_credentials_store="file"` to every
|
|
42
|
+
* command it forwards, but that only covers processes it launches itself.
|
|
43
|
+
* Third-party front-ends (menu-bar apps such as CodexBar, editor extensions)
|
|
44
|
+
* exec the official binary directly, so the *persisted* value is what decides
|
|
45
|
+
* whether the official CLI reaches into the macOS login keychain. Leaving it on
|
|
46
|
+
* `"keychain"` is what produces the repeated "Always Allow" prompts reported in
|
|
47
|
+
* issue #641.
|
|
48
|
+
*
|
|
49
|
+
* Only top-level assignments are rewritten. A `cli_auth_credentials_store`
|
|
50
|
+
* inside a `[profiles.*]` table must be left alone: rewriting it in place would
|
|
51
|
+
* mangle a scoped setting while still leaving the top-level default pointed at
|
|
52
|
+
* the keychain, so the prompts would continue.
|
|
53
|
+
*
|
|
54
|
+
* @param configPath - Config file to reconcile; defaults to the resolved Codex
|
|
55
|
+
* CLI `config.toml`.
|
|
56
|
+
* @returns `true` when the file was rewritten, `false` when it was already
|
|
57
|
+
* correct or enforcement is disabled.
|
|
58
|
+
*/
|
|
59
|
+
export declare function ensureCodexCliFileAuthStore(configPath?: string): Promise<boolean>;
|
|
9
60
|
export declare function setCodexCliActiveSelection(selection: ActiveSelection): Promise<boolean>;
|
|
10
61
|
export declare function getLastCodexCliSelectionWriteTimestamp(): number;
|
|
11
62
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../../lib/codex-cli/writer.ts"],"names":[],"mappings":"AAoBA,UAAU,eAAe;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;
|
|
1
|
+
{"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../../lib/codex-cli/writer.ts"],"names":[],"mappings":"AAoBA,UAAU,eAAe;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AA8ND;;;;;;GAMG;AACH,wBAAgB,kCAAkC,IAAI,OAAO,CAG5D;AAiBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iCAAiC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASlF;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,2BAA2B,CAChD,UAAU,GAAE,MAAgC,GAC1C,OAAO,CAAC,OAAO,CAAC,CAgDlB;AAmHD,wBAAsB,0BAA0B,CAC/C,SAAS,EAAE,eAAe,GACxB,OAAO,CAAC,OAAO,CAAC,CAqMlB;AAED,wBAAgB,sCAAsC,IAAI,MAAM,CAE/D"}
|
|
@@ -199,30 +199,111 @@ async function atomicWriteText(path, content) {
|
|
|
199
199
|
async function atomicWriteJson(path, payload) {
|
|
200
200
|
return atomicWriteText(path, JSON.stringify(payload, null, 2));
|
|
201
201
|
}
|
|
202
|
-
|
|
202
|
+
/**
|
|
203
|
+
* Whether `config.toml` may be rewritten to pin the credential store.
|
|
204
|
+
*
|
|
205
|
+
* Exported so callers that only *plan* a rewrite — `doctor --fix --dry-run` —
|
|
206
|
+
* can report the same outcome the real run would produce, instead of promising
|
|
207
|
+
* a change the opt-out would suppress.
|
|
208
|
+
*/
|
|
209
|
+
export function shouldEnforceCodexCliFileAuthStore() {
|
|
203
210
|
const override = (process.env.CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE ?? "1").trim();
|
|
204
211
|
return override !== "0";
|
|
205
212
|
}
|
|
206
|
-
|
|
207
|
-
|
|
213
|
+
/**
|
|
214
|
+
* Matches a complete TOML table header, mirroring `readTomlTableName` in
|
|
215
|
+
* `lib/runtime/config-toml.ts` (plus an optional trailing comment).
|
|
216
|
+
*
|
|
217
|
+
* A loose `/^\s*\[/` would also match a continuation line of a multi-line array
|
|
218
|
+
* such as `[1, 2],`, cutting the top-level scan short. The writer would then
|
|
219
|
+
* miss a real top-level assignment below it and insert a second one — a
|
|
220
|
+
* duplicate key, which is invalid TOML and stops the official CLI from starting.
|
|
221
|
+
*/
|
|
222
|
+
const TOML_TABLE_HEADER_PATTERN = /^\s*\[{1,2}\s*[^\]]+?\s*\]{1,2}\s*(?:#.*)?$/;
|
|
223
|
+
function ensureTrailingNewline(value, lineEnding = "\n") {
|
|
224
|
+
return value.endsWith("\n") ? value : `${value}${lineEnding}`;
|
|
208
225
|
}
|
|
209
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Reads the top-level `cli_auth_credentials_store` value out of a raw
|
|
228
|
+
* `config.toml`.
|
|
229
|
+
*
|
|
230
|
+
* Mirrors the scoping rules of {@link ensureCodexCliFileAuthStore}: assignments
|
|
231
|
+
* inside a `[table]` belong to that table, so only lines before the first table
|
|
232
|
+
* header describe the official CLI's default credential store. Reporting a
|
|
233
|
+
* profile-scoped value as the effective one would let `doctor` call a config
|
|
234
|
+
* healthy while the CLI still reaches for the keychain.
|
|
235
|
+
*
|
|
236
|
+
* Both TOML string forms count. A literal string (`'file'`) is as valid as a
|
|
237
|
+
* basic string (`"file"`), so treating only the latter as real would make
|
|
238
|
+
* `doctor` warn about a perfectly healthy config. Literal strings have no
|
|
239
|
+
* escape sequences, and the values in play here are bare identifiers, so a
|
|
240
|
+
* non-greedy character class is an exact match for both forms.
|
|
241
|
+
*
|
|
242
|
+
* @param rawConfig - Full text of `config.toml`.
|
|
243
|
+
* @returns The declared mode, or `null` when no top-level assignment exists.
|
|
244
|
+
*/
|
|
245
|
+
export function readTopLevelCodexCliAuthStoreMode(rawConfig) {
|
|
246
|
+
for (const line of rawConfig.split(/\r?\n/)) {
|
|
247
|
+
if (TOML_TABLE_HEADER_PATTERN.test(line))
|
|
248
|
+
break;
|
|
249
|
+
const match = line.match(/^\s*cli_auth_credentials_store\s*=\s*(?:"([^"]*)"|'([^']*)')\s*(?:#.*)?$/);
|
|
250
|
+
if (match)
|
|
251
|
+
return (match[1] ?? match[2] ?? "").trim() || null;
|
|
252
|
+
}
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Persists `cli_auth_credentials_store = "file"` as a top-level key in the
|
|
257
|
+
* official Codex CLI `config.toml`.
|
|
258
|
+
*
|
|
259
|
+
* The wrapper already appends `-c cli_auth_credentials_store="file"` to every
|
|
260
|
+
* command it forwards, but that only covers processes it launches itself.
|
|
261
|
+
* Third-party front-ends (menu-bar apps such as CodexBar, editor extensions)
|
|
262
|
+
* exec the official binary directly, so the *persisted* value is what decides
|
|
263
|
+
* whether the official CLI reaches into the macOS login keychain. Leaving it on
|
|
264
|
+
* `"keychain"` is what produces the repeated "Always Allow" prompts reported in
|
|
265
|
+
* issue #641.
|
|
266
|
+
*
|
|
267
|
+
* Only top-level assignments are rewritten. A `cli_auth_credentials_store`
|
|
268
|
+
* inside a `[profiles.*]` table must be left alone: rewriting it in place would
|
|
269
|
+
* mangle a scoped setting while still leaving the top-level default pointed at
|
|
270
|
+
* the keychain, so the prompts would continue.
|
|
271
|
+
*
|
|
272
|
+
* @param configPath - Config file to reconcile; defaults to the resolved Codex
|
|
273
|
+
* CLI `config.toml`.
|
|
274
|
+
* @returns `true` when the file was rewritten, `false` when it was already
|
|
275
|
+
* correct or enforcement is disabled.
|
|
276
|
+
*/
|
|
277
|
+
export async function ensureCodexCliFileAuthStore(configPath = getCodexCliConfigPath()) {
|
|
210
278
|
if (!shouldEnforceCodexCliFileAuthStore())
|
|
211
279
|
return false;
|
|
212
280
|
const desired = 'cli_auth_credentials_store = "file"';
|
|
213
281
|
const raw = existsSync(configPath) ? await fs.readFile(configPath, "utf-8") : "";
|
|
282
|
+
// Quote style is not ours to normalize: a literal-string `'file'` already
|
|
283
|
+
// keeps the CLI off the keychain, so rewriting it to `"file"` would churn a
|
|
284
|
+
// healthy config on every wrapper startup for no behavioral gain.
|
|
285
|
+
if (readTopLevelCodexCliAuthStoreMode(raw) === "file")
|
|
286
|
+
return false;
|
|
287
|
+
// Preserve the file's existing line endings, as the runtime config rewriter
|
|
288
|
+
// does. Splitting on /\r?\n/ and rejoining with "\n" would silently convert a
|
|
289
|
+
// CRLF config.toml to LF -- an edit the user never asked for, and one this
|
|
290
|
+
// code now risks on every wrapper startup rather than only on account switch.
|
|
291
|
+
const lineEnding = raw.includes("\r\n") ? "\r\n" : "\n";
|
|
214
292
|
const lines = raw.length > 0 ? raw.split(/\r?\n/) : [];
|
|
215
293
|
const assignmentRegex = /^\s*cli_auth_credentials_store\s*=/;
|
|
216
294
|
let replaced = false;
|
|
295
|
+
let inTopLevelTable = true;
|
|
217
296
|
const nextLines = lines.map((line) => {
|
|
218
|
-
if (
|
|
297
|
+
if (TOML_TABLE_HEADER_PATTERN.test(line))
|
|
298
|
+
inTopLevelTable = false;
|
|
299
|
+
if (inTopLevelTable && !replaced && assignmentRegex.test(line)) {
|
|
219
300
|
replaced = true;
|
|
220
301
|
return desired;
|
|
221
302
|
}
|
|
222
303
|
return line;
|
|
223
304
|
});
|
|
224
305
|
if (!replaced) {
|
|
225
|
-
let insertAt = nextLines.findIndex((line) =>
|
|
306
|
+
let insertAt = nextLines.findIndex((line) => TOML_TABLE_HEADER_PATTERN.test(line));
|
|
226
307
|
if (insertAt < 0)
|
|
227
308
|
insertAt = nextLines.length;
|
|
228
309
|
nextLines.splice(insertAt, 0, desired);
|
|
@@ -230,8 +311,8 @@ async function ensureCodexCliFileAuthStore(configPath) {
|
|
|
230
311
|
nextLines.splice(insertAt + 1, 0, "");
|
|
231
312
|
}
|
|
232
313
|
}
|
|
233
|
-
const nextRaw = ensureTrailingNewline(nextLines.join(
|
|
234
|
-
if (nextRaw === ensureTrailingNewline(raw)) {
|
|
314
|
+
const nextRaw = ensureTrailingNewline(nextLines.join(lineEnding), lineEnding);
|
|
315
|
+
if (nextRaw === ensureTrailingNewline(raw, lineEnding)) {
|
|
235
316
|
return false;
|
|
236
317
|
}
|
|
237
318
|
await atomicWriteText(configPath, nextRaw);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"repair-commands.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/repair-commands.ts"],"names":[],"mappings":"AAcA,OAAO,EAAkC,KAAK,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxF,OAAO,EACN,uBAAuB,EAEvB,MAAM,mBAAmB,CAAC;AAG3B,OAAO,EASN,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EAGrB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"repair-commands.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/repair-commands.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,7 +9,7 @@ import { isCodexUnavailableError } from "../errors.js";
|
|
|
9
9
|
import { queuedRefresh } from "../refresh-queue.js";
|
|
10
10
|
import { findMatchingAccountIndex, getStoragePath, loadAccounts, loadFlaggedAccounts, setStoragePath, withAccountStorageTransaction, withAccountAndFlaggedStorageTransaction, withFlaggedStorageTransaction, } from "../storage.js";
|
|
11
11
|
import { getCodexCliAuthPath, getCodexCliConfigPath, loadCodexCliState, } from "../codex-cli/state.js";
|
|
12
|
-
import { setCodexCliActiveSelection } from "../codex-cli/writer.js";
|
|
12
|
+
import { ensureCodexCliFileAuthStore, readTopLevelCodexCliAuthStoreMode, setCodexCliActiveSelection, shouldEnforceCodexCliFileAuthStore, } from "../codex-cli/writer.js";
|
|
13
13
|
import { MODEL_FAMILIES } from "../prompts/codex.js";
|
|
14
14
|
import { DEFAULT_PROBE_MODEL, resolveNormalizedModel } from "../request/helpers/model-map.js";
|
|
15
15
|
import { loadPersistedRuntimeObservabilitySnapshot } from "../runtime/runtime-observability.js";
|
|
@@ -1300,14 +1300,16 @@ export async function runDoctor(args, deps) {
|
|
|
1300
1300
|
: "Codex config file does not exist",
|
|
1301
1301
|
details: codexConfigPath,
|
|
1302
1302
|
});
|
|
1303
|
+
const supplementalFixActions = [];
|
|
1304
|
+
// Tracked separately from account storage: pinning the auth store is a real
|
|
1305
|
+
// remediation even on a machine with no accounts yet, which is exactly the
|
|
1306
|
+
// state a user hitting repeated keychain prompts tends to be in.
|
|
1307
|
+
let authStoreFixChanged = false;
|
|
1303
1308
|
let codexAuthStoreMode;
|
|
1304
1309
|
if (codexConfigFileExists) {
|
|
1305
1310
|
try {
|
|
1306
1311
|
const configRaw = await fs.readFile(codexConfigPath, "utf-8");
|
|
1307
|
-
|
|
1308
|
-
if (match?.[1]) {
|
|
1309
|
-
codexAuthStoreMode = match[1].trim();
|
|
1310
|
-
}
|
|
1312
|
+
codexAuthStoreMode = readTopLevelCodexCliAuthStoreMode(configRaw) ?? undefined;
|
|
1311
1313
|
}
|
|
1312
1314
|
catch (error) {
|
|
1313
1315
|
addCheck({
|
|
@@ -1319,13 +1321,59 @@ export async function runDoctor(args, deps) {
|
|
|
1319
1321
|
}
|
|
1320
1322
|
}
|
|
1321
1323
|
if (!checks.some((check) => check.key === "codex-auth-store")) {
|
|
1324
|
+
// A config that is not pinned to "file" is what makes the official CLI
|
|
1325
|
+
// reach into the macOS login keychain, so `--fix` repairs it directly
|
|
1326
|
+
// instead of waiting for the incidental `pendingCodexActiveSync` path
|
|
1327
|
+
// below, which only fires when there is an active account to mirror.
|
|
1328
|
+
if (options.fix &&
|
|
1329
|
+
codexAuthStoreMode !== "file" &&
|
|
1330
|
+
shouldEnforceCodexCliFileAuthStore()) {
|
|
1331
|
+
if (options.dryRun) {
|
|
1332
|
+
authStoreFixChanged = true;
|
|
1333
|
+
supplementalFixActions.push({
|
|
1334
|
+
key: "codex-auth-store",
|
|
1335
|
+
message: "Prepared Codex CLI credential store pin to file in config.toml (dry-run)",
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
else {
|
|
1339
|
+
try {
|
|
1340
|
+
if (await ensureCodexCliFileAuthStore(codexConfigPath)) {
|
|
1341
|
+
codexAuthStoreMode = "file";
|
|
1342
|
+
authStoreFixChanged = true;
|
|
1343
|
+
supplementalFixActions.push({
|
|
1344
|
+
key: "codex-auth-store",
|
|
1345
|
+
message: "Pinned Codex CLI credential store to file in config.toml",
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
catch (error) {
|
|
1350
|
+
addCheck({
|
|
1351
|
+
key: "codex-auth-store-fix",
|
|
1352
|
+
severity: "warn",
|
|
1353
|
+
message: "Failed to pin Codex CLI credential store to file",
|
|
1354
|
+
details: error instanceof Error ? error.message : String(error),
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const authStoreDetails = [
|
|
1360
|
+
codexAuthStoreMode ? `mode=${codexAuthStoreMode}` : "mode=unset",
|
|
1361
|
+
];
|
|
1362
|
+
if (codexAuthStoreMode !== "file" && !options.fix) {
|
|
1363
|
+
authStoreDetails.push("run `codex-multi-auth doctor --fix` to pin it to file");
|
|
1364
|
+
}
|
|
1365
|
+
if (process.platform === "darwin" && codexAuthStoreMode === "file") {
|
|
1366
|
+
// Purely informational: reading or deleting keychain items would
|
|
1367
|
+
// itself raise the prompt this check exists to eliminate.
|
|
1368
|
+
authStoreDetails.push("credentials saved by an earlier official `codex login` may still sit in the login keychain; they are no longer read and can be removed manually via Keychain Access");
|
|
1369
|
+
}
|
|
1322
1370
|
addCheck({
|
|
1323
1371
|
key: "codex-auth-store",
|
|
1324
1372
|
severity: codexAuthStoreMode === "file" ? "ok" : "warn",
|
|
1325
1373
|
message: codexAuthStoreMode === "file"
|
|
1326
1374
|
? "Codex auth storage is set to file"
|
|
1327
1375
|
: "Codex auth storage is not explicitly set to file",
|
|
1328
|
-
details:
|
|
1376
|
+
details: authStoreDetails.join("; "),
|
|
1329
1377
|
});
|
|
1330
1378
|
}
|
|
1331
1379
|
const codexCliState = await loadCodexCliState({ forceRefresh: true });
|
|
@@ -1343,7 +1391,6 @@ export async function runDoctor(args, deps) {
|
|
|
1343
1391
|
let fixChanged = false;
|
|
1344
1392
|
let storageFixChanged = false;
|
|
1345
1393
|
let structuralFixActions = [];
|
|
1346
|
-
const supplementalFixActions = [];
|
|
1347
1394
|
let doctorRefreshMutation = null;
|
|
1348
1395
|
let pendingCodexActiveSync = null;
|
|
1349
1396
|
if (options.fix && storageForChecks && storageForChecks.accounts.length > 0) {
|
|
@@ -1677,7 +1724,9 @@ export async function runDoctor(args, deps) {
|
|
|
1677
1724
|
}
|
|
1678
1725
|
}
|
|
1679
1726
|
const fixActions = [...structuralFixActions, ...supplementalFixActions];
|
|
1680
|
-
if (options.fix &&
|
|
1727
|
+
if (options.fix &&
|
|
1728
|
+
((storageForChecks && storageForChecks.accounts.length > 0) ||
|
|
1729
|
+
authStoreFixChanged)) {
|
|
1681
1730
|
fixChanged = storageFixChanged || fixActions.length > 0;
|
|
1682
1731
|
addCheck({
|
|
1683
1732
|
key: "auto-fix",
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
export declare const FIRST_RUN_MARKER_VERSION =
|
|
2
|
-
type FirstRunStepStatus = "completed" | "skipped" | "failed";
|
|
1
|
+
export declare const FIRST_RUN_MARKER_VERSION = 2;
|
|
2
|
+
export type FirstRunStepStatus = "completed" | "skipped" | "failed";
|
|
3
3
|
interface FirstRunSetupOutcome {
|
|
4
4
|
appBind: FirstRunStepStatus;
|
|
5
5
|
launcher: FirstRunStepStatus;
|
|
6
|
+
authStore: FirstRunStepStatus;
|
|
6
7
|
}
|
|
7
8
|
type FirstRunSkipReason = "ci" | "not-installed" | "already-done" | "claim-race" | "error";
|
|
8
9
|
export type FirstRunResult = {
|
|
9
10
|
ran: false;
|
|
10
11
|
reason: FirstRunSkipReason;
|
|
12
|
+
} | {
|
|
13
|
+
ran: false;
|
|
14
|
+
reason: "migrated";
|
|
15
|
+
authStore: FirstRunStepStatus;
|
|
11
16
|
} | ({
|
|
12
17
|
ran: true;
|
|
13
18
|
} & FirstRunSetupOutcome);
|
|
@@ -19,6 +24,7 @@ export interface FirstRunSetupDeps {
|
|
|
19
24
|
resolveRotation?: () => boolean;
|
|
20
25
|
bindCodexApp?: () => Promise<FirstRunStepStatus>;
|
|
21
26
|
installLauncher?: () => Promise<FirstRunStepStatus>;
|
|
27
|
+
enforceAuthStore?: () => Promise<FirstRunStepStatus>;
|
|
22
28
|
notify?: (message: string) => void;
|
|
23
29
|
now?: () => number;
|
|
24
30
|
}
|
|
@@ -68,10 +74,10 @@ type LauncherInstallFn = (options: {
|
|
|
68
74
|
*/
|
|
69
75
|
export declare function loadLauncherInstall(candidateOverride?: readonly string[]): Promise<LauncherInstallFn | null>;
|
|
70
76
|
/**
|
|
71
|
-
* Runs the lazily deferred install setup (Codex
|
|
72
|
-
* exactly once per runtime root. Never throws and
|
|
73
|
-
* failure: every error path resolves with a
|
|
74
|
-
* debug-logs sanitized messages.
|
|
77
|
+
* Runs the lazily deferred install setup (Codex auth-store enforcement + app
|
|
78
|
+
* bind + launcher routing) exactly once per runtime root. Never throws and
|
|
79
|
+
* never blocks a command on failure: every error path resolves with a
|
|
80
|
+
* skip/failed status and only debug-logs sanitized messages.
|
|
75
81
|
*/
|
|
76
82
|
export declare function ensureFirstRunSetup(deps?: FirstRunSetupDeps): Promise<FirstRunResult>;
|
|
77
83
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"first-run.d.ts","sourceRoot":"","sources":["../../../lib/runtime/first-run.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"first-run.d.ts","sourceRoot":"","sources":["../../../lib/runtime/first-run.ts"],"names":[],"mappings":"AAmCA,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAkB1C,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEpE,UAAU,oBAAoB;IAC7B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,EAAE,kBAAkB,CAAC;IAC7B,SAAS,EAAE,kBAAkB,CAAC;CAC9B;AAED,KAAK,kBAAkB,GACpB,IAAI,GACJ,eAAe,GACf,cAAc,GACd,YAAY,GACZ,OAAO,CAAC;AAEX,MAAM,MAAM,cAAc,GACvB;IAAE,GAAG,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC1C;IAAE,GAAG,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,UAAU,CAAC;IAAC,SAAS,EAAE,kBAAkB,CAAA;CAAE,GACjE,CAAC;IAAE,GAAG,EAAE,IAAI,CAAA;CAAE,GAAG,oBAAoB,CAAC,CAAC;AAW1C,MAAM,WAAW,iBAAiB;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;IACjC,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACjD,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpD,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACrD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAgBD,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAG7E;AAeD,MAAM,WAAW,0BAA0B;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CACjC,OAAO,GAAE,0BAA+B,GACtC,OAAO,CA8BT;AAED,wBAAgB,sBAAsB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,kBAAkB,GAAE,MAAM,OACgC,GACxD,OAAO,CAWT;AAED,MAAM,WAAW,uBAAuB;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,uBAAuB,GAC9B,OAAO,CAcT;AAED,MAAM,WAAW,2BAA2B;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;CACzB;AAED,wBAAgB,uCAAuC,CACtD,OAAO,EAAE,2BAA2B,GAClC,OAAO,CAUT;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACxC,UAAU,GAAE,MAAuC,EACnD,GAAG,GAAE,MAAsB,GACzB,OAAO,CAST;AAsGD,KAAK,iBAAiB,GAAG,CAAC,OAAO,EAAE;IAClC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEvB;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACxC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,GACnC,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAenC;AAuJD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACxC,IAAI,GAAE,iBAAsB,GAC1B,OAAO,CAAC,cAAc,CAAC,CA8EzB"}
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
// a failure is debug-logged (messages only — never tokens or emails) and the
|
|
10
10
|
// command proceeds normally.
|
|
11
11
|
import { randomBytes } from "node:crypto";
|
|
12
|
-
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
|
|
13
13
|
import { open, rename, unlink } from "node:fs/promises";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
16
16
|
import process from "node:process";
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
18
|
+
import { ensureCodexCliFileAuthStore } from "../codex-cli/writer.js";
|
|
18
19
|
import { getCodexRuntimeRotationProxy, loadPluginConfig } from "../config.js";
|
|
19
20
|
import { withFileOperationRetry } from "../fs-retry.js";
|
|
20
21
|
import { createLogger } from "../logger.js";
|
|
@@ -22,7 +23,8 @@ import { getCodexMultiAuthDir } from "../runtime-paths.js";
|
|
|
22
23
|
import { bindCodexAppRuntimeRotation, getAppBindStatus } from "./app-bind.js";
|
|
23
24
|
const log = createLogger("first-run");
|
|
24
25
|
const FIRST_RUN_MARKER_FILE = "first-run-setup.json";
|
|
25
|
-
|
|
26
|
+
// v2 adds the `authStore` step to the marker payload.
|
|
27
|
+
export const FIRST_RUN_MARKER_VERSION = 2;
|
|
26
28
|
const TRUE_VALUES = new Set(["1", "true", "yes"]);
|
|
27
29
|
const FALSE_VALUES = new Set(["0", "false", "no"]);
|
|
28
30
|
const CI_ENV_KEYS = [
|
|
@@ -202,12 +204,16 @@ async function atomicWriteMarker(target, content) {
|
|
|
202
204
|
});
|
|
203
205
|
}
|
|
204
206
|
async function finalizeFirstRunMarker(markerPath, startedAt, outcome, now) {
|
|
207
|
+
// A failed auth-store step records a pre-v2 version so the next invocation
|
|
208
|
+
// replays it through the migration path — which reruns only that step, never
|
|
209
|
+
// app bind or launcher install.
|
|
205
210
|
const payload = `${JSON.stringify({
|
|
206
|
-
version:
|
|
211
|
+
version: markerVersionFor(outcome.authStore, undefined),
|
|
207
212
|
startedAt,
|
|
208
213
|
completedAt: now(),
|
|
209
214
|
appBind: outcome.appBind,
|
|
210
215
|
launcher: outcome.launcher,
|
|
216
|
+
authStore: outcome.authStore,
|
|
211
217
|
}, null, "\t")}\n`;
|
|
212
218
|
try {
|
|
213
219
|
await atomicWriteMarker(markerPath, payload);
|
|
@@ -254,6 +260,110 @@ async function defaultBindCodexApp(env, rotationEnabled, detectDesktopApp, notif
|
|
|
254
260
|
}
|
|
255
261
|
return "completed";
|
|
256
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Pins the official Codex CLI to the file-backed credential store.
|
|
265
|
+
*
|
|
266
|
+
* Enforcement used to be lazy — it only happened when something called
|
|
267
|
+
* `setCodexCliActiveSelection` (switch, login, health check, repair). A user who
|
|
268
|
+
* installed the package and drove Codex through a third-party front-end could
|
|
269
|
+
* therefore stay on `cli_auth_credentials_store = "keychain"` indefinitely and
|
|
270
|
+
* keep getting macOS login-keychain prompts (issue #641). Doing it at first run
|
|
271
|
+
* closes that window before the official CLI ever reads its store.
|
|
272
|
+
*
|
|
273
|
+
* Deliberately not gated on `isCodexCliSyncEnabled()`: turning CLI sync off is a
|
|
274
|
+
* statement about account mirroring, not a request to re-enable keychain
|
|
275
|
+
* prompts. The `CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE=0` opt-out is
|
|
276
|
+
* honored inside `ensureCodexCliFileAuthStore` itself.
|
|
277
|
+
*/
|
|
278
|
+
async function defaultEnforceAuthStore() {
|
|
279
|
+
return (await ensureCodexCliFileAuthStore()) ? "completed" : "skipped";
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Decides the version to persist after an auth-store attempt.
|
|
283
|
+
*
|
|
284
|
+
* Recording the current version after a *failed* step would strand the user:
|
|
285
|
+
* `ensureFirstRunSetup` short-circuits on version alone, so a single transient
|
|
286
|
+
* Windows `EPERM`/`EBUSY` on a locked `config.toml` would leave the CLI on
|
|
287
|
+
* keychain mode permanently — the exact symptom this whole change exists to
|
|
288
|
+
* remove. Staying pre-v2 costs one extra marker read per invocation and
|
|
289
|
+
* self-heals as soon as the write succeeds, because the step is idempotent.
|
|
290
|
+
*
|
|
291
|
+
* `"skipped"` deliberately does bump. It is the normal outcome for a config
|
|
292
|
+
* already pinned to `"file"`, so treating it as unfinished would mean the
|
|
293
|
+
* marker never reaches the current version for healthy installs. It also covers
|
|
294
|
+
* an explicit `CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE=0` opt-out — a user
|
|
295
|
+
* choice, not a failure — and if that opt-out is later removed, the
|
|
296
|
+
* wrapper-startup guard and `doctor --fix` both still reconcile the config.
|
|
297
|
+
*/
|
|
298
|
+
function markerVersionFor(authStore, previousVersion) {
|
|
299
|
+
if (authStore === "failed")
|
|
300
|
+
return previousVersion ?? 1;
|
|
301
|
+
return FIRST_RUN_MARKER_VERSION;
|
|
302
|
+
}
|
|
303
|
+
function readFirstRunMarker(markerPath) {
|
|
304
|
+
try {
|
|
305
|
+
const parsed = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
306
|
+
return typeof parsed === "object" && parsed !== null
|
|
307
|
+
? parsed
|
|
308
|
+
: null;
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// An unreadable or truncated marker is treated as pre-v2 and migrated;
|
|
312
|
+
// the migration only replays the idempotent auth-store step.
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Brings a marker written before the auth-store step existed up to the current
|
|
318
|
+
* version.
|
|
319
|
+
*
|
|
320
|
+
* `ensureFirstRunSetup` short-circuits on marker existence, so bumping
|
|
321
|
+
* `FIRST_RUN_MARKER_VERSION` on its own would leave every already-installed
|
|
322
|
+
* user — precisely the population reporting repeated keychain prompts — without
|
|
323
|
+
* the enforcement step forever.
|
|
324
|
+
*
|
|
325
|
+
* Only the auth-store step is replayed. Rerunning app bind or launcher install
|
|
326
|
+
* would reinstall shortcuts and rebind the desktop app, which the user may have
|
|
327
|
+
* deliberately removed since.
|
|
328
|
+
*
|
|
329
|
+
* No exclusive claim is taken, unlike the initial setup. Both operations here
|
|
330
|
+
* are idempotent and atomic — `ensureCodexCliFileAuthStore` skips a config
|
|
331
|
+
* already on `"file"`, and `atomicWriteMarker` renames into place — so
|
|
332
|
+
* concurrent invocations converge on the same result instead of racing.
|
|
333
|
+
*/
|
|
334
|
+
async function migrateFirstRunMarker(markerPath, marker, deps, now) {
|
|
335
|
+
let authStore;
|
|
336
|
+
try {
|
|
337
|
+
authStore = await (deps.enforceAuthStore
|
|
338
|
+
? deps.enforceAuthStore()
|
|
339
|
+
: defaultEnforceAuthStore());
|
|
340
|
+
}
|
|
341
|
+
catch (error) {
|
|
342
|
+
authStore = "failed";
|
|
343
|
+
log.debug("first-run marker migration auth store step skipped", {
|
|
344
|
+
error: errorMessage(error),
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
const payload = `${JSON.stringify({
|
|
348
|
+
version: markerVersionFor(authStore, marker.version),
|
|
349
|
+
startedAt: marker.startedAt ?? now(),
|
|
350
|
+
completedAt: marker.completedAt ?? now(),
|
|
351
|
+
appBind: marker.appBind ?? "skipped",
|
|
352
|
+
launcher: marker.launcher ?? "skipped",
|
|
353
|
+
authStore,
|
|
354
|
+
}, null, "\t")}\n`;
|
|
355
|
+
try {
|
|
356
|
+
await atomicWriteMarker(markerPath, payload);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
// Leaving the marker at v1 just means the idempotent step runs again on
|
|
360
|
+
// the next invocation, so this is safe to swallow.
|
|
361
|
+
log.debug("first-run marker migration write failed", {
|
|
362
|
+
error: errorMessage(error),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
return authStore;
|
|
366
|
+
}
|
|
257
367
|
async function defaultInstallLauncher(env, rotationEnabled, notify) {
|
|
258
368
|
if (!shouldInstallCodexAppLauncherOnFirstRun({ env, rotationEnabled })) {
|
|
259
369
|
return "skipped";
|
|
@@ -265,10 +375,10 @@ async function defaultInstallLauncher(env, rotationEnabled, notify) {
|
|
|
265
375
|
return "completed";
|
|
266
376
|
}
|
|
267
377
|
/**
|
|
268
|
-
* Runs the lazily deferred install setup (Codex
|
|
269
|
-
* exactly once per runtime root. Never throws and
|
|
270
|
-
* failure: every error path resolves with a
|
|
271
|
-
* debug-logs sanitized messages.
|
|
378
|
+
* Runs the lazily deferred install setup (Codex auth-store enforcement + app
|
|
379
|
+
* bind + launcher routing) exactly once per runtime root. Never throws and
|
|
380
|
+
* never blocks a command on failure: every error path resolves with a
|
|
381
|
+
* skip/failed status and only debug-logs sanitized messages.
|
|
272
382
|
*/
|
|
273
383
|
export async function ensureFirstRunSetup(deps = {}) {
|
|
274
384
|
try {
|
|
@@ -279,9 +389,18 @@ export async function ensureFirstRunSetup(deps = {}) {
|
|
|
279
389
|
if (!installed)
|
|
280
390
|
return { ran: false, reason: "not-installed" };
|
|
281
391
|
const markerPath = deps.markerPath ?? getFirstRunMarkerPath();
|
|
282
|
-
if (existsSync(markerPath))
|
|
283
|
-
return { ran: false, reason: "already-done" };
|
|
284
392
|
const now = deps.now ?? Date.now;
|
|
393
|
+
if (existsSync(markerPath)) {
|
|
394
|
+
const marker = readFirstRunMarker(markerPath);
|
|
395
|
+
if ((marker?.version ?? 1) >= FIRST_RUN_MARKER_VERSION) {
|
|
396
|
+
return { ran: false, reason: "already-done" };
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
ran: false,
|
|
400
|
+
reason: "migrated",
|
|
401
|
+
authStore: await migrateFirstRunMarker(markerPath, marker ?? {}, deps, now),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
285
404
|
const startedAt = now();
|
|
286
405
|
if (!claimFirstRunMarker(markerPath, startedAt)) {
|
|
287
406
|
return { ran: false, reason: "claim-race" };
|
|
@@ -296,7 +415,19 @@ export async function ensureFirstRunSetup(deps = {}) {
|
|
|
296
415
|
const outcome = {
|
|
297
416
|
appBind: "skipped",
|
|
298
417
|
launcher: "skipped",
|
|
418
|
+
authStore: "skipped",
|
|
299
419
|
};
|
|
420
|
+
try {
|
|
421
|
+
outcome.authStore = await (deps.enforceAuthStore
|
|
422
|
+
? deps.enforceAuthStore()
|
|
423
|
+
: defaultEnforceAuthStore());
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
outcome.authStore = "failed";
|
|
427
|
+
log.debug("first-run codex auth store enforcement skipped", {
|
|
428
|
+
error: errorMessage(error),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
300
431
|
try {
|
|
301
432
|
outcome.appBind = await (deps.bindCodexApp
|
|
302
433
|
? deps.bindCodexApp()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Codex CLI multi-account OAuth manager with account switching, health checks, runtime rotation, diagnostics, and recovery tools for @openai/codex",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
package/scripts/codex.js
CHANGED
|
@@ -74,6 +74,10 @@ const APP_RUNTIME_HELPER_OWNER_PID_ENV =
|
|
|
74
74
|
"CODEX_MULTI_AUTH_APP_ROTATION_OWNER_PID";
|
|
75
75
|
const APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV =
|
|
76
76
|
"CODEX_MULTI_AUTH_REAL_CODEX_HOME";
|
|
77
|
+
const APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV =
|
|
78
|
+
"CODEX_MULTI_AUTH_APP_ROTATION_USE_CANONICAL_HOME";
|
|
79
|
+
const APP_SERVER_CONFIG_ARGS_ENV =
|
|
80
|
+
"CODEX_MULTI_AUTH_APP_SERVER_CONFIG_ARGS_JSON";
|
|
77
81
|
const APP_RUNTIME_HELPER_STATUS_FILE =
|
|
78
82
|
RUNTIME_CONSTANTS.APP_RUNTIME_HELPER_STATUS_FILE;
|
|
79
83
|
const DEFAULT_APP_RUNTIME_HELPER_IDLE_MS = 12 * 60 * 60 * 1000;
|
|
@@ -1083,6 +1087,34 @@ async function loadRuntimeConfigTomlModule() {
|
|
|
1083
1087
|
}
|
|
1084
1088
|
}
|
|
1085
1089
|
|
|
1090
|
+
// Reconciles the persisted `cli_auth_credentials_store` in ~/.codex/config.toml
|
|
1091
|
+
// to "file" before forwarding.
|
|
1092
|
+
//
|
|
1093
|
+
// `buildForwardArgs` already appends `-c cli_auth_credentials_store="file"`, but
|
|
1094
|
+
// that only covers the process this wrapper spawns. Third-party front-ends
|
|
1095
|
+
// (CodexBar and friends) exec the official binary directly and read config.toml
|
|
1096
|
+
// instead, so a config still pointing at "keychain" keeps producing macOS login
|
|
1097
|
+
// keychain prompts (issue #641). Running the reconcile here self-heals a config
|
|
1098
|
+
// that something else flipped back — e.g. a fresh official `codex login`.
|
|
1099
|
+
//
|
|
1100
|
+
// Idempotent: the lib helper returns without writing when the value is already
|
|
1101
|
+
// "file", so this adds no write amplification on the common path.
|
|
1102
|
+
async function ensurePersistedCodexFileAuthStore() {
|
|
1103
|
+
const forceFileAuthStore = (process.env.CODEX_MULTI_AUTH_FORCE_FILE_AUTH_STORE ?? "1").trim() !== "0";
|
|
1104
|
+
if (!forceFileAuthStore) return;
|
|
1105
|
+
|
|
1106
|
+
try {
|
|
1107
|
+
const mod = await import("../dist/lib/codex-cli/writer.js");
|
|
1108
|
+
if (typeof mod.ensureCodexCliFileAuthStore !== "function") return;
|
|
1109
|
+
await mod.ensureCodexCliFileAuthStore();
|
|
1110
|
+
} catch (error) {
|
|
1111
|
+
if (isModuleNotFoundError(error)) return;
|
|
1112
|
+
// Best effort only. A read-only or locked config.toml (Windows
|
|
1113
|
+
// EPERM/EBUSY) must never block the wrapped command: the per-invocation
|
|
1114
|
+
// `-c` override still protects this run.
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1086
1118
|
async function autoSyncManagerActiveSelectionIfEnabled() {
|
|
1087
1119
|
const enabled = (process.env.CODEX_MULTI_AUTH_AUTO_SYNC_ON_STARTUP ?? "1").trim() !== "0";
|
|
1088
1120
|
if (!enabled) return;
|
|
@@ -3490,6 +3522,38 @@ function createRuntimeRotationProxyCodexHome(
|
|
|
3490
3522
|
};
|
|
3491
3523
|
}
|
|
3492
3524
|
|
|
3525
|
+
function createRuntimeRotationProxyCanonicalCodexHome(
|
|
3526
|
+
baseEnv,
|
|
3527
|
+
proxyBaseUrl,
|
|
3528
|
+
clientApiKey,
|
|
3529
|
+
configTomlModule,
|
|
3530
|
+
) {
|
|
3531
|
+
const originalCodexHome = resolveRuntimeRotationProxyOriginalCodexHome(baseEnv);
|
|
3532
|
+
const providerTable = `model_providers.${RUNTIME_ROTATION_PROXY_PROVIDER_ID}`;
|
|
3533
|
+
const configArgs = [
|
|
3534
|
+
`${providerTable}.name=${configTomlModule.tomlStringLiteral(APP_SERVER_ACCOUNT_DISPLAY_NAME)}`,
|
|
3535
|
+
`${providerTable}.base_url=${configTomlModule.tomlStringLiteral(proxyBaseUrl)}`,
|
|
3536
|
+
`${providerTable}.env_key=${configTomlModule.tomlStringLiteral("OPENAI_API_KEY")}`,
|
|
3537
|
+
`${providerTable}.requires_openai_auth=false`,
|
|
3538
|
+
`${providerTable}.wire_api=${configTomlModule.tomlStringLiteral("responses")}`,
|
|
3539
|
+
"disable_response_storage=false",
|
|
3540
|
+
].flatMap((assignment) => ["-c", assignment]);
|
|
3541
|
+
|
|
3542
|
+
return {
|
|
3543
|
+
args: configArgs,
|
|
3544
|
+
env: {
|
|
3545
|
+
...baseEnv,
|
|
3546
|
+
CODEX_HOME: originalCodexHome,
|
|
3547
|
+
OPENAI_API_KEY: clientApiKey,
|
|
3548
|
+
CODEX_MULTI_AUTH_DIR: resolveRuntimeRotationOriginalMultiAuthDir(
|
|
3549
|
+
originalCodexHome,
|
|
3550
|
+
baseEnv,
|
|
3551
|
+
),
|
|
3552
|
+
},
|
|
3553
|
+
cleanup: () => {},
|
|
3554
|
+
};
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3493
3557
|
function appendNodeImportOption(nodeOptions, preloadPath) {
|
|
3494
3558
|
const importOption = `--import=${pathToFileURL(preloadPath).href}`;
|
|
3495
3559
|
const trimmed = (nodeOptions ?? "").trim();
|
|
@@ -3504,15 +3568,26 @@ function createRuntimeRotationAppServerPreloadSource(wrapperScriptPath) {
|
|
|
3504
3568
|
"",
|
|
3505
3569
|
`const wrapperScriptPath = ${JSON.stringify(wrapperScriptPath)};`,
|
|
3506
3570
|
`const accountLabelEnv = ${JSON.stringify(APP_SERVER_ACCOUNT_LABEL_ENV)};`,
|
|
3571
|
+
`const configArgsEnv = ${JSON.stringify(APP_SERVER_CONFIG_ARGS_ENV)};`,
|
|
3572
|
+
"let configArgs = [];",
|
|
3573
|
+
"try {",
|
|
3574
|
+
' const parsed = JSON.parse(process.env[configArgsEnv] ?? "[]");',
|
|
3575
|
+
" if (Array.isArray(parsed)) {",
|
|
3576
|
+
' configArgs = parsed.filter((arg) => typeof arg === "string");',
|
|
3577
|
+
" }",
|
|
3578
|
+
"} catch {",
|
|
3579
|
+
" // Ignore malformed internal metadata and preserve app-server startup.",
|
|
3580
|
+
"}",
|
|
3507
3581
|
"const rawArgs = process.argv.slice(1);",
|
|
3508
3582
|
"const firstArg = rawArgs[0] ?? \"\";",
|
|
3509
3583
|
'if (basename(firstArg).toLowerCase() === "app-server") {',
|
|
3510
|
-
' const args = ["app-server", ...rawArgs.slice(1)];',
|
|
3584
|
+
' const args = ["app-server", ...rawArgs.slice(1), ...configArgs];',
|
|
3511
3585
|
" const env = {",
|
|
3512
3586
|
" ...process.env,",
|
|
3513
3587
|
' CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY: "0",',
|
|
3514
3588
|
" [accountLabelEnv]: \"1\",",
|
|
3515
3589
|
" };",
|
|
3590
|
+
" delete env[configArgsEnv];",
|
|
3516
3591
|
" const child = spawn(process.execPath, [wrapperScriptPath, ...args], {",
|
|
3517
3592
|
" stdio: \"inherit\",",
|
|
3518
3593
|
" env,",
|
|
@@ -3558,7 +3633,7 @@ function sweepStaleRuntimeRotationAppServerShimDirs(shimRootDir) {
|
|
|
3558
3633
|
}
|
|
3559
3634
|
}
|
|
3560
3635
|
|
|
3561
|
-
function installRuntimeRotationAppServerCliShim(forwardedEnv) {
|
|
3636
|
+
function installRuntimeRotationAppServerCliShim(forwardedEnv, configArgs = []) {
|
|
3562
3637
|
const shadowCodexHome = forwardedEnv.CODEX_HOME;
|
|
3563
3638
|
if (!shadowCodexHome) {
|
|
3564
3639
|
throw new Error("runtime app-server shim requires CODEX_HOME");
|
|
@@ -3615,6 +3690,11 @@ function installRuntimeRotationAppServerCliShim(forwardedEnv) {
|
|
|
3615
3690
|
);
|
|
3616
3691
|
forwardedEnv.CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY = "0";
|
|
3617
3692
|
forwardedEnv[APP_SERVER_ACCOUNT_LABEL_ENV] = "1";
|
|
3693
|
+
if (configArgs.length > 0) {
|
|
3694
|
+
forwardedEnv[APP_SERVER_CONFIG_ARGS_ENV] = JSON.stringify(configArgs);
|
|
3695
|
+
} else {
|
|
3696
|
+
delete forwardedEnv[APP_SERVER_CONFIG_ARGS_ENV];
|
|
3697
|
+
}
|
|
3618
3698
|
return shimDir;
|
|
3619
3699
|
}
|
|
3620
3700
|
|
|
@@ -3715,6 +3795,7 @@ function pickRuntimeRotationAppHelperEnv(env) {
|
|
|
3715
3795
|
"CODEX_MULTI_AUTH_REAL_CODEX_BIN",
|
|
3716
3796
|
"CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY",
|
|
3717
3797
|
APP_SERVER_ACCOUNT_LABEL_ENV,
|
|
3798
|
+
APP_SERVER_CONFIG_ARGS_ENV,
|
|
3718
3799
|
]) {
|
|
3719
3800
|
if (env[name]) {
|
|
3720
3801
|
picked[name] = env[name];
|
|
@@ -3776,7 +3857,7 @@ function createRuntimeRotationAppHelperStatus({
|
|
|
3776
3857
|
|
|
3777
3858
|
async function runRuntimeRotationAppHelper() {
|
|
3778
3859
|
let proxyServer = null;
|
|
3779
|
-
let
|
|
3860
|
+
let runtimeContext = null;
|
|
3780
3861
|
let appServerShimDir = null;
|
|
3781
3862
|
let statusTimer = null;
|
|
3782
3863
|
let closing = false;
|
|
@@ -3813,7 +3894,7 @@ async function runRuntimeRotationAppHelper() {
|
|
|
3813
3894
|
}
|
|
3814
3895
|
appServerShimDir = null;
|
|
3815
3896
|
}
|
|
3816
|
-
|
|
3897
|
+
runtimeContext?.cleanup?.();
|
|
3817
3898
|
} finally {
|
|
3818
3899
|
try {
|
|
3819
3900
|
await proxyServer?.close?.();
|
|
@@ -3844,13 +3925,33 @@ async function runRuntimeRotationAppHelper() {
|
|
|
3844
3925
|
}
|
|
3845
3926
|
const clientApiKey = createRuntimeRotationProxyClientApiKey();
|
|
3846
3927
|
proxyServer = await proxyModule.startRuntimeRotationProxy({ clientApiKey });
|
|
3847
|
-
|
|
3848
|
-
process.env
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3928
|
+
const useCanonicalHome =
|
|
3929
|
+
(process.env[APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV] ?? "").trim() ===
|
|
3930
|
+
"1";
|
|
3931
|
+
runtimeContext = useCanonicalHome
|
|
3932
|
+
? createRuntimeRotationProxyCanonicalCodexHome(
|
|
3933
|
+
process.env,
|
|
3934
|
+
proxyServer.baseUrl,
|
|
3935
|
+
clientApiKey,
|
|
3936
|
+
configTomlModule,
|
|
3937
|
+
)
|
|
3938
|
+
: createRuntimeRotationProxyCodexHome(
|
|
3939
|
+
process.env,
|
|
3940
|
+
proxyServer.baseUrl,
|
|
3941
|
+
clientApiKey,
|
|
3942
|
+
configTomlModule,
|
|
3943
|
+
);
|
|
3944
|
+
const appServerConfigArgs = useCanonicalHome
|
|
3945
|
+
? [
|
|
3946
|
+
...(runtimeContext.args ?? []),
|
|
3947
|
+
"-c",
|
|
3948
|
+
`model_provider=${configTomlModule.tomlStringLiteral(RUNTIME_ROTATION_PROXY_PROVIDER_ID)}`,
|
|
3949
|
+
]
|
|
3950
|
+
: [];
|
|
3951
|
+
appServerShimDir = installRuntimeRotationAppServerCliShim(
|
|
3952
|
+
runtimeContext.env,
|
|
3953
|
+
appServerConfigArgs,
|
|
3852
3954
|
);
|
|
3853
|
-
appServerShimDir = installRuntimeRotationAppServerCliShim(shadowContext.env);
|
|
3854
3955
|
lastRequestCount = proxyServer.getStatus?.().totalRequests ?? 0;
|
|
3855
3956
|
publishStatus("running");
|
|
3856
3957
|
process.stdout.write(
|
|
@@ -3859,7 +3960,8 @@ async function runRuntimeRotationAppHelper() {
|
|
|
3859
3960
|
pid: process.pid,
|
|
3860
3961
|
baseUrl: proxyServer.baseUrl,
|
|
3861
3962
|
statusPath: resolveRuntimeRotationAppHelperStatusPath(),
|
|
3862
|
-
|
|
3963
|
+
args: runtimeContext.args ?? [],
|
|
3964
|
+
env: pickRuntimeRotationAppHelperEnv(runtimeContext.env),
|
|
3863
3965
|
})}\n`,
|
|
3864
3966
|
);
|
|
3865
3967
|
|
|
@@ -3920,7 +4022,7 @@ function stopRuntimeRotationAppHelper(helper) {
|
|
|
3920
4022
|
return waitForRuntimeRotationAppHelperExit(helper);
|
|
3921
4023
|
}
|
|
3922
4024
|
|
|
3923
|
-
function startRuntimeRotationAppHelper(baseContext) {
|
|
4025
|
+
function startRuntimeRotationAppHelper(baseContext, options = {}) {
|
|
3924
4026
|
const realCodexHome =
|
|
3925
4027
|
baseContext.originalCodexHome ??
|
|
3926
4028
|
resolveRuntimeRotationProxyOriginalCodexHome(baseContext.env);
|
|
@@ -3940,6 +4042,8 @@ function startRuntimeRotationAppHelper(baseContext) {
|
|
|
3940
4042
|
),
|
|
3941
4043
|
[APP_RUNTIME_HELPER_OWNER_PID_ENV]: String(process.pid),
|
|
3942
4044
|
[APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV]: realCodexHome,
|
|
4045
|
+
[APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV]:
|
|
4046
|
+
options.useCanonicalHome === true ? "1" : "0",
|
|
3943
4047
|
},
|
|
3944
4048
|
stdio: ["ignore", "pipe", "pipe"],
|
|
3945
4049
|
detached: true,
|
|
@@ -4005,8 +4109,14 @@ async function createRuntimeRotationAppHelperContext(
|
|
|
4005
4109
|
options = {},
|
|
4006
4110
|
) {
|
|
4007
4111
|
const startedAt = Date.now();
|
|
4008
|
-
const { helper, message } = await startRuntimeRotationAppHelper(
|
|
4112
|
+
const { helper, message } = await startRuntimeRotationAppHelper(
|
|
4113
|
+
baseContext,
|
|
4114
|
+
options,
|
|
4115
|
+
);
|
|
4009
4116
|
const helperEnv = message.env ?? {};
|
|
4117
|
+
const helperArgs = Array.isArray(message.args)
|
|
4118
|
+
? message.args.filter((arg) => typeof arg === "string")
|
|
4119
|
+
: [];
|
|
4010
4120
|
const detachGraceMs = resolveRuntimeRotationAppHelperDetachGraceMs(baseContext.env);
|
|
4011
4121
|
|
|
4012
4122
|
const cleanup = async ({ exitCode } = {}) => {
|
|
@@ -4023,6 +4133,7 @@ async function createRuntimeRotationAppHelperContext(
|
|
|
4023
4133
|
return {
|
|
4024
4134
|
args: [
|
|
4025
4135
|
...baseContext.args,
|
|
4136
|
+
...helperArgs,
|
|
4026
4137
|
"-c",
|
|
4027
4138
|
`model_provider=${configTomlModule.tomlStringLiteral(RUNTIME_ROTATION_PROXY_PROVIDER_ID)}`,
|
|
4028
4139
|
],
|
|
@@ -4063,6 +4174,7 @@ async function createRuntimeRotationProxyContextIfEnabled(
|
|
|
4063
4174
|
if (isCodexInteractiveTuiCommand(rawArgs)) {
|
|
4064
4175
|
return createRuntimeRotationAppHelperContext(baseContext, configTomlModule, {
|
|
4065
4176
|
detachOnExit: true,
|
|
4177
|
+
useCanonicalHome: true,
|
|
4066
4178
|
});
|
|
4067
4179
|
}
|
|
4068
4180
|
|
|
@@ -5045,6 +5157,7 @@ async function main() {
|
|
|
5045
5157
|
}
|
|
5046
5158
|
const forwardArgs = forcedAccount.forwardArgs;
|
|
5047
5159
|
|
|
5160
|
+
await ensurePersistedCodexFileAuthStore();
|
|
5048
5161
|
await autoSyncManagerActiveSelectionIfEnabled();
|
|
5049
5162
|
await maybePrintForwardStatusLine(forwardArgs);
|
|
5050
5163
|
maybeRefreshQuotaCacheInBackground();
|