codex-multi-auth 2.7.1 → 2.8.1

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/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 shadowContext = null;
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
- shadowContext?.cleanup?.();
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
- shadowContext = createRuntimeRotationProxyCodexHome(
3848
- process.env,
3849
- proxyServer.baseUrl,
3850
- clientApiKey,
3851
- configTomlModule,
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
- env: pickRuntimeRotationAppHelperEnv(shadowContext.env),
3963
+ args: runtimeContext.args ?? [],
3964
+ env: pickRuntimeRotationAppHelperEnv(runtimeContext.env),
3863
3965
  })}\n`,
3864
3966
  );
3865
3967
 
@@ -3901,6 +4003,9 @@ function waitForRuntimeRotationAppHelperExit(helper, timeoutMs = 2_000) {
3901
4003
  if (settled) return;
3902
4004
  settled = true;
3903
4005
  if (timer) clearTimeout(timer);
4006
+ // Drop the listener when the timeout wins, so a helper that outlives the
4007
+ // graceful window does not keep a live reference back into this process.
4008
+ helper.off?.("close", finish);
3904
4009
  resolve();
3905
4010
  };
3906
4011
  timer = setTimeout(finish, timeoutMs);
@@ -3908,19 +4013,52 @@ function waitForRuntimeRotationAppHelperExit(helper, timeoutMs = 2_000) {
3908
4013
  });
3909
4014
  }
3910
4015
 
3911
- function stopRuntimeRotationAppHelper(helper) {
3912
- if (!helper || helper.killed) {
3913
- return Promise.resolve();
3914
- }
4016
+ function hasRuntimeRotationAppHelperExited(helper) {
4017
+ return helper.exitCode !== null || helper.signalCode !== null;
4018
+ }
4019
+
4020
+ // Releases every handle the helper still holds in this process. The helper is
4021
+ // spawned with piped stdio, so those pipes keep the parent event loop alive on
4022
+ // their own — destroying and unref-ing them is what actually lets `mcodex` return
4023
+ // to the shell when the child ignores or outlives SIGTERM (#647).
4024
+ function releaseRuntimeRotationAppHelperResources(helper) {
4025
+ helper.stdout?.destroy();
4026
+ helper.stderr?.destroy();
3915
4027
  try {
3916
- helper.kill("SIGTERM");
4028
+ helper.unref();
3917
4029
  } catch {
3918
- return Promise.resolve();
4030
+ // Best-effort only.
4031
+ }
4032
+ }
4033
+
4034
+ async function stopRuntimeRotationAppHelper(helper) {
4035
+ if (!helper) {
4036
+ return;
4037
+ }
4038
+ if (hasRuntimeRotationAppHelperExited(helper)) {
4039
+ releaseRuntimeRotationAppHelperResources(helper);
4040
+ return;
4041
+ }
4042
+ if (!helper.killed) {
4043
+ try {
4044
+ helper.kill("SIGTERM");
4045
+ } catch {
4046
+ releaseRuntimeRotationAppHelperResources(helper);
4047
+ return;
4048
+ }
4049
+ }
4050
+ await waitForRuntimeRotationAppHelperExit(helper);
4051
+ if (!hasRuntimeRotationAppHelperExited(helper)) {
4052
+ try {
4053
+ helper.kill("SIGKILL");
4054
+ } catch {
4055
+ // Best-effort force-stop once the graceful shutdown window has elapsed.
4056
+ }
3919
4057
  }
3920
- return waitForRuntimeRotationAppHelperExit(helper);
4058
+ releaseRuntimeRotationAppHelperResources(helper);
3921
4059
  }
3922
4060
 
3923
- function startRuntimeRotationAppHelper(baseContext) {
4061
+ function startRuntimeRotationAppHelper(baseContext, options = {}) {
3924
4062
  const realCodexHome =
3925
4063
  baseContext.originalCodexHome ??
3926
4064
  resolveRuntimeRotationProxyOriginalCodexHome(baseContext.env);
@@ -3940,6 +4078,8 @@ function startRuntimeRotationAppHelper(baseContext) {
3940
4078
  ),
3941
4079
  [APP_RUNTIME_HELPER_OWNER_PID_ENV]: String(process.pid),
3942
4080
  [APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV]: realCodexHome,
4081
+ [APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV]:
4082
+ options.useCanonicalHome === true ? "1" : "0",
3943
4083
  },
3944
4084
  stdio: ["ignore", "pipe", "pipe"],
3945
4085
  detached: true,
@@ -4005,8 +4145,14 @@ async function createRuntimeRotationAppHelperContext(
4005
4145
  options = {},
4006
4146
  ) {
4007
4147
  const startedAt = Date.now();
4008
- const { helper, message } = await startRuntimeRotationAppHelper(baseContext);
4148
+ const { helper, message } = await startRuntimeRotationAppHelper(
4149
+ baseContext,
4150
+ options,
4151
+ );
4009
4152
  const helperEnv = message.env ?? {};
4153
+ const helperArgs = Array.isArray(message.args)
4154
+ ? message.args.filter((arg) => typeof arg === "string")
4155
+ : [];
4010
4156
  const detachGraceMs = resolveRuntimeRotationAppHelperDetachGraceMs(baseContext.env);
4011
4157
 
4012
4158
  const cleanup = async ({ exitCode } = {}) => {
@@ -4023,6 +4169,7 @@ async function createRuntimeRotationAppHelperContext(
4023
4169
  return {
4024
4170
  args: [
4025
4171
  ...baseContext.args,
4172
+ ...helperArgs,
4026
4173
  "-c",
4027
4174
  `model_provider=${configTomlModule.tomlStringLiteral(RUNTIME_ROTATION_PROXY_PROVIDER_ID)}`,
4028
4175
  ],
@@ -4060,9 +4207,13 @@ async function createRuntimeRotationProxyContextIfEnabled(
4060
4207
  if (isCodexAppCommand(rawArgs)) {
4061
4208
  return createRuntimeRotationAppHelperContext(baseContext, configTomlModule);
4062
4209
  }
4063
- if (isCodexInteractiveTuiCommand(rawArgs)) {
4210
+ if (
4211
+ isCodexInteractiveTuiCommand(rawArgs) ||
4212
+ isCodexInteractiveResumeCommand(rawArgs)
4213
+ ) {
4064
4214
  return createRuntimeRotationAppHelperContext(baseContext, configTomlModule, {
4065
4215
  detachOnExit: true,
4216
+ useCanonicalHome: true,
4066
4217
  });
4067
4218
  }
4068
4219
 
@@ -4263,6 +4414,17 @@ function isCodexInteractiveTuiCommand(rawArgs) {
4263
4414
  return findForwardedCommand(rawArgs) === null;
4264
4415
  }
4265
4416
 
4417
+ // `resume` and `fork` are interactive TUI entry points that happen to carry a
4418
+ // forwarded subcommand, so the bare-invocation predicate above misses them. They
4419
+ // must not take the shadow-home transport: the shadow mirror deliberately omits the
4420
+ // runtime SQLite state (`isRuntimeRotationShadowHomeOmittedEntry`), so the requested
4421
+ // thread is absent from the shadow session index and the resumed TUI hangs on a
4422
+ // blank screen (#647).
4423
+ function isCodexInteractiveResumeCommand(rawArgs) {
4424
+ const command = findForwardedCommand(rawArgs)?.command;
4425
+ return command === "resume" || command === "fork";
4426
+ }
4427
+
4266
4428
  function shouldUseRuntimeRoutingForForwardedArgs(rawArgs) {
4267
4429
  if (!Array.isArray(rawArgs) || rawArgs.length === 0) {
4268
4430
  return true;
@@ -4302,7 +4464,17 @@ function shouldUseRuntimeRoutingForForwardedArgs(rawArgs) {
4302
4464
  );
4303
4465
  }
4304
4466
 
4305
- if (command.command === "app" && hasHelpFlagAfterCommand(rawArgs, command.index)) {
4467
+ // Request commands still print their help without making a single model
4468
+ // request, so the help form skips the transport entirely: no proxy, no shadow
4469
+ // home, no detached helper. This matters most for the interactive commands,
4470
+ // which detach their helper on a clean exit — help always exits clean, so
4471
+ // `resume --help` would otherwise strand a helper until its idle timeout. It is
4472
+ // keyed off the help flag rather than the command, so a real run still routes
4473
+ // through rotation, and it matches how `app-server` help is handled above (#647).
4474
+ if (
4475
+ requestCommands.has(command.command) &&
4476
+ hasHelpFlagAfterCommand(rawArgs, command.index)
4477
+ ) {
4306
4478
  return false;
4307
4479
  }
4308
4480
  if (requestCommands.has(command.command)) {
@@ -5045,6 +5217,7 @@ async function main() {
5045
5217
  }
5046
5218
  const forwardArgs = forcedAccount.forwardArgs;
5047
5219
 
5220
+ await ensurePersistedCodexFileAuthStore();
5048
5221
  await autoSyncManagerActiveSelectionIfEnabled();
5049
5222
  await maybePrintForwardStatusLine(forwardArgs);
5050
5223
  maybeRefreshQuotaCacheInBackground();