codex-multi-auth 2.8.3 → 2.8.4

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.8.3",
3
+ "version": "2.8.4",
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
@@ -392,7 +392,8 @@ codex-multi-auth doctor --json
392
392
 
393
393
  ## Release Notes
394
394
 
395
- - Current stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md) — install via `npm i -g codex-multi-auth`
395
+ - Current stable: [docs/releases/v2.8.4.md](docs/releases/v2.8.4.md) — install via `npm i -g codex-multi-auth`
396
+ - Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
396
397
  - Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
397
398
  - Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
398
399
  - Previous stable: [docs/releases/v2.7.0.md](docs/releases/v2.7.0.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.8.3",
3
+ "version": "2.8.4",
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
@@ -76,6 +76,8 @@ const APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV =
76
76
  "CODEX_MULTI_AUTH_REAL_CODEX_HOME";
77
77
  const APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV =
78
78
  "CODEX_MULTI_AUTH_APP_ROTATION_USE_CANONICAL_HOME";
79
+ const APP_RUNTIME_HELPER_INSTALL_APP_SERVER_SHIM_ENV =
80
+ "CODEX_MULTI_AUTH_APP_ROTATION_INSTALL_APP_SERVER_SHIM";
79
81
  const APP_SERVER_CONFIG_ARGS_ENV =
80
82
  "CODEX_MULTI_AUTH_APP_SERVER_CONFIG_ARGS_JSON";
81
83
  const APP_RUNTIME_HELPER_STATUS_FILE =
@@ -1769,6 +1771,10 @@ async function forwardToRealCodex(codexBin, rawArgs, baseEnv = process.env) {
1769
1771
  compatibility,
1770
1772
  rawArgs,
1771
1773
  );
1774
+ if (runtimeProxyContext.startupError) {
1775
+ console.error(runtimeProxyContext.startupError);
1776
+ return 1;
1777
+ }
1772
1778
  const result = await forwardToRealCodexOnce(
1773
1779
  codexBin,
1774
1780
  runtimeProxyContext.args,
@@ -4058,17 +4064,30 @@ async function runRuntimeRotationAppHelper(identityToken = "") {
4058
4064
  clientApiKey,
4059
4065
  configTomlModule,
4060
4066
  );
4061
- const appServerConfigArgs = useCanonicalHome
4062
- ? [
4063
- ...(runtimeContext.args ?? []),
4064
- "-c",
4065
- `model_provider=${configTomlModule.tomlStringLiteral(RUNTIME_ROTATION_PROXY_PROVIDER_ID)}`,
4066
- ]
4067
- : [];
4068
- appServerShimDir = installRuntimeRotationAppServerCliShim(
4069
- runtimeContext.env,
4070
- appServerConfigArgs,
4071
- );
4067
+ // The shim only exists so a Codex process that spawns its own
4068
+ // `codex app-server` through `CODEX_CLI_PATH` — the desktop app — gets that
4069
+ // spawn routed back through the wrapper. Installing it is not free and not
4070
+ // inert: it copies the node image into a per-pid directory and stamps
4071
+ // `CODEX_CLI_PATH`, `NODE_OPTIONS` and `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0`
4072
+ // onto the environment every forwarded child inherits. A wrapper-invoked
4073
+ // `app-server` is already launched with the rotation overrides on its own
4074
+ // command line, so it must not pay for or inherit any of that (#659).
4075
+ const installAppServerShim =
4076
+ (process.env[APP_RUNTIME_HELPER_INSTALL_APP_SERVER_SHIM_ENV] ?? "1").trim() !==
4077
+ "0";
4078
+ if (installAppServerShim) {
4079
+ const appServerConfigArgs = useCanonicalHome
4080
+ ? [
4081
+ ...(runtimeContext.args ?? []),
4082
+ "-c",
4083
+ `model_provider=${configTomlModule.tomlStringLiteral(RUNTIME_ROTATION_PROXY_PROVIDER_ID)}`,
4084
+ ]
4085
+ : [];
4086
+ appServerShimDir = installRuntimeRotationAppServerCliShim(
4087
+ runtimeContext.env,
4088
+ appServerConfigArgs,
4089
+ );
4090
+ }
4072
4091
  lastRequestCount = proxyServer.getStatus?.().totalRequests ?? 0;
4073
4092
  publishStatus("running");
4074
4093
  process.stdout.write(
@@ -4194,6 +4213,8 @@ function startRuntimeRotationAppHelper(baseContext, options = {}) {
4194
4213
  [APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV]: realCodexHome,
4195
4214
  [APP_RUNTIME_HELPER_USE_CANONICAL_HOME_ENV]:
4196
4215
  options.useCanonicalHome === true ? "1" : "0",
4216
+ [APP_RUNTIME_HELPER_INSTALL_APP_SERVER_SHIM_ENV]:
4217
+ options.installAppServerShim === false ? "0" : "1",
4197
4218
  };
4198
4219
  const helper = spawn(
4199
4220
  process.execPath,
@@ -4227,6 +4248,12 @@ function startRuntimeRotationAppHelper(baseContext, options = {}) {
4227
4248
  }, APP_RUNTIME_HELPER_LAUNCH_TIMEOUT_MS);
4228
4249
  helper.stdout?.setEncoding("utf8");
4229
4250
  helper.stdout?.on("data", (chunk) => {
4251
+ // Keep consuming after startup settles so the helper never blocks on a
4252
+ // full pipe, but stop accumulating: `app-server` holds this parent open
4253
+ // for the resident server's whole life, so a buffer that only ever grows
4254
+ // is a leak, and re-parsing the retained ready line on every later chunk
4255
+ // is pure waste.
4256
+ if (settled) return;
4230
4257
  stdoutBuffer += chunk;
4231
4258
  const newlineIndex = stdoutBuffer.indexOf("\n");
4232
4259
  if (newlineIndex < 0) return;
@@ -4249,6 +4276,7 @@ function startRuntimeRotationAppHelper(baseContext, options = {}) {
4249
4276
  });
4250
4277
  helper.stderr?.setEncoding("utf8");
4251
4278
  helper.stderr?.on("data", (chunk) => {
4279
+ if (settled) return;
4252
4280
  stderrBuffer += chunk;
4253
4281
  });
4254
4282
  helper.once("error", fail);
@@ -4268,11 +4296,17 @@ async function createRuntimeRotationAppHelperContext(
4268
4296
  configTomlModule,
4269
4297
  options = {},
4270
4298
  ) {
4271
- const startedAt = Date.now();
4272
4299
  const { helper, message } = await startRuntimeRotationAppHelper(
4273
4300
  baseContext,
4274
4301
  options,
4275
4302
  );
4303
+ // Start the grace clock once the helper is ready, not before it is launched.
4304
+ // Launching is bounded by APP_RUNTIME_HELPER_LAUNCH_TIMEOUT_MS (15s), so a
4305
+ // cold or loaded machine could otherwise burn the whole 5s window before the
4306
+ // forwarded command had run at all, and `codex app` — which relies on the
4307
+ // window rather than an explicit `detachOnExit` — would kill the helper it
4308
+ // had just handed the desktop app off to.
4309
+ const startedAt = Date.now();
4276
4310
  const helperEnv = message.env ?? {};
4277
4311
  const helperShimDir =
4278
4312
  typeof helperEnv.CODEX_CLI_PATH === "string" &&
@@ -4284,9 +4318,16 @@ async function createRuntimeRotationAppHelperContext(
4284
4318
  : [];
4285
4319
  const detachGraceMs = resolveRuntimeRotationAppHelperDetachGraceMs(baseContext.env);
4286
4320
 
4321
+ // An explicit `detachOnExit: false` is a decision, not a default: a resident
4322
+ // server owns its proxy for its whole lifetime, and leaving the helper behind
4323
+ // on a short-lived run — a client attaches, runs one query, disconnects —
4324
+ // strands it for the full idle timeout with nobody left to refresh its
4325
+ // activity clock. Only an unset `detachOnExit` falls back to the grace window.
4287
4326
  const cleanup = async ({ exitCode } = {}) => {
4288
4327
  const livedMs = Date.now() - startedAt;
4289
- if (exitCode === 0 && (options.detachOnExit === true || livedMs < detachGraceMs)) {
4328
+ const detachWithinGrace =
4329
+ options.detachOnExit !== false && livedMs < detachGraceMs;
4330
+ if (exitCode === 0 && (options.detachOnExit === true || detachWithinGrace)) {
4290
4331
  helper.stdout?.destroy();
4291
4332
  helper.stderr?.destroy();
4292
4333
  helper.unref();
@@ -4321,6 +4362,11 @@ async function createRuntimeRotationAppHelperContext(
4321
4362
  ...baseContext.env,
4322
4363
  ...helperEnv,
4323
4364
  },
4365
+ // Without the app-server shim there is no `CODEX_MULTI_AUTH_APP_SERVER_ACCOUNT_LABEL`
4366
+ // in the forwarded environment, so the caller has to ask for the
4367
+ // `account/read` / `getAuthStatus` / `account/rateLimits/read` rewriting
4368
+ // explicitly. Only the shadow branch used to set it.
4369
+ proxyAppServerAccountRead: options.proxyAppServerAccountRead === true,
4324
4370
  cleanup: async (details) => {
4325
4371
  try {
4326
4372
  await cleanup(details);
@@ -4348,14 +4394,56 @@ async function createRuntimeRotationProxyContextIfEnabled(
4348
4394
  return baseContext;
4349
4395
  }
4350
4396
 
4397
+ // A helper that cannot start is a hard failure for these branches — unlike the
4398
+ // shadow path below, there is no rotation-off shape left to degrade into. Turn
4399
+ // it into a diagnostic and a nonzero exit rather than an unhandled rejection,
4400
+ // and release the compatibility shadow home the caller already built.
4401
+ const startAppHelperContext = async (options) => {
4402
+ try {
4403
+ return await createRuntimeRotationAppHelperContext(
4404
+ baseContext,
4405
+ configTomlModule,
4406
+ options,
4407
+ );
4408
+ } catch (error) {
4409
+ try {
4410
+ baseContext.cleanup?.();
4411
+ } catch {
4412
+ // Best-effort cleanup only; report the original failure.
4413
+ }
4414
+ return {
4415
+ startupError: `codex-multi-auth runtime rotation helper failed to start: ${
4416
+ error instanceof Error ? error.message : String(error)
4417
+ }`,
4418
+ };
4419
+ }
4420
+ };
4421
+
4422
+ // `app-server` is a resident process, and the shadow home cannot host one: the
4423
+ // mirror links directories, so Codex's lstat-strict check on
4424
+ // `<CODEX_HOME>/app-server-control` refuses to start, and a server that does
4425
+ // start serves every attached client a frozen snapshot of the thread index for
4426
+ // its whole life. Same reasoning that moved `resume`/`fork` in #648 (#659).
4427
+ if (isCodexAppServerCommand(rawArgs)) {
4428
+ return startAppHelperContext({
4429
+ // A resident server owns its proxy for its whole lifetime.
4430
+ detachOnExit: false,
4431
+ useCanonicalHome: true,
4432
+ // Nothing here spawns `codex app-server` through `CODEX_CLI_PATH`; the
4433
+ // server is launched with the rotation overrides already on its command
4434
+ // line. See the shim gate in `runRuntimeRotationAppHelper`.
4435
+ installAppServerShim: false,
4436
+ proxyAppServerAccountRead: true,
4437
+ });
4438
+ }
4351
4439
  if (isCodexAppCommand(rawArgs)) {
4352
- return createRuntimeRotationAppHelperContext(baseContext, configTomlModule);
4440
+ return startAppHelperContext();
4353
4441
  }
4354
4442
  if (
4355
4443
  isCodexInteractiveTuiCommand(rawArgs) ||
4356
4444
  isCodexInteractiveResumeCommand(rawArgs)
4357
4445
  ) {
4358
- return createRuntimeRotationAppHelperContext(baseContext, configTomlModule, {
4446
+ return startAppHelperContext({
4359
4447
  detachOnExit: true,
4360
4448
  useCanonicalHome: true,
4361
4449
  });