codex-multi-auth 1.3.2 → 2.0.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.
Files changed (132) hide show
  1. package/README.md +40 -26
  2. package/config/README.md +4 -4
  3. package/config/codex-legacy.json +571 -646
  4. package/config/codex-modern.json +258 -274
  5. package/config/minimal-codex.json +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +85 -14
  9. package/dist/index.js.map +1 -1
  10. package/dist/lib/auto-update-checker.d.ts +23 -1
  11. package/dist/lib/auto-update-checker.d.ts.map +1 -1
  12. package/dist/lib/auto-update-checker.js +481 -8
  13. package/dist/lib/auto-update-checker.js.map +1 -1
  14. package/dist/lib/cli.d.ts +5 -0
  15. package/dist/lib/cli.d.ts.map +1 -1
  16. package/dist/lib/cli.js.map +1 -1
  17. package/dist/lib/codex-manager/commands/best.js +1 -1
  18. package/dist/lib/codex-manager/commands/best.js.map +1 -1
  19. package/dist/lib/codex-manager/commands/forecast.js +3 -3
  20. package/dist/lib/codex-manager/commands/forecast.js.map +1 -1
  21. package/dist/lib/codex-manager/commands/report.js +3 -3
  22. package/dist/lib/codex-manager/commands/report.js.map +1 -1
  23. package/dist/lib/codex-manager/commands/rotation.d.ts +26 -0
  24. package/dist/lib/codex-manager/commands/rotation.d.ts.map +1 -0
  25. package/dist/lib/codex-manager/commands/rotation.js +313 -0
  26. package/dist/lib/codex-manager/commands/rotation.js.map +1 -0
  27. package/dist/lib/codex-manager/commands/status.d.ts +6 -0
  28. package/dist/lib/codex-manager/commands/status.d.ts.map +1 -1
  29. package/dist/lib/codex-manager/commands/status.js +64 -10
  30. package/dist/lib/codex-manager/commands/status.js.map +1 -1
  31. package/dist/lib/codex-manager/forecast-report-commands.js +4 -4
  32. package/dist/lib/codex-manager/forecast-report-commands.js.map +1 -1
  33. package/dist/lib/codex-manager/help.d.ts.map +1 -1
  34. package/dist/lib/codex-manager/help.js +3 -2
  35. package/dist/lib/codex-manager/help.js.map +1 -1
  36. package/dist/lib/codex-manager/rate-limit-markers.d.ts +2 -0
  37. package/dist/lib/codex-manager/rate-limit-markers.d.ts.map +1 -0
  38. package/dist/lib/codex-manager/rate-limit-markers.js +4 -0
  39. package/dist/lib/codex-manager/rate-limit-markers.js.map +1 -0
  40. package/dist/lib/codex-manager/repair-commands.js +3 -3
  41. package/dist/lib/codex-manager/repair-commands.js.map +1 -1
  42. package/dist/lib/codex-manager.d.ts.map +1 -1
  43. package/dist/lib/codex-manager.js +115 -87
  44. package/dist/lib/codex-manager.js.map +1 -1
  45. package/dist/lib/config.d.ts +1 -0
  46. package/dist/lib/config.d.ts.map +1 -1
  47. package/dist/lib/config.js +9 -0
  48. package/dist/lib/config.js.map +1 -1
  49. package/dist/lib/constants.d.ts +2 -0
  50. package/dist/lib/constants.d.ts.map +1 -1
  51. package/dist/lib/constants.js +2 -0
  52. package/dist/lib/constants.js.map +1 -1
  53. package/dist/lib/fs-retry.d.ts +7 -0
  54. package/dist/lib/fs-retry.d.ts.map +1 -0
  55. package/dist/lib/fs-retry.js +35 -0
  56. package/dist/lib/fs-retry.js.map +1 -0
  57. package/dist/lib/index.d.ts +1 -0
  58. package/dist/lib/index.d.ts.map +1 -1
  59. package/dist/lib/index.js +1 -0
  60. package/dist/lib/index.js.map +1 -1
  61. package/dist/lib/logger.d.ts.map +1 -1
  62. package/dist/lib/logger.js +1 -0
  63. package/dist/lib/logger.js.map +1 -1
  64. package/dist/lib/prompts/codex.d.ts +1 -1
  65. package/dist/lib/prompts/codex.d.ts.map +1 -1
  66. package/dist/lib/prompts/codex.js +3 -3
  67. package/dist/lib/prompts/codex.js.map +1 -1
  68. package/dist/lib/quota-probe.js +1 -1
  69. package/dist/lib/quota-probe.js.map +1 -1
  70. package/dist/lib/quota-readiness.d.ts +16 -0
  71. package/dist/lib/quota-readiness.d.ts.map +1 -0
  72. package/dist/lib/quota-readiness.js +82 -0
  73. package/dist/lib/quota-readiness.js.map +1 -0
  74. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  75. package/dist/lib/request/fetch-helpers.js +19 -8
  76. package/dist/lib/request/fetch-helpers.js.map +1 -1
  77. package/dist/lib/request/helpers/model-map.d.ts +2 -1
  78. package/dist/lib/request/helpers/model-map.d.ts.map +1 -1
  79. package/dist/lib/request/helpers/model-map.js +52 -60
  80. package/dist/lib/request/helpers/model-map.js.map +1 -1
  81. package/dist/lib/request/request-transformer.d.ts +2 -2
  82. package/dist/lib/request/request-transformer.js +4 -3
  83. package/dist/lib/request/request-transformer.js.map +1 -1
  84. package/dist/lib/runtime/app-bind.d.ts +74 -0
  85. package/dist/lib/runtime/app-bind.d.ts.map +1 -0
  86. package/dist/lib/runtime/app-bind.js +620 -0
  87. package/dist/lib/runtime/app-bind.js.map +1 -0
  88. package/dist/lib/runtime/config-toml.d.ts +10 -0
  89. package/dist/lib/runtime/config-toml.d.ts.map +1 -0
  90. package/dist/lib/runtime/config-toml.js +128 -0
  91. package/dist/lib/runtime/config-toml.js.map +1 -0
  92. package/dist/lib/runtime/login-menu-accounts.d.ts +5 -0
  93. package/dist/lib/runtime/login-menu-accounts.d.ts.map +1 -1
  94. package/dist/lib/runtime/login-menu-accounts.js +7 -2
  95. package/dist/lib/runtime/login-menu-accounts.js.map +1 -1
  96. package/dist/lib/runtime/quota-probe.js +1 -1
  97. package/dist/lib/runtime/quota-probe.js.map +1 -1
  98. package/dist/lib/runtime/runtime-current-account.d.ts +52 -0
  99. package/dist/lib/runtime/runtime-current-account.d.ts.map +1 -0
  100. package/dist/lib/runtime/runtime-current-account.js +252 -0
  101. package/dist/lib/runtime/runtime-current-account.js.map +1 -0
  102. package/dist/lib/runtime/runtime-observability.d.ts +5 -0
  103. package/dist/lib/runtime/runtime-observability.d.ts.map +1 -1
  104. package/dist/lib/runtime/runtime-observability.js +5 -0
  105. package/dist/lib/runtime/runtime-observability.js.map +1 -1
  106. package/dist/lib/runtime-constants.d.ts +3 -0
  107. package/dist/lib/runtime-constants.d.ts.map +1 -0
  108. package/dist/lib/runtime-constants.js +3 -0
  109. package/dist/lib/runtime-constants.js.map +1 -0
  110. package/dist/lib/runtime-rotation-proxy.d.ts +36 -0
  111. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -0
  112. package/dist/lib/runtime-rotation-proxy.js +828 -0
  113. package/dist/lib/runtime-rotation-proxy.js.map +1 -0
  114. package/dist/lib/schemas.d.ts +1 -0
  115. package/dist/lib/schemas.d.ts.map +1 -1
  116. package/dist/lib/schemas.js +1 -0
  117. package/dist/lib/schemas.js.map +1 -1
  118. package/dist/lib/ui/auth-menu.d.ts +6 -1
  119. package/dist/lib/ui/auth-menu.d.ts.map +1 -1
  120. package/dist/lib/ui/auth-menu.js +22 -2
  121. package/dist/lib/ui/auth-menu.js.map +1 -1
  122. package/package.json +13 -11
  123. package/scripts/bench-format/codex-host.mjs +142 -73
  124. package/scripts/bench-format/models.mjs +23 -24
  125. package/scripts/benchmark-edit-formats.mjs +9 -7
  126. package/scripts/check-pack-budget-lib.js +49 -6
  127. package/scripts/codex-app-launcher.js +653 -0
  128. package/scripts/codex-app-router.js +322 -0
  129. package/scripts/codex-multi-auth.js +3 -1
  130. package/scripts/codex-routing.js +6 -0
  131. package/scripts/codex.js +2428 -445
  132. package/scripts/postinstall.js +343 -0
package/scripts/codex.js CHANGED
@@ -1,23 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from "node:child_process";
4
+ import { createHash, randomBytes } from "node:crypto";
4
5
  import {
5
6
  chmodSync,
6
7
  copyFileSync,
8
+ cpSync,
7
9
  existsSync,
10
+ linkSync,
8
11
  mkdirSync,
9
12
  mkdtempSync,
13
+ readdirSync,
10
14
  renameSync,
11
15
  readFileSync,
12
16
  rmSync,
13
17
  statSync,
18
+ symlinkSync,
14
19
  writeFileSync,
15
20
  } from "node:fs";
16
21
  import { createRequire } from "node:module";
17
22
  import { homedir, tmpdir } from "node:os";
18
23
  import { basename, delimiter, dirname, join, resolve as resolvePath } from "node:path";
19
24
  import process from "node:process";
20
- import { fileURLToPath } from "node:url";
25
+ import { StringDecoder } from "node:string_decoder";
26
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
27
  import {
22
28
  resolveRealCodexBin as resolveRealCodexBinFromEnvironment,
23
29
  splitPathEntries,
@@ -26,7 +32,39 @@ import { normalizeAuthAlias, shouldHandleMultiAuthAuth } from "./codex-routing.j
26
32
 
27
33
  const RETRYABLE_SHADOW_HOME_CLEANUP_CODES = new Set(["EBUSY", "EPERM", "ENOTEMPTY"]);
28
34
  const SHADOW_HOME_CLEANUP_BACKOFF_MS = [20, 60, 120];
35
+ const SHADOW_HOME_ORPHAN_LOCK_STALE_AGE_MS = 2_000;
36
+ const SHADOW_HOME_SYNC_LOCK_WAIT_TIMEOUT_MS =
37
+ SHADOW_HOME_ORPHAN_LOCK_STALE_AGE_MS +
38
+ SHADOW_HOME_CLEANUP_BACKOFF_MS.reduce((total, value) => total + value, 0);
29
39
  const SHADOW_HOME_STATE_FILES = ["auth.json", "accounts.json", ".codex-global-state.json"];
40
+ const RUNTIME_ROTATION_SHADOW_HOME_OMIT_STATE_FILES = new Set([
41
+ "auth.json",
42
+ "accounts.json",
43
+ ]);
44
+ const SHADOW_HOME_STATE_FILE_SET = new Set(SHADOW_HOME_STATE_FILES);
45
+ const SHADOW_HOME_CONFIG_FILE = "config.toml";
46
+ const SHADOW_HOME_SYNC_LOCK_DIR = ".codex-multi-auth-shadow-sync.lock";
47
+ const SHADOW_HOME_SYNC_STATE_FILE = ".codex-multi-auth-shadow-sync-state.json";
48
+ const APP_SERVER_ACCOUNT_DISPLAY_NAME = "codex-multi-auth";
49
+ const RUNTIME_CONSTANTS = await loadRuntimeConstants();
50
+ const RUNTIME_ROTATION_PROXY_PROVIDER_ID =
51
+ RUNTIME_CONSTANTS.RUNTIME_ROTATION_PROXY_PROVIDER_ID;
52
+ const APP_SERVER_ACCOUNT_LABEL_ENV = "CODEX_MULTI_AUTH_APP_SERVER_ACCOUNT_LABEL";
53
+ const INTERNAL_RUNTIME_ROTATION_APP_HELPER_ARG =
54
+ "--codex-multi-auth-runtime-app-helper";
55
+ const APP_RUNTIME_HELPER_OWNER_PID_ENV =
56
+ "CODEX_MULTI_AUTH_APP_ROTATION_OWNER_PID";
57
+ const APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV =
58
+ "CODEX_MULTI_AUTH_REAL_CODEX_HOME";
59
+ const APP_RUNTIME_HELPER_STATUS_FILE =
60
+ RUNTIME_CONSTANTS.APP_RUNTIME_HELPER_STATUS_FILE;
61
+ const DEFAULT_APP_RUNTIME_HELPER_IDLE_MS = 12 * 60 * 60 * 1000;
62
+ const DEFAULT_APP_RUNTIME_HELPER_DETACH_GRACE_MS = 5_000;
63
+ const APP_RUNTIME_HELPER_LAUNCH_TIMEOUT_MS = 15_000;
64
+ const APP_SERVER_SHIM_DIR_NAME = "app-server-shims";
65
+ const APP_SERVER_SHIM_HELPER_PREFIX = "helper-";
66
+ const DEFAULT_STARTUP_AUTO_UPDATE_BUDGET_MS = 3_000;
67
+ const STARTUP_AUTO_UPDATE_TIMED_OUT = Symbol("startup-auto-update-timed-out");
30
68
  let shadowHomeCleanupBusyFailuresRemaining = Number.parseInt(
31
69
  process.env.CODEX_MULTI_AUTH_TEST_SHADOW_CLEANUP_BUSY_FAILURES ?? "0",
32
70
  10,
@@ -35,8 +73,45 @@ let shadowHomeCleanupPreflightReadBusyFailuresRemaining = Number.parseInt(
35
73
  process.env.CODEX_MULTI_AUTH_TEST_SHADOW_PREFLIGHT_READ_BUSY_FAILURES ?? "0",
36
74
  10,
37
75
  );
76
+ let shadowHomeSyncLockRecreateStaleCount = Number.parseInt(
77
+ process.env.CODEX_MULTI_AUTH_TEST_SHADOW_LOCK_RECREATE_STALE_COUNT ?? "0",
78
+ 10,
79
+ );
80
+ let shadowHomeSyncMetadataBusyFailuresRemaining = Number.parseInt(
81
+ process.env.CODEX_MULTI_AUTH_TEST_SHADOW_SYNC_METADATA_BUSY_FAILURES ?? "0",
82
+ 10,
83
+ );
84
+ let shadowHomeSyncLockOwnerWriteFailuresRemaining = Number.parseInt(
85
+ process.env.CODEX_MULTI_AUTH_TEST_SHADOW_LOCK_OWNER_WRITE_FAILURES ?? "0",
86
+ 10,
87
+ );
38
88
  const shadowHomeCleanupRetryMarkerDir =
39
89
  (process.env.CODEX_MULTI_AUTH_TEST_SHADOW_RETRY_MARKER_DIR ?? "").trim();
90
+ let warnedInvalidRuntimeRotationProxyEnv = false;
91
+ let warnedPendingAccountReadIdOverflow = false;
92
+
93
+ async function loadRuntimeConstants() {
94
+ const fallback = {
95
+ RUNTIME_ROTATION_PROXY_PROVIDER_ID: `${APP_SERVER_ACCOUNT_DISPLAY_NAME}-runtime-proxy`,
96
+ APP_RUNTIME_HELPER_STATUS_FILE: "runtime-rotation-app-helper.json",
97
+ };
98
+ try {
99
+ const mod = await import("../dist/lib/runtime-constants.js");
100
+ return {
101
+ RUNTIME_ROTATION_PROXY_PROVIDER_ID:
102
+ typeof mod.RUNTIME_ROTATION_PROXY_PROVIDER_ID === "string"
103
+ ? mod.RUNTIME_ROTATION_PROXY_PROVIDER_ID
104
+ : fallback.RUNTIME_ROTATION_PROXY_PROVIDER_ID,
105
+ APP_RUNTIME_HELPER_STATUS_FILE:
106
+ typeof mod.APP_RUNTIME_HELPER_STATUS_FILE === "string"
107
+ ? mod.APP_RUNTIME_HELPER_STATUS_FILE
108
+ : fallback.APP_RUNTIME_HELPER_STATUS_FILE,
109
+ };
110
+ } catch {
111
+ // Keep wrapper startup resilient when dist has not been built yet.
112
+ }
113
+ return fallback;
114
+ }
40
115
 
41
116
  function isRetryableShadowHomeCleanupError(error) {
42
117
  const code = error && typeof error === "object" && "code" in error ? error.code : undefined;
@@ -81,6 +156,37 @@ function hydrateCliVersionEnv() {
81
156
  }
82
157
  }
83
158
 
159
+ function isRotationEnableCommand(args) {
160
+ return args[0] === "auth" && args[1] === "rotation" && args[2] === "enable";
161
+ }
162
+
163
+ function shouldAutoInstallCodexAppLauncher(env = process.env) {
164
+ const override = (env.CODEX_MULTI_AUTH_APP_LAUNCHER_INSTALL ?? "1").trim().toLowerCase();
165
+ return !new Set(["0", "false", "no"]).has(override);
166
+ }
167
+
168
+ async function maybeInstallCodexAppLauncherAfterRotationEnable(args, exitCode) {
169
+ if (exitCode !== 0 || !isRotationEnableCommand(args)) {
170
+ return;
171
+ }
172
+ if (!shouldAutoInstallCodexAppLauncher()) {
173
+ return;
174
+ }
175
+ try {
176
+ const mod = await import("./codex-app-launcher.js");
177
+ if (typeof mod.installCodexAppLauncher !== "function") {
178
+ return;
179
+ }
180
+ await mod.installCodexAppLauncher({
181
+ log: (message) => console.error(`codex-multi-auth: ${message}`),
182
+ });
183
+ } catch (error) {
184
+ console.error(
185
+ `codex-multi-auth: could not route Codex app launchers: ${error instanceof Error ? error.message : String(error)}`,
186
+ );
187
+ }
188
+ }
189
+
84
190
  async function loadRunCodexMultiAuthCli() {
85
191
  try {
86
192
  const mod = await import("../dist/lib/codex-manager.js");
@@ -105,6 +211,191 @@ async function loadRunCodexMultiAuthCli() {
105
211
  }
106
212
  }
107
213
 
214
+ async function loadRuntimeRotationProxyModule() {
215
+ try {
216
+ const mod = await import("../dist/lib/runtime-rotation-proxy.js");
217
+ if (typeof mod.startRuntimeRotationProxy !== "function") {
218
+ console.error(
219
+ "dist/lib/runtime-rotation-proxy.js is missing required export: startRuntimeRotationProxy",
220
+ );
221
+ return null;
222
+ }
223
+ return mod;
224
+ } catch (error) {
225
+ if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
226
+ console.error(
227
+ [
228
+ "codex-multi-auth runtime rotation proxy requires built runtime files, but dist output is missing.",
229
+ "Run: npm run build",
230
+ ].join("\n"),
231
+ );
232
+ return null;
233
+ }
234
+ throw error;
235
+ }
236
+ }
237
+
238
+ async function loadRuntimeRotationConfigModule() {
239
+ try {
240
+ const mod = await import("../dist/lib/config.js");
241
+ if (
242
+ typeof mod.loadPluginConfig !== "function" ||
243
+ typeof mod.getCodexRuntimeRotationProxy !== "function"
244
+ ) {
245
+ return null;
246
+ }
247
+ return mod;
248
+ } catch (error) {
249
+ if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
250
+ return null;
251
+ }
252
+ throw error;
253
+ }
254
+ }
255
+
256
+ function readBooleanEnvFlag(name) {
257
+ const normalized = (process.env[name] ?? "").trim().toLowerCase();
258
+ if (["1", "true", "yes"].includes(normalized)) return true;
259
+ if (["0", "false", "no"].includes(normalized)) return false;
260
+ return null;
261
+ }
262
+
263
+ function isStartupAutoUpdateDebugEnabled() {
264
+ return readBooleanEnvFlag("CODEX_MULTI_AUTH_DEBUG") === true;
265
+ }
266
+
267
+ function shouldLogStartupAutoUpdateProgress() {
268
+ return process.stderr.isTTY === true || isStartupAutoUpdateDebugEnabled();
269
+ }
270
+
271
+ function readStartupAutoUpdateBudgetMs() {
272
+ const raw =
273
+ process.env.CODEX_MULTI_AUTH_AUTO_UPDATE_STARTUP_BUDGET_MS ??
274
+ process.env.CODEX_MULTI_AUTH_TEST_STARTUP_AUTO_UPDATE_BUDGET_MS;
275
+ if (!raw) return DEFAULT_STARTUP_AUTO_UPDATE_BUDGET_MS;
276
+ const parsed = Number.parseInt(raw, 10);
277
+ return Number.isFinite(parsed) && parsed > 0
278
+ ? parsed
279
+ : DEFAULT_STARTUP_AUTO_UPDATE_BUDGET_MS;
280
+ }
281
+
282
+ function resolveStartupAutoUpdateTimeouts(budgetMs) {
283
+ const fetchTimeoutMs = Math.max(1, Math.floor(budgetMs * 0.4));
284
+ return {
285
+ fetchTimeoutMs,
286
+ updateTimeoutMs: Math.max(1, budgetMs - fetchTimeoutMs),
287
+ };
288
+ }
289
+
290
+ function isModuleNotFoundError(error) {
291
+ return (
292
+ error &&
293
+ typeof error === "object" &&
294
+ "code" in error &&
295
+ error.code === "ERR_MODULE_NOT_FOUND"
296
+ );
297
+ }
298
+
299
+ function shouldRunStartupAutoUpdate(rawArgs, normalizedArgs) {
300
+ if ((process.env.CODEX_MULTI_AUTH_BYPASS ?? "").trim() === "1") {
301
+ return false;
302
+ }
303
+ if (isPureHelpOrVersionArgs(rawArgs)) {
304
+ return false;
305
+ }
306
+ if (shouldHandleMultiAuthAuth(normalizedArgs)) {
307
+ return false;
308
+ }
309
+ return true;
310
+ }
311
+
312
+ async function withStartupAutoUpdateBudget(promise, budgetMs) {
313
+ let timeout = null;
314
+ try {
315
+ return await Promise.race([
316
+ promise,
317
+ new Promise((resolve) => {
318
+ timeout = setTimeout(
319
+ () => {
320
+ timeout?.unref?.();
321
+ resolve(STARTUP_AUTO_UPDATE_TIMED_OUT);
322
+ },
323
+ budgetMs,
324
+ );
325
+ }),
326
+ ]);
327
+ } finally {
328
+ if (timeout) clearTimeout(timeout);
329
+ }
330
+ }
331
+
332
+ function logStartupAutoUpdateDebug(message) {
333
+ if (isStartupAutoUpdateDebugEnabled()) {
334
+ console.error(`codex-multi-auth: ${message}`);
335
+ }
336
+ }
337
+
338
+ async function autoUpdatePackageIfEnabled(rawArgs, normalizedArgs) {
339
+ if (!shouldRunStartupAutoUpdate(rawArgs, normalizedArgs)) return;
340
+ const budgetMs = readStartupAutoUpdateBudgetMs();
341
+ const { fetchTimeoutMs, updateTimeoutMs } =
342
+ resolveStartupAutoUpdateTimeouts(budgetMs);
343
+ try {
344
+ const mod = await import("../dist/lib/auto-update-checker.js");
345
+ if (typeof mod.autoUpdateIfAvailable !== "function") {
346
+ return;
347
+ }
348
+ const updatePromise = mod.autoUpdateIfAvailable({
349
+ fetchTimeoutMs,
350
+ timeoutMs: updateTimeoutMs,
351
+ onUpdateStart: (update) => {
352
+ if (!update?.latestVersion) return;
353
+ if (!shouldLogStartupAutoUpdateProgress()) return;
354
+ console.error(
355
+ `codex-multi-auth: auto-update found ${update.latestVersion}; starting npm update -g codex-multi-auth. Startup will continue if it exceeds ${budgetMs}ms.`,
356
+ );
357
+ },
358
+ });
359
+ const result = await withStartupAutoUpdateBudget(updatePromise, budgetMs);
360
+ if (result === STARTUP_AUTO_UPDATE_TIMED_OUT) {
361
+ logStartupAutoUpdateDebug(
362
+ `auto-update skipped: startup budget exceeded after ${budgetMs}ms`,
363
+ );
364
+ return;
365
+ }
366
+ if (result?.updated && result.latestVersion) {
367
+ if (shouldLogStartupAutoUpdateProgress()) {
368
+ console.error(
369
+ `codex-multi-auth: auto-updated to ${result.latestVersion}. New sessions will use the latest package.`,
370
+ );
371
+ }
372
+ }
373
+ } catch (error) {
374
+ if (isModuleNotFoundError(error)) return;
375
+ logStartupAutoUpdateDebug(
376
+ `auto-update skipped: ${error instanceof Error ? error.message : String(error)}`,
377
+ );
378
+ }
379
+ }
380
+
381
+ async function loadRuntimeConfigTomlModule() {
382
+ try {
383
+ const mod = await import("../dist/lib/runtime/config-toml.js");
384
+ if (
385
+ typeof mod.rewriteConfigTomlForRuntimeRotationProvider !== "function" ||
386
+ typeof mod.tomlStringLiteral !== "function"
387
+ ) {
388
+ return null;
389
+ }
390
+ return mod;
391
+ } catch (error) {
392
+ if (error && typeof error === "object" && "code" in error && error.code === "ERR_MODULE_NOT_FOUND") {
393
+ return null;
394
+ }
395
+ throw error;
396
+ }
397
+ }
398
+
108
399
  async function autoSyncManagerActiveSelectionIfEnabled() {
109
400
  const enabled = (process.env.CODEX_MULTI_AUTH_AUTO_SYNC_ON_STARTUP ?? "1").trim() !== "0";
110
401
  if (!enabled) return;
@@ -149,49 +440,76 @@ const NORMALIZED_UNSUPPORTED_MODEL_PATTERN =
149
440
  /model ['"]([^'"]+)['"] is not currently available for this chatgpt account when using codex oauth/i;
150
441
  const MODEL_ACCESS_DENIED_PATTERN =
151
442
  /the model [`'"]([^`'"]+)[`'"] does not exist or you do not have access to it/i;
152
- const DIRECT_UNSUPPORTED_MODEL_PATTERN =
153
- /['"]([^'"]+)['"]\s+model is not supported when using codex with a chatgpt account/i;
154
- const WRAPPER_UNSUPPORTED_MODEL_FALLBACK_CHAIN = {
155
- "gpt-5.5": ["gpt-5.4"],
156
- "gpt-5.5-pro": ["gpt-5.4"],
157
- };
158
-
159
- function canonicalizeRequestedModelName(model) {
160
- if (typeof model !== "string") return "";
161
- const normalizedModel = normalizeRequestedModel(model);
162
- if (normalizedModel) {
163
- return normalizedModel;
443
+ const DIRECT_UNSUPPORTED_MODEL_PATTERN =
444
+ /['"]([^'"]+)['"]\s+model is not supported when using codex with a chatgpt account/i;
445
+ const CURRENT_CODEX_MODEL = "gpt-5.3-codex";
446
+ const LEGACY_CODEX_MODEL = "gpt-5-codex";
447
+ const WRAPPER_UNSUPPORTED_MODEL_FALLBACK_CHAIN = {
448
+ "gpt-5": ["gpt-5.5"],
449
+ "gpt-5-pro": ["gpt-5.5-pro"],
450
+ "gpt-5-chat-latest": ["gpt-5.5"],
451
+ "gpt-5.5": ["gpt-5.4"],
452
+ "gpt-5.5-pro": ["gpt-5.4"],
453
+ "gpt-5.5-2026-04-23": ["gpt-5.4"],
454
+ "gpt-5.5-pro-2026-04-23": ["gpt-5.4"],
455
+ "gpt-5.5-20260423": ["gpt-5.4"],
456
+ "gpt-5.5-pro-20260423": ["gpt-5.4"],
457
+ "gpt-5.3-codex-spark": [CURRENT_CODEX_MODEL],
458
+ "codex-max": [CURRENT_CODEX_MODEL],
459
+ "gpt-5.1-codex-max": [CURRENT_CODEX_MODEL],
460
+ "codex-mini-latest": [CURRENT_CODEX_MODEL],
461
+ "gpt-5-codex-mini": [CURRENT_CODEX_MODEL],
462
+ "gpt-5.1-codex-mini": [CURRENT_CODEX_MODEL],
463
+ [LEGACY_CODEX_MODEL]: [CURRENT_CODEX_MODEL],
464
+ "gpt-5.2-codex": [CURRENT_CODEX_MODEL],
465
+ "gpt-5.1-codex": [CURRENT_CODEX_MODEL],
466
+ };
467
+
468
+ function canonicalizeRequestedModelName(model) {
469
+ if (typeof model !== "string") return "";
470
+ const normalizedModel = normalizeRequestedModel(model);
471
+ if (normalizedModel) {
472
+ return normalizedModel;
164
473
  }
165
474
 
166
475
  const stripped = stripProviderPrefix(model).trim().toLowerCase();
167
476
  if (!stripped) {
168
477
  return "";
169
478
  }
170
-
171
- return stripped.replace(/-(none|minimal|low|medium|high|xhigh)$/i, "");
172
- }
173
-
174
- function extractUnsupportedModelFromOutput(output) {
175
- if (typeof output !== "string" || output.length === 0) {
176
- return undefined;
177
- }
178
-
179
- const directMatch = output.match(DIRECT_UNSUPPORTED_MODEL_PATTERN);
180
- if (directMatch?.[1]) {
181
- return canonicalizeRequestedModelName(directMatch[1]);
182
- }
183
-
184
- const normalizedMatch = output.match(NORMALIZED_UNSUPPORTED_MODEL_PATTERN);
185
- if (normalizedMatch?.[1]) {
186
- return canonicalizeRequestedModelName(normalizedMatch[1]);
187
- }
188
- const accessDeniedMatch = output.match(MODEL_ACCESS_DENIED_PATTERN);
189
- if (accessDeniedMatch?.[1]) {
190
- return canonicalizeRequestedModelName(accessDeniedMatch[1]);
191
- }
192
-
193
- return undefined;
194
- }
479
+
480
+ return stripped.replace(/-(none|minimal|low|medium|high|xhigh)$/i, "");
481
+ }
482
+
483
+ function canonicalizeUnsupportedModelKey(model) {
484
+ if (typeof model !== "string") return "";
485
+ const stripped = stripProviderPrefix(model).trim().toLowerCase();
486
+ if (!stripped) {
487
+ return "";
488
+ }
489
+ return stripped.replace(/-(none|minimal|low|medium|high|xhigh)$/i, "");
490
+ }
491
+
492
+ function extractUnsupportedModelFromOutput(output) {
493
+ if (typeof output !== "string" || output.length === 0) {
494
+ return undefined;
495
+ }
496
+
497
+ const directMatch = output.match(DIRECT_UNSUPPORTED_MODEL_PATTERN);
498
+ if (directMatch?.[1]) {
499
+ return canonicalizeUnsupportedModelKey(directMatch[1]);
500
+ }
501
+
502
+ const normalizedMatch = output.match(NORMALIZED_UNSUPPORTED_MODEL_PATTERN);
503
+ if (normalizedMatch?.[1]) {
504
+ return canonicalizeUnsupportedModelKey(normalizedMatch[1]);
505
+ }
506
+ const accessDeniedMatch = output.match(MODEL_ACCESS_DENIED_PATTERN);
507
+ if (accessDeniedMatch?.[1]) {
508
+ return canonicalizeUnsupportedModelKey(accessDeniedMatch[1]);
509
+ }
510
+
511
+ return undefined;
512
+ }
195
513
 
196
514
  function resolveUnsupportedModelRetryTarget(
197
515
  requestedModel,
@@ -208,14 +526,14 @@ function resolveUnsupportedModelRetryTarget(
208
526
  return undefined;
209
527
  }
210
528
 
211
- const attempted = new Set(
212
- attemptedModels
213
- .map((model) => canonicalizeRequestedModelName(model))
214
- .filter(Boolean),
215
- );
216
- const normalizedRequestedModel = canonicalizeRequestedModelName(requestedModel);
217
- const blockedModel =
218
- extractUnsupportedModelFromOutput(output) ?? normalizedRequestedModel;
529
+ const attempted = new Set(
530
+ attemptedModels
531
+ .map((model) => canonicalizeUnsupportedModelKey(model))
532
+ .filter(Boolean),
533
+ );
534
+ const normalizedRequestedModel = canonicalizeUnsupportedModelKey(requestedModel);
535
+ const blockedModel =
536
+ extractUnsupportedModelFromOutput(output) ?? normalizedRequestedModel;
219
537
  const fallbackChain =
220
538
  WRAPPER_UNSUPPORTED_MODEL_FALLBACK_CHAIN[blockedModel] ??
221
539
  (normalizedRequestedModel
@@ -232,173 +550,432 @@ function resolveUnsupportedModelRetryTarget(
232
550
  return undefined;
233
551
  }
234
552
 
235
- function replaceRequestedModel(args, nextModel) {
236
- if (!nextModel) {
237
- return [...args];
238
- }
553
+ function replaceRequestedModel(args, nextModel) {
554
+ if (!nextModel) {
555
+ return [...args];
556
+ }
239
557
 
240
558
  const nextArgs = [...args];
241
559
  for (let i = 0; i < nextArgs.length; i += 1) {
242
- const arg = nextArgs[i];
243
- if (arg === "--model" && typeof nextArgs[i + 1] === "string") {
244
- nextArgs[i + 1] = nextModel;
245
- return nextArgs;
246
- }
560
+ const arg = nextArgs[i];
561
+ if ((arg === "--model" || arg === "-m") && typeof nextArgs[i + 1] === "string") {
562
+ nextArgs[i + 1] = nextModel;
563
+ return nextArgs;
564
+ }
247
565
  if (typeof arg === "string" && arg.startsWith("--model=")) {
248
566
  nextArgs[i] = `--model=${nextModel}`;
249
567
  return nextArgs;
250
568
  }
251
569
  }
252
570
 
253
- return nextArgs;
254
- }
255
-
256
- function shouldCaptureForwardedCodexOutput(env = process.env) {
257
- const override = (env.CODEX_MULTI_AUTH_CAPTURE_FORWARD_OUTPUT ?? "").trim();
258
- if (override === "1") {
259
- return true;
260
- }
261
- if (override === "0") {
262
- return false;
263
- }
264
- // Windows child processes can report undefined isTTY; treat that as non-TTY so retry capture remains available.
265
- return process.stdout.isTTY !== true || process.stderr.isTTY !== true;
266
- }
267
-
268
- function forwardToRealCodexOnce(
269
- codexBin,
270
- args,
271
- env = process.env,
272
- cleanup,
273
- options = {},
274
- ) {
275
- return new Promise((resolve) => {
276
- let settled = false;
277
- let stdout = "";
278
- let stderr = "";
279
- const captureOutput = options.captureOutput === true;
280
- const finalize = (exitCode) => {
281
- if (settled) {
282
- return;
283
- }
284
- settled = true;
285
- try {
286
- cleanup?.();
287
- } catch {
288
- // Best-effort cleanup only.
289
- }
290
- resolve({
291
- exitCode,
292
- // No-capture output stays empty by design so retry parsing cannot
293
- // reintroduce pipes that break terminal passthrough.
294
- output: `${stdout}\n${stderr}`.trim(),
295
- });
296
- };
297
-
298
- const command = codexBin.launchWithNode ? process.execPath : codexBin.path;
299
- const commandArgs = codexBin.launchWithNode ? [codexBin.path, ...args] : args;
300
- let child;
301
- const failLaunch = (error) => {
302
- const message = `Failed to launch real Codex CLI: ${String(error)}`;
303
- stderr += `${stderr ? "\n" : ""}${message}`;
304
- console.error(message);
305
- finalize(1);
306
- };
307
- try {
308
- child = spawn(command, commandArgs, {
309
- stdio: captureOutput ? ["inherit", "pipe", "pipe"] : "inherit",
310
- env,
311
- });
312
- } catch (error) {
313
- failLaunch(error);
314
- return;
315
- }
316
-
317
- if (captureOutput) {
318
- child.stdout?.on("data", (chunk) => {
319
- const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
320
- stdout += text;
321
- process.stdout.write(chunk);
322
- });
323
- child.stderr?.on("data", (chunk) => {
324
- const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
325
- stderr += text;
326
- process.stderr.write(chunk);
327
- });
328
- }
329
-
330
- child.once("error", (error) => {
331
- failLaunch(error);
332
- });
333
-
334
- child.once("close", (code, signal) => {
335
- if (signal) {
336
- const signalNumber = signal === "SIGINT" ? 130 : 1;
337
- finalize(signalNumber);
338
- return;
339
- }
340
- finalize(typeof code === "number" ? code : 1);
341
- });
342
- });
571
+ return nextArgs;
343
572
  }
344
573
 
345
- async function forwardToRealCodex(codexBin, rawArgs, baseEnv = process.env) {
346
- let currentArgs = [...rawArgs];
347
- let lastExitCode = 1;
348
- const attemptedModels = new Set();
574
+ function shouldCaptureForwardedCodexOutput(env = process.env) {
575
+ const override = (env.CODEX_MULTI_AUTH_CAPTURE_FORWARD_OUTPUT ?? "").trim();
576
+ if (override === "1") {
577
+ return true;
578
+ }
579
+ if (override === "0") {
580
+ return false;
581
+ }
582
+ // Windows child processes can report undefined isTTY; treat that as non-TTY so retry capture remains available.
583
+ return process.stdout.isTTY !== true || process.stderr.isTTY !== true;
584
+ }
349
585
 
350
- for (let attempt = 0; attempt < 4; attempt += 1) {
351
- const requestedModel = extractRequestedModel(currentArgs);
352
- if (requestedModel) {
353
- attemptedModels.add(requestedModel);
354
- }
586
+ function jsonRpcIdKey(id) {
587
+ if (
588
+ typeof id === "string" ||
589
+ typeof id === "number" ||
590
+ typeof id === "boolean" ||
591
+ id === null
592
+ ) {
593
+ return `${typeof id}:${JSON.stringify(id)}`;
594
+ }
595
+ return null;
596
+ }
355
597
 
356
- const { args: forwardArgs, requestedModel: compatibilityRequestedModel } =
357
- buildForwardArgs(currentArgs);
358
- const compatibility = createCompatibilityCodexHome(
359
- forwardArgs,
360
- compatibilityRequestedModel,
361
- baseEnv,
362
- );
363
- const result = await forwardToRealCodexOnce(
364
- codexBin,
365
- compatibility.args,
366
- compatibility.env,
367
- compatibility.cleanup,
368
- {
369
- captureOutput: shouldCaptureForwardedCodexOutput(compatibility.env),
370
- },
371
- );
372
- lastExitCode = result.exitCode;
373
- if (result.exitCode === 0) {
374
- return result.exitCode;
375
- }
598
+ function parseJsonObjectLine(line) {
599
+ const trimmed = line.trim();
600
+ if (!trimmed.startsWith("{")) {
601
+ return null;
602
+ }
603
+ try {
604
+ const parsed = JSON.parse(trimmed);
605
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
606
+ ? parsed
607
+ : null;
608
+ } catch {
609
+ return null;
610
+ }
611
+ }
376
612
 
377
- const fallbackModel = resolveUnsupportedModelRetryTarget(
378
- requestedModel,
379
- result.output,
380
- [...attemptedModels],
381
- );
382
- if (!fallbackModel) {
383
- return result.exitCode;
384
- }
613
+ function splitProtocolLineEnding(line) {
614
+ if (line.endsWith("\r\n")) {
615
+ return { body: line.slice(0, -2), lineEnding: "\r\n" };
616
+ }
617
+ if (line.endsWith("\n")) {
618
+ return { body: line.slice(0, -1), lineEnding: "\n" };
619
+ }
620
+ return { body: line, lineEnding: "" };
621
+ }
385
622
 
386
- console.error(
387
- `codex-multi-auth: model ${requestedModel ?? "requested model"} is unsupported on this ChatGPT Codex surface. Retrying with ${fallbackModel}.`,
388
- );
389
- attemptedModels.add(fallbackModel);
390
- const nextArgs = replaceRequestedModel(currentArgs, fallbackModel);
391
- if (JSON.stringify(nextArgs) === JSON.stringify(currentArgs)) {
392
- return result.exitCode;
623
+ function createProtocolLineAccumulator(onLine) {
624
+ const decoder = new StringDecoder("utf8");
625
+ let buffer = "";
626
+ const drain = () => {
627
+ let newlineIndex = buffer.indexOf("\n");
628
+ while (newlineIndex >= 0) {
629
+ const line = buffer.slice(0, newlineIndex + 1);
630
+ buffer = buffer.slice(newlineIndex + 1);
631
+ onLine(line);
632
+ newlineIndex = buffer.indexOf("\n");
393
633
  }
394
- currentArgs = nextArgs;
395
- }
634
+ };
396
635
 
397
- return lastExitCode;
636
+ return {
637
+ write(chunk) {
638
+ buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
639
+ drain();
640
+ },
641
+ end() {
642
+ buffer += decoder.end();
643
+ if (buffer.length > 0) {
644
+ onLine(buffer);
645
+ buffer = "";
646
+ }
647
+ },
648
+ };
398
649
  }
399
650
 
400
- function hasCliAuthCredentialsStoreOverride(args) {
401
- for (let i = 0; i < args.length; i += 1) {
651
+ function createSyntheticAppServerAccountReadResult() {
652
+ return {
653
+ account: {
654
+ type: "chatgpt",
655
+ email: APP_SERVER_ACCOUNT_DISPLAY_NAME,
656
+ planType: "unknown",
657
+ },
658
+ requiresOpenaiAuth: false,
659
+ };
660
+ }
661
+
662
+ function createSyntheticAppServerAuthStatusResult() {
663
+ return {
664
+ authMethod: "apikey",
665
+ authToken: null,
666
+ requiresOpenaiAuth: false,
667
+ };
668
+ }
669
+
670
+ function createSyntheticAppServerRateLimitsResult() {
671
+ return {
672
+ rateLimits: {
673
+ limitId: null,
674
+ limitName: null,
675
+ primary: null,
676
+ secondary: null,
677
+ credits: null,
678
+ planType: null,
679
+ rateLimitReachedType: null,
680
+ },
681
+ rateLimitsByLimitId: null,
682
+ };
683
+ }
684
+
685
+ function createAppServerAccountReadProtocolProxy() {
686
+ const maxPendingAuthRequestIds = 4096;
687
+ const pendingAuthRequestMethodsById = new Map();
688
+ const inputLines = createProtocolLineAccumulator((line) => {
689
+ const { body } = splitProtocolLineEnding(line);
690
+ const message = parseJsonObjectLine(body);
691
+ if (
692
+ ![
693
+ "account/read",
694
+ "account/rateLimits/read",
695
+ "getAuthStatus",
696
+ ].includes(message?.method) ||
697
+ !Object.hasOwn(message, "id")
698
+ ) {
699
+ return;
700
+ }
701
+ const key = jsonRpcIdKey(message.id);
702
+ if (key) {
703
+ if (pendingAuthRequestMethodsById.size >= maxPendingAuthRequestIds) {
704
+ pendingAuthRequestMethodsById.clear();
705
+ if (!warnedPendingAccountReadIdOverflow) {
706
+ warnedPendingAccountReadIdOverflow = true;
707
+ console.error(
708
+ "codex-multi-auth: cleared pending app-server auth request ids after exceeding the safety cap.",
709
+ );
710
+ }
711
+ }
712
+ pendingAuthRequestMethodsById.set(key, message.method);
713
+ }
714
+ });
715
+ const outputLines = createProtocolLineAccumulator((line) => {
716
+ process.stdout.write(
717
+ rewriteAppServerAccountReadResponseLine(line, pendingAuthRequestMethodsById),
718
+ );
719
+ });
720
+
721
+ return {
722
+ observeInput(chunk) {
723
+ inputLines.write(chunk);
724
+ },
725
+ flushInput() {
726
+ inputLines.end();
727
+ },
728
+ writeOutput(chunk) {
729
+ outputLines.write(chunk);
730
+ },
731
+ flushOutput() {
732
+ outputLines.end();
733
+ },
734
+ };
735
+ }
736
+
737
+ function rewriteAppServerAccountReadResponseLine(line, pendingAuthRequestMethodsById) {
738
+ const { body, lineEnding } = splitProtocolLineEnding(line);
739
+ const message = parseJsonObjectLine(body);
740
+ if (!message || !Object.hasOwn(message, "id")) {
741
+ return line;
742
+ }
743
+ const key = jsonRpcIdKey(message.id);
744
+ if (!key || !pendingAuthRequestMethodsById.has(key)) {
745
+ return line;
746
+ }
747
+ const method = pendingAuthRequestMethodsById.get(key);
748
+ pendingAuthRequestMethodsById.delete(key);
749
+ const result =
750
+ method === "account/read"
751
+ ? createSyntheticAppServerAccountReadResult()
752
+ : method === "account/rateLimits/read"
753
+ ? createSyntheticAppServerRateLimitsResult()
754
+ : method === "getAuthStatus"
755
+ ? createSyntheticAppServerAuthStatusResult()
756
+ : null;
757
+ if (!result) {
758
+ return line;
759
+ }
760
+ return `${JSON.stringify({
761
+ jsonrpc: typeof message.jsonrpc === "string" ? message.jsonrpc : "2.0",
762
+ id: message.id,
763
+ result,
764
+ })}${lineEnding}`;
765
+ }
766
+
767
+ function forwardToRealCodexOnce(
768
+ codexBin,
769
+ args,
770
+ env = process.env,
771
+ cleanup,
772
+ options = {},
773
+ ) {
774
+ return new Promise((resolve) => {
775
+ let settled = false;
776
+ let stdout = "";
777
+ let stderr = "";
778
+ const captureOutput = options.captureOutput === true;
779
+ const proxyAppServerAccountRead =
780
+ options.proxyAppServerAccountRead === true;
781
+ const protocolProxy = proxyAppServerAccountRead
782
+ ? createAppServerAccountReadProtocolProxy()
783
+ : null;
784
+ let cleanupProtocolProxy = () => {};
785
+ const finalize = async (exitCode) => {
786
+ if (settled) {
787
+ return;
788
+ }
789
+ settled = true;
790
+ cleanupProtocolProxy();
791
+ protocolProxy?.flushOutput();
792
+ try {
793
+ await cleanup?.({ exitCode });
794
+ } catch {
795
+ // Best-effort cleanup only.
796
+ }
797
+ resolve({
798
+ exitCode,
799
+ // No-capture output stays empty by design so retry parsing cannot
800
+ // reintroduce pipes that break terminal passthrough.
801
+ output: `${stdout}\n${stderr}`.trim(),
802
+ });
803
+ };
804
+
805
+ const command = codexBin.launchWithNode ? process.execPath : codexBin.path;
806
+ const commandArgs = codexBin.launchWithNode ? [codexBin.path, ...args] : args;
807
+ let child;
808
+ const failLaunch = (error) => {
809
+ const message = `Failed to launch real Codex CLI: ${String(error)}`;
810
+ stderr += `${stderr ? "\n" : ""}${message}`;
811
+ console.error(message);
812
+ finalize(1);
813
+ };
814
+ try {
815
+ child = spawn(command, commandArgs, {
816
+ stdio: proxyAppServerAccountRead
817
+ ? ["pipe", "pipe", "pipe"]
818
+ : captureOutput
819
+ ? ["inherit", "pipe", "pipe"]
820
+ : "inherit",
821
+ env,
822
+ });
823
+ } catch (error) {
824
+ failLaunch(error);
825
+ return;
826
+ }
827
+
828
+ if (proxyAppServerAccountRead && protocolProxy) {
829
+ let stdinClosed = false;
830
+ let stdoutClosed = false;
831
+ const closeChildStdin = () => {
832
+ if (stdinClosed) return;
833
+ stdinClosed = true;
834
+ protocolProxy.flushInput();
835
+ child.stdin?.end();
836
+ };
837
+ const onProcessStdinData = (chunk) => {
838
+ protocolProxy.observeInput(chunk);
839
+ if (child.stdin && !child.stdin.destroyed && !child.stdin.write(chunk)) {
840
+ process.stdin.pause();
841
+ }
842
+ };
843
+ const onProcessStdinEnd = () => {
844
+ closeChildStdin();
845
+ };
846
+ const onProcessStdinError = () => {
847
+ closeChildStdin();
848
+ };
849
+ const onChildStdinDrain = () => {
850
+ process.stdin.resume();
851
+ };
852
+ const onChildStdoutData = (chunk) => {
853
+ protocolProxy.writeOutput(chunk);
854
+ };
855
+ const onChildStdoutEnd = () => {
856
+ if (stdoutClosed) return;
857
+ stdoutClosed = true;
858
+ protocolProxy.flushOutput();
859
+ };
860
+ const onChildStderrData = (chunk) => {
861
+ process.stderr.write(chunk);
862
+ };
863
+ cleanupProtocolProxy = () => {
864
+ process.stdin.removeListener("data", onProcessStdinData);
865
+ process.stdin.removeListener("end", onProcessStdinEnd);
866
+ process.stdin.removeListener("close", onProcessStdinEnd);
867
+ process.stdin.removeListener("error", onProcessStdinError);
868
+ child.stdin?.removeListener("drain", onChildStdinDrain);
869
+ child.stdout?.removeListener("data", onChildStdoutData);
870
+ child.stdout?.removeListener("end", onChildStdoutEnd);
871
+ child.stderr?.removeListener("data", onChildStderrData);
872
+ process.stdin.resume();
873
+ };
874
+ process.stdin.on("data", onProcessStdinData);
875
+ process.stdin.once("end", onProcessStdinEnd);
876
+ process.stdin.once("close", onProcessStdinEnd);
877
+ process.stdin.once("error", onProcessStdinError);
878
+ child.stdin?.on("drain", onChildStdinDrain);
879
+ child.stdout?.on("data", onChildStdoutData);
880
+ child.stdout?.on("end", onChildStdoutEnd);
881
+ child.stderr?.on("data", onChildStderrData);
882
+ } else if (captureOutput) {
883
+ child.stdout?.on("data", (chunk) => {
884
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
885
+ stdout += text;
886
+ process.stdout.write(chunk);
887
+ });
888
+ child.stderr?.on("data", (chunk) => {
889
+ const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
890
+ stderr += text;
891
+ process.stderr.write(chunk);
892
+ });
893
+ }
894
+
895
+ child.once("error", (error) => {
896
+ failLaunch(error);
897
+ });
898
+
899
+ child.once("close", (code, signal) => {
900
+ if (signal) {
901
+ const signalNumber = signal === "SIGINT" ? 130 : 1;
902
+ finalize(signalNumber);
903
+ return;
904
+ }
905
+ finalize(typeof code === "number" ? code : 1);
906
+ });
907
+ });
908
+ }
909
+
910
+ async function forwardToRealCodex(codexBin, rawArgs, baseEnv = process.env) {
911
+ let currentArgs = [...rawArgs];
912
+ let lastExitCode = 1;
913
+ const attemptedModels = new Set();
914
+
915
+ for (let attempt = 0; attempt < 4; attempt += 1) {
916
+ const requestedModel = extractRequestedModel(currentArgs);
917
+ if (requestedModel) {
918
+ attemptedModels.add(requestedModel);
919
+ }
920
+
921
+ const { args: forwardArgs, requestedModel: compatibilityRequestedModel } =
922
+ buildForwardArgs(currentArgs);
923
+ const compatibility = createCompatibilityCodexHome(
924
+ forwardArgs,
925
+ compatibilityRequestedModel,
926
+ baseEnv,
927
+ );
928
+ const runtimeProxyContext = await createRuntimeRotationProxyContextIfEnabled(
929
+ compatibility,
930
+ rawArgs,
931
+ );
932
+ const result = await forwardToRealCodexOnce(
933
+ codexBin,
934
+ runtimeProxyContext.args,
935
+ runtimeProxyContext.env,
936
+ runtimeProxyContext.cleanup,
937
+ {
938
+ captureOutput: shouldCaptureForwardedOutputForArgs(
939
+ rawArgs,
940
+ runtimeProxyContext.env,
941
+ ),
942
+ proxyAppServerAccountRead:
943
+ isCodexAppServerCommand(rawArgs) &&
944
+ (runtimeProxyContext.proxyAppServerAccountRead === true ||
945
+ (runtimeProxyContext.env[APP_SERVER_ACCOUNT_LABEL_ENV] ?? "").trim() ===
946
+ "1"),
947
+ },
948
+ );
949
+ lastExitCode = result.exitCode;
950
+ if (result.exitCode === 0) {
951
+ return result.exitCode;
952
+ }
953
+
954
+ const fallbackModel = resolveUnsupportedModelRetryTarget(
955
+ requestedModel,
956
+ result.output,
957
+ [...attemptedModels],
958
+ );
959
+ if (!fallbackModel) {
960
+ return result.exitCode;
961
+ }
962
+
963
+ console.error(
964
+ `codex-multi-auth: model ${requestedModel ?? "requested model"} is unsupported on this ChatGPT Codex surface. Retrying with ${fallbackModel}.`,
965
+ );
966
+ attemptedModels.add(fallbackModel);
967
+ const nextArgs = replaceRequestedModel(currentArgs, fallbackModel);
968
+ if (JSON.stringify(nextArgs) === JSON.stringify(currentArgs)) {
969
+ return result.exitCode;
970
+ }
971
+ currentArgs = nextArgs;
972
+ }
973
+
974
+ return lastExitCode;
975
+ }
976
+
977
+ function hasCliAuthCredentialsStoreOverride(args) {
978
+ for (let i = 0; i < args.length; i += 1) {
402
979
  const arg = args[i];
403
980
  if (arg === "-c" || arg === "--config") {
404
981
  const next = args[i + 1];
@@ -424,19 +1001,17 @@ function hasCliAuthCredentialsStoreOverride(args) {
424
1001
  // `lib/request/helpers/model-map.ts` `MODEL_PROFILES`
425
1002
  // and its GPT-5 normalization helpers.
426
1003
  // This wrapper runs before the TypeScript build, so it cannot import that source.
427
- const SUPPORTED_REASONING_EFFORTS_BY_MODEL = {
428
- "gpt-5-codex": ["low", "medium", "high", "xhigh"],
429
- "gpt-5.1-codex-max": ["medium", "high", "xhigh"],
430
- "gpt-5.1-codex-mini": ["medium", "high"],
431
- "gpt-5.5": ["none", "low", "medium", "high", "xhigh"],
432
- "gpt-5.5-pro": ["medium", "high", "xhigh"],
433
- "gpt-5.4": ["none", "low", "medium", "high", "xhigh"],
434
- "gpt-5.4-pro": ["medium", "high", "xhigh"],
435
- "gpt-5.2-pro": ["medium", "high", "xhigh"],
436
- "gpt-5-pro": ["high"],
1004
+ const SUPPORTED_REASONING_EFFORTS_BY_MODEL = {
1005
+ [CURRENT_CODEX_MODEL]: ["low", "medium", "high", "xhigh"],
1006
+ "gpt-5.5": ["none", "low", "medium", "high", "xhigh"],
1007
+ "gpt-5.5-pro": ["medium", "high", "xhigh"],
1008
+ "gpt-5.4": ["none", "low", "medium", "high", "xhigh"],
1009
+ "gpt-5.4-pro": ["medium", "high", "xhigh"],
1010
+ "gpt-5.4-mini": ["medium"],
1011
+ "gpt-5.4-nano": ["medium"],
1012
+ "gpt-5.2-pro": ["medium", "high", "xhigh"],
437
1013
  "gpt-5.2": ["none", "low", "medium", "high", "xhigh"],
438
1014
  "gpt-5.1": ["none", "low", "medium", "high"],
439
- "gpt-5": ["minimal", "low", "medium", "high"],
440
1015
  "gpt-5-mini": ["medium"],
441
1016
  "gpt-5-nano": ["medium"],
442
1017
  };
@@ -452,11 +1027,13 @@ const REASONING_FALLBACKS = {
452
1027
 
453
1028
  const KNOWN_REASONING_EFFORTS = new Set(Object.keys(REASONING_FALLBACKS));
454
1029
  const REQUESTED_MODEL_ALIASES = new Map();
455
- const DEFAULT_GENERAL_GPT5_MODEL = "gpt-5.4";
456
- const GPT_5_5_CANONICAL_MODEL = "gpt-5.5";
457
- const GPT_5_5_PRO_CANONICAL_MODEL = "gpt-5.5-pro";
458
- const GPT_5_5_RELEASE_MODEL = "gpt-5.5-20260423";
459
- const GPT_5_5_PRO_RELEASE_MODEL = "gpt-5.5-pro-20260423";
1030
+ const DEFAULT_GENERAL_GPT5_MODEL = "gpt-5.5";
1031
+ const GPT_5_5_CANONICAL_MODEL = "gpt-5.5";
1032
+ const GPT_5_5_PRO_CANONICAL_MODEL = "gpt-5.5-pro";
1033
+ const GPT_5_5_RELEASE_MODEL = "gpt-5.5-2026-04-23";
1034
+ const GPT_5_5_PRO_RELEASE_MODEL = "gpt-5.5-pro-2026-04-23";
1035
+ const GPT_5_5_RELEASE_COMPAT_MODEL = "gpt-5.5-20260423";
1036
+ const GPT_5_5_PRO_RELEASE_COMPAT_MODEL = "gpt-5.5-pro-20260423";
460
1037
  const GENERAL_GPT5_VERSION_CATALOG = {
461
1038
  1: {
462
1039
  base: "gpt-5.1",
@@ -465,12 +1042,12 @@ const GENERAL_GPT5_VERSION_CATALOG = {
465
1042
  base: "gpt-5.2",
466
1043
  pro: "gpt-5.2-pro",
467
1044
  },
468
- 4: {
469
- base: DEFAULT_GENERAL_GPT5_MODEL,
470
- pro: "gpt-5.4-pro",
471
- mini: "gpt-5-mini",
472
- nano: "gpt-5-nano",
473
- },
1045
+ 4: {
1046
+ base: DEFAULT_GENERAL_GPT5_MODEL,
1047
+ pro: "gpt-5.4-pro",
1048
+ mini: "gpt-5.4-mini",
1049
+ nano: "gpt-5.4-nano",
1050
+ },
474
1051
  5: {
475
1052
  base: GPT_5_5_CANONICAL_MODEL,
476
1053
  pro: GPT_5_5_PRO_CANONICAL_MODEL,
@@ -478,13 +1055,13 @@ const GENERAL_GPT5_VERSION_CATALOG = {
478
1055
  nano: "gpt-5-nano",
479
1056
  },
480
1057
  };
481
- const GENERAL_GPT5_STABLE_VARIANTS = GENERAL_GPT5_VERSION_CATALOG[4];
482
- const GENERAL_GPT5_GENERIC_VARIANTS = {
483
- base: DEFAULT_GENERAL_GPT5_MODEL,
484
- pro: "gpt-5-pro",
485
- mini: "gpt-5-mini",
486
- nano: "gpt-5-nano",
487
- };
1058
+ const GENERAL_GPT5_STABLE_VARIANTS = GENERAL_GPT5_VERSION_CATALOG[5];
1059
+ const GENERAL_GPT5_GENERIC_VARIANTS = {
1060
+ base: DEFAULT_GENERAL_GPT5_MODEL,
1061
+ pro: GPT_5_5_PRO_CANONICAL_MODEL,
1062
+ mini: "gpt-5-mini",
1063
+ nano: "gpt-5-nano",
1064
+ };
488
1065
 
489
1066
  function addRequestedModelAlias(alias, normalizedModel) {
490
1067
  REQUESTED_MODEL_ALIASES.set(alias, normalizedModel);
@@ -515,6 +1092,27 @@ function maybeThrowSimulatedShadowHomePreflightReadBusyError() {
515
1092
  }
516
1093
  }
517
1094
 
1095
+ function maybeThrowSimulatedShadowHomeSyncMetadataBusyError(targetPath) {
1096
+ if (
1097
+ basename(targetPath) === SHADOW_HOME_SYNC_STATE_FILE &&
1098
+ shadowHomeSyncMetadataBusyFailuresRemaining > 0
1099
+ ) {
1100
+ shadowHomeSyncMetadataBusyFailuresRemaining -= 1;
1101
+ const error = new Error("simulated busy shadow-home sync metadata write");
1102
+ error.code = "EBUSY";
1103
+ throw error;
1104
+ }
1105
+ }
1106
+
1107
+ function maybeThrowSimulatedShadowHomeSyncLockOwnerWriteError() {
1108
+ if (shadowHomeSyncLockOwnerWriteFailuresRemaining > 0) {
1109
+ shadowHomeSyncLockOwnerWriteFailuresRemaining -= 1;
1110
+ const error = new Error("simulated shadow sync lock owner write failure");
1111
+ error.code = "EPERM";
1112
+ throw error;
1113
+ }
1114
+ }
1115
+
518
1116
  function writeShadowHomeCleanupRetryMarker(destinationPath, attempt) {
519
1117
  if (shadowHomeCleanupRetryMarkerDir.length === 0) {
520
1118
  return;
@@ -576,42 +1174,50 @@ function seedRequestedModelAliases() {
576
1174
  GPT_5_5_CANONICAL_MODEL,
577
1175
  GPT_5_5_CANONICAL_MODEL,
578
1176
  );
579
- addRequestedModelReasoningAliases(
580
- GPT_5_5_RELEASE_MODEL,
581
- GPT_5_5_CANONICAL_MODEL,
582
- );
1177
+ addRequestedModelReasoningAliases(
1178
+ GPT_5_5_RELEASE_MODEL,
1179
+ GPT_5_5_CANONICAL_MODEL,
1180
+ );
1181
+ addRequestedModelReasoningAliases(
1182
+ GPT_5_5_RELEASE_COMPAT_MODEL,
1183
+ GPT_5_5_CANONICAL_MODEL,
1184
+ );
583
1185
  addRequestedModelReasoningAliases(
584
1186
  GPT_5_5_PRO_CANONICAL_MODEL,
585
1187
  GPT_5_5_PRO_CANONICAL_MODEL,
586
1188
  );
587
- addRequestedModelReasoningAliases(
588
- GPT_5_5_PRO_RELEASE_MODEL,
589
- GPT_5_5_PRO_CANONICAL_MODEL,
590
- );
591
- addRequestedModelReasoningAliases("gpt-5.4", "gpt-5.4");
592
- addRequestedModelReasoningAliases("gpt-5.4-pro", "gpt-5.4-pro");
1189
+ addRequestedModelReasoningAliases(
1190
+ GPT_5_5_PRO_RELEASE_MODEL,
1191
+ GPT_5_5_PRO_CANONICAL_MODEL,
1192
+ );
1193
+ addRequestedModelReasoningAliases(
1194
+ GPT_5_5_PRO_RELEASE_COMPAT_MODEL,
1195
+ GPT_5_5_PRO_CANONICAL_MODEL,
1196
+ );
1197
+ addRequestedModelReasoningAliases("gpt-5.4", "gpt-5.4");
1198
+ addRequestedModelReasoningAliases("gpt-5.4-pro", "gpt-5.4-pro");
1199
+ addRequestedModelReasoningAliases("gpt-5.4-mini", "gpt-5.4-mini");
1200
+ addRequestedModelReasoningAliases("gpt-5.4-nano", "gpt-5.4-nano");
593
1201
  addRequestedModelReasoningAliases("gpt-5.2-pro", "gpt-5.2-pro");
594
- addRequestedModelReasoningAliases("gpt-5-pro", "gpt-5-pro");
1202
+ addRequestedModelReasoningAliases("gpt-5-pro", GPT_5_5_PRO_CANONICAL_MODEL);
595
1203
  addRequestedModelReasoningAliases("gpt-5.2", "gpt-5.2");
596
1204
  addRequestedModelReasoningAliases("gpt-5.1", "gpt-5.1");
597
- addRequestedModelReasoningAliases("gpt-5", "gpt-5");
1205
+ addRequestedModelReasoningAliases("gpt-5", DEFAULT_GENERAL_GPT5_MODEL);
598
1206
  addRequestedModelReasoningAliases("gpt-5-mini", "gpt-5-mini");
599
1207
  addRequestedModelReasoningAliases("gpt-5-nano", "gpt-5-nano");
600
1208
  addRequestedModelReasoningAliases("gpt-5.1-chat-latest", "gpt-5.1");
601
- addRequestedModelReasoningAliases("gpt-5-chat-latest", "gpt-5");
602
- addRequestedModelReasoningAliases("gpt-5.4-mini", "gpt-5-mini");
603
- addRequestedModelReasoningAliases("gpt-5.4-nano", "gpt-5-nano");
604
- addRequestedModelReasoningAliases("gpt-5-codex", "gpt-5-codex");
605
- addRequestedModelReasoningAliases("gpt-5.3-codex-spark", "gpt-5-codex");
606
- addRequestedModelReasoningAliases("gpt-5.3-codex", "gpt-5-codex");
607
- addRequestedModelReasoningAliases("gpt-5.2-codex", "gpt-5-codex");
608
- addRequestedModelReasoningAliases("gpt-5.1-codex", "gpt-5-codex");
609
- addRequestedModelAlias("gpt_5_codex", "gpt-5-codex");
610
- addRequestedModelReasoningAliases("codex-max", "gpt-5.1-codex-max");
611
- addRequestedModelReasoningAliases("gpt-5.1-codex-max", "gpt-5.1-codex-max");
612
- addRequestedModelAlias("codex-mini-latest", "gpt-5.1-codex-mini");
613
- addRequestedModelReasoningAliases("gpt-5-codex-mini", "gpt-5.1-codex-mini");
614
- addRequestedModelReasoningAliases("gpt-5.1-codex-mini", "gpt-5.1-codex-mini");
1209
+ addRequestedModelReasoningAliases("gpt-5-chat-latest", DEFAULT_GENERAL_GPT5_MODEL);
1210
+ addRequestedModelReasoningAliases(CURRENT_CODEX_MODEL, CURRENT_CODEX_MODEL);
1211
+ addRequestedModelReasoningAliases("gpt-5.3-codex-spark", CURRENT_CODEX_MODEL);
1212
+ addRequestedModelReasoningAliases(LEGACY_CODEX_MODEL, CURRENT_CODEX_MODEL);
1213
+ addRequestedModelReasoningAliases("gpt-5.2-codex", CURRENT_CODEX_MODEL);
1214
+ addRequestedModelReasoningAliases("gpt-5.1-codex", CURRENT_CODEX_MODEL);
1215
+ addRequestedModelAlias("gpt_5_codex", CURRENT_CODEX_MODEL);
1216
+ addRequestedModelReasoningAliases("codex-max", CURRENT_CODEX_MODEL);
1217
+ addRequestedModelReasoningAliases("gpt-5.1-codex-max", CURRENT_CODEX_MODEL);
1218
+ addRequestedModelAlias("codex-mini-latest", CURRENT_CODEX_MODEL);
1219
+ addRequestedModelReasoningAliases("gpt-5-codex-mini", CURRENT_CODEX_MODEL);
1220
+ addRequestedModelReasoningAliases("gpt-5.1-codex-mini", CURRENT_CODEX_MODEL);
615
1221
  }
616
1222
 
617
1223
  seedRequestedModelAliases();
@@ -658,24 +1264,24 @@ function resolveStableGeneralGpt5Variant(variant) {
658
1264
 
659
1265
  function resolveCodexRequestedModel(normalized) {
660
1266
  if (
661
- normalized.includes("gpt-5.1-codex-max") ||
662
- normalized.includes("gpt 5.1 codex max") ||
663
- normalized.includes("codex-max")
664
- ) {
665
- return "gpt-5.1-codex-max";
666
- }
1267
+ normalized.includes("gpt-5.1-codex-max") ||
1268
+ normalized.includes("gpt 5.1 codex max") ||
1269
+ normalized.includes("codex-max")
1270
+ ) {
1271
+ return CURRENT_CODEX_MODEL;
1272
+ }
667
1273
  if (
668
1274
  normalized.includes("gpt-5.1-codex-mini") ||
669
1275
  normalized.includes("gpt 5.1 codex mini") ||
670
1276
  normalized.includes("gpt-5-codex-mini") ||
671
- normalized.includes("gpt 5 codex mini") ||
672
- normalized.includes("codex-mini-latest")
673
- ) {
674
- return "gpt-5.1-codex-mini";
675
- }
676
- if (
677
- normalized.includes("gpt-5.3-codex-spark") ||
678
- normalized.includes("gpt 5.3 codex spark") ||
1277
+ normalized.includes("gpt 5 codex mini") ||
1278
+ normalized.includes("codex-mini-latest")
1279
+ ) {
1280
+ return CURRENT_CODEX_MODEL;
1281
+ }
1282
+ if (
1283
+ normalized.includes("gpt-5.3-codex-spark") ||
1284
+ normalized.includes("gpt 5.3 codex spark") ||
679
1285
  normalized.includes("gpt-5.3-codex") ||
680
1286
  normalized.includes("gpt 5.3 codex") ||
681
1287
  normalized.includes("gpt-5.2-codex") ||
@@ -683,11 +1289,11 @@ function resolveCodexRequestedModel(normalized) {
683
1289
  normalized.includes("gpt-5.1-codex") ||
684
1290
  normalized.includes("gpt 5.1 codex") ||
685
1291
  normalized.includes("gpt-5-codex") ||
686
- normalized.includes("gpt 5 codex") ||
687
- normalized === "codex"
688
- ) {
689
- return "gpt-5-codex";
690
- }
1292
+ normalized.includes("gpt 5 codex") ||
1293
+ normalized === "codex"
1294
+ ) {
1295
+ return CURRENT_CODEX_MODEL;
1296
+ }
691
1297
 
692
1298
  return "";
693
1299
  }
@@ -773,12 +1379,12 @@ function coerceReasoningEffortForModel(model, effort) {
773
1379
  }
774
1380
 
775
1381
  function extractRequestedModel(args) {
776
- for (let i = 0; i < args.length; i += 1) {
777
- const arg = args[i];
778
- if (arg === "--model") {
779
- const next = args[i + 1];
780
- if (typeof next === "string" && next.trim().length > 0) {
781
- return next.trim();
1382
+ for (let i = 0; i < args.length; i += 1) {
1383
+ const arg = args[i];
1384
+ if (arg === "--model" || arg === "-m") {
1385
+ const next = args[i + 1];
1386
+ if (typeof next === "string" && next.trim().length > 0) {
1387
+ return next.trim();
782
1388
  }
783
1389
  continue;
784
1390
  }
@@ -860,126 +1466,1421 @@ function rewriteReasoningConfigArgs(rawArgs) {
860
1466
  requestedModel,
861
1467
  )}`;
862
1468
  }
863
- }
1469
+ }
1470
+
1471
+ return {
1472
+ args: nextArgs,
1473
+ requestedModel,
1474
+ };
1475
+ }
1476
+
1477
+ function resolveCodexHomeDir(env = process.env) {
1478
+ const override = (env.CODEX_HOME ?? "").trim();
1479
+ if (override.length > 0) return override;
1480
+ if (process.platform === "win32") {
1481
+ const homeDir = resolveWindowsUserHomeDir();
1482
+ if (homeDir) {
1483
+ return join(homeDir, ".codex");
1484
+ }
1485
+ }
1486
+ return join(env.HOME ?? homedir(), ".codex");
1487
+ }
1488
+
1489
+ function ensureTrailingNewline(value) {
1490
+ return value.endsWith("\n") ? value : `${value}\n`;
1491
+ }
1492
+
1493
+ function captureShadowHomeState(filePath, options = {}) {
1494
+ try {
1495
+ if (!existsSync(filePath)) {
1496
+ return { exists: false, content: null };
1497
+ }
1498
+ if (options.rethrowRetryableReadErrors) {
1499
+ maybeThrowSimulatedShadowHomePreflightReadBusyError();
1500
+ }
1501
+ return {
1502
+ exists: true,
1503
+ content: readFileSync(filePath, "utf8"),
1504
+ };
1505
+ } catch (error) {
1506
+ if (options.rethrowRetryableReadErrors && isRetryableShadowHomeCleanupError(error)) {
1507
+ throw error;
1508
+ }
1509
+ return { exists: true, content: null, unreadable: true };
1510
+ }
1511
+ }
1512
+
1513
+ function shadowHomeStateMatches(left, right) {
1514
+ return (
1515
+ left.exists === right.exists &&
1516
+ left.content === right.content &&
1517
+ Boolean(left.unreadable) === Boolean(right.unreadable)
1518
+ );
1519
+ }
1520
+
1521
+ function hashShadowHomeState(state) {
1522
+ if (state.unreadable) {
1523
+ return null;
1524
+ }
1525
+ if (!state.exists) {
1526
+ return "missing";
1527
+ }
1528
+ if (typeof state.content !== "string") {
1529
+ return null;
1530
+ }
1531
+ return `sha256:${createHash("sha256").update(state.content).digest("hex")}`;
1532
+ }
1533
+
1534
+ function readShadowHomeSyncState(originalCodexHome) {
1535
+ try {
1536
+ const parsed = JSON.parse(
1537
+ readFileSync(join(originalCodexHome, SHADOW_HOME_SYNC_STATE_FILE), "utf8"),
1538
+ );
1539
+ if (
1540
+ !parsed ||
1541
+ typeof parsed !== "object" ||
1542
+ parsed.version !== 1 ||
1543
+ !parsed.files ||
1544
+ typeof parsed.files !== "object"
1545
+ ) {
1546
+ return { version: 1, files: {} };
1547
+ }
1548
+ return parsed;
1549
+ } catch {
1550
+ return { version: 1, files: {} };
1551
+ }
1552
+ }
1553
+
1554
+ function rememberShadowHomeSyncState(
1555
+ originalCodexHome,
1556
+ syncState,
1557
+ name,
1558
+ baseState,
1559
+ syncedState,
1560
+ ) {
1561
+ const baseHash = hashShadowHomeState(baseState);
1562
+ const syncedHash = hashShadowHomeState(syncedState);
1563
+ if (!baseHash || !syncedHash) {
1564
+ return;
1565
+ }
1566
+ syncState.files[name] = {
1567
+ baseHash,
1568
+ syncedHash,
1569
+ updatedAt: Date.now(),
1570
+ };
1571
+ try {
1572
+ writeOwnerOnlyJsonFileAtomicSync(
1573
+ join(originalCodexHome, SHADOW_HOME_SYNC_STATE_FILE),
1574
+ syncState,
1575
+ );
1576
+ } catch {
1577
+ // Best-effort metadata; failed metadata must not fail auth cleanup.
1578
+ }
1579
+ }
1580
+
1581
+ function canRebaseShadowHomeSyncState(syncState, name, baseState, currentState) {
1582
+ const entry = syncState.files?.[name];
1583
+ if (!entry || typeof entry !== "object") {
1584
+ return false;
1585
+ }
1586
+ // Permit a later shadow session to write over an earlier shadow sync from
1587
+ // the same launch snapshot, while still refusing unrelated external edits.
1588
+ return (
1589
+ entry.baseHash === hashShadowHomeState(baseState) &&
1590
+ entry.syncedHash === hashShadowHomeState(currentState)
1591
+ );
1592
+ }
1593
+
1594
+ function readShadowHomeSyncLockOwnerPid(lockPath) {
1595
+ try {
1596
+ const rawOwner = JSON.parse(readFileSync(join(lockPath, "owner.json"), "utf8"));
1597
+ const pid = Number(rawOwner?.pid);
1598
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
1599
+ } catch {
1600
+ return null;
1601
+ }
1602
+ }
1603
+
1604
+ function isShadowHomeSyncLockOldEnoughToSteal(lockPath) {
1605
+ try {
1606
+ const stats = statSync(lockPath);
1607
+ const newestTimestamp = Math.max(stats.mtimeMs, stats.ctimeMs);
1608
+ return Date.now() - newestTimestamp >= SHADOW_HOME_ORPHAN_LOCK_STALE_AGE_MS;
1609
+ } catch {
1610
+ return true;
1611
+ }
1612
+ }
1613
+
1614
+ function removeStaleShadowHomeSyncLock(lockPath) {
1615
+ const ownerPid = readShadowHomeSyncLockOwnerPid(lockPath);
1616
+ if (ownerPid !== null && isProcessAlive(ownerPid)) {
1617
+ return false;
1618
+ }
1619
+ if (ownerPid === null && !isShadowHomeSyncLockOldEnoughToSteal(lockPath)) {
1620
+ return false;
1621
+ }
1622
+ try {
1623
+ removeDirectoryWithRetry(lockPath);
1624
+ if (shadowHomeSyncLockRecreateStaleCount > 0) {
1625
+ shadowHomeSyncLockRecreateStaleCount -= 1;
1626
+ mkdirSync(lockPath, { recursive: true });
1627
+ writeShadowHomeSyncLockOwner(lockPath, {
1628
+ pid: 2_147_483_647,
1629
+ createdAt: 1,
1630
+ });
1631
+ }
1632
+ return true;
1633
+ } catch {
1634
+ return false;
1635
+ }
1636
+ }
1637
+
1638
+ function writeShadowHomeSyncLockOwner(lockPath, owner) {
1639
+ const ownerPath = join(lockPath, "owner.json");
1640
+ maybeThrowSimulatedShadowHomeSyncLockOwnerWriteError();
1641
+ writeFileSync(ownerPath, `${JSON.stringify(owner)}\n`, {
1642
+ encoding: "utf8",
1643
+ mode: 0o600,
1644
+ });
1645
+ try {
1646
+ chmodSync(ownerPath, 0o600);
1647
+ } catch {
1648
+ // Best-effort only; permission semantics vary by platform.
1649
+ }
1650
+ }
1651
+
1652
+ function writeShadowHomeSyncLockOwnerWithRetry(lockPath, owner) {
1653
+ for (let attempt = 0; attempt <= SHADOW_HOME_CLEANUP_BACKOFF_MS.length; attempt += 1) {
1654
+ try {
1655
+ writeShadowHomeSyncLockOwner(lockPath, owner);
1656
+ return;
1657
+ } catch (error) {
1658
+ if (
1659
+ !isRetryableShadowHomeCleanupError(error) ||
1660
+ attempt === SHADOW_HOME_CLEANUP_BACKOFF_MS.length
1661
+ ) {
1662
+ throw error;
1663
+ }
1664
+ sleepSync(SHADOW_HOME_CLEANUP_BACKOFF_MS[attempt]);
1665
+ }
1666
+ }
1667
+ }
1668
+
1669
+ function acquireShadowHomeSyncLock(originalCodexHome) {
1670
+ const lockPath = join(originalCodexHome, SHADOW_HOME_SYNC_LOCK_DIR);
1671
+ mkdirSync(originalCodexHome, { recursive: true });
1672
+ const maxStaleRecoveries = SHADOW_HOME_CLEANUP_BACKOFF_MS.length + 1;
1673
+ let staleRecoveries = 0;
1674
+ let attempt = 0;
1675
+ const deadline = Date.now() + SHADOW_HOME_SYNC_LOCK_WAIT_TIMEOUT_MS;
1676
+ while (true) {
1677
+ try {
1678
+ mkdirSync(lockPath);
1679
+ try {
1680
+ writeShadowHomeSyncLockOwnerWithRetry(lockPath, {
1681
+ pid: process.pid,
1682
+ createdAt: Date.now(),
1683
+ });
1684
+ } catch (error) {
1685
+ try {
1686
+ removeDirectoryWithRetry(lockPath);
1687
+ } catch {
1688
+ // Preserve the owner write failure while avoiding orphaned locks when possible.
1689
+ }
1690
+ throw error;
1691
+ }
1692
+ return () => {
1693
+ try {
1694
+ removeDirectoryWithRetry(lockPath);
1695
+ } catch {
1696
+ // Best-effort lock cleanup only.
1697
+ }
1698
+ };
1699
+ } catch (error) {
1700
+ const code =
1701
+ error && typeof error === "object" && "code" in error
1702
+ ? error.code
1703
+ : undefined;
1704
+ if (code !== "EEXIST") {
1705
+ throw error;
1706
+ }
1707
+ if (
1708
+ staleRecoveries < maxStaleRecoveries &&
1709
+ removeStaleShadowHomeSyncLock(lockPath)
1710
+ ) {
1711
+ staleRecoveries += 1;
1712
+ attempt = 0;
1713
+ continue;
1714
+ }
1715
+ const remainingMs = deadline - Date.now();
1716
+ if (remainingMs <= 0) {
1717
+ throw error;
1718
+ }
1719
+ const backoffMs =
1720
+ SHADOW_HOME_CLEANUP_BACKOFF_MS[
1721
+ Math.min(attempt, SHADOW_HOME_CLEANUP_BACKOFF_MS.length - 1)
1722
+ ] ?? SHADOW_HOME_CLEANUP_BACKOFF_MS[0];
1723
+ sleepSync(Math.min(backoffMs, remainingMs));
1724
+ attempt += 1;
1725
+ }
1726
+ }
1727
+ }
1728
+
1729
+ function syncShadowHomeStateFile(
1730
+ sourcePath,
1731
+ destinationPath,
1732
+ expectedDestinationState,
1733
+ ) {
1734
+ const tempPath = join(
1735
+ dirname(destinationPath),
1736
+ `.${basename(destinationPath)}.codex-multi-auth-sync-${process.pid}.tmp`,
1737
+ );
1738
+ try {
1739
+ mkdirSync(dirname(destinationPath), { recursive: true });
1740
+ copyFileSync(sourcePath, tempPath);
1741
+ renameFileWithRetry(tempPath, destinationPath, expectedDestinationState);
1742
+ } catch (error) {
1743
+ try {
1744
+ rmSync(tempPath, { force: true });
1745
+ } catch {
1746
+ // Best-effort cleanup only.
1747
+ }
1748
+ throw error;
1749
+ }
1750
+ }
1751
+
1752
+ function syncShadowHomeStateFileBestEffort(
1753
+ sourcePath,
1754
+ destinationPath,
1755
+ expectedDestinationState,
1756
+ tightenFile,
1757
+ ) {
1758
+ try {
1759
+ syncShadowHomeStateFile(
1760
+ sourcePath,
1761
+ destinationPath,
1762
+ expectedDestinationState,
1763
+ );
1764
+ tightenFile(destinationPath);
1765
+ return true;
1766
+ } catch {
1767
+ // Best-effort sync-back: keep attempting sibling files after Windows locks.
1768
+ return false;
1769
+ }
1770
+ }
1771
+
1772
+ function isDirectoryLike(path) {
1773
+ try {
1774
+ return statSync(path).isDirectory();
1775
+ } catch {
1776
+ return false;
1777
+ }
1778
+ }
1779
+
1780
+ function isFileLike(path) {
1781
+ try {
1782
+ return statSync(path).isFile();
1783
+ } catch {
1784
+ return false;
1785
+ }
1786
+ }
1787
+
1788
+ function mirrorDirectoryIntoShadowHome(sourcePath, destinationPath) {
1789
+ try {
1790
+ if ((process.env.CODEX_MULTI_AUTH_TEST_FORCE_SHADOW_DIR_COPY ?? "").trim() === "1") {
1791
+ throw new Error("simulated directory link failure");
1792
+ }
1793
+ symlinkSync(
1794
+ sourcePath,
1795
+ destinationPath,
1796
+ process.platform === "win32" ? "junction" : "dir",
1797
+ );
1798
+ return "linked";
1799
+ } catch {
1800
+ // Fall back to a copy when links are unavailable. Directory links are
1801
+ // preferred because they keep sessions, plugins, and skills live.
1802
+ }
1803
+ cpSync(sourcePath, destinationPath, {
1804
+ recursive: true,
1805
+ dereference: false,
1806
+ });
1807
+ return "copied";
1808
+ }
1809
+
1810
+ function linkFileIntoShadowHome(sourcePath, destinationPath) {
1811
+ try {
1812
+ symlinkSync(sourcePath, destinationPath, "file");
1813
+ return true;
1814
+ } catch {
1815
+ // File symlinks keep SQLite/cache-style root files realtime when allowed.
1816
+ }
1817
+ try {
1818
+ linkSync(sourcePath, destinationPath);
1819
+ return true;
1820
+ } catch {
1821
+ // Hard links cover platforms where file symlinks require extra privileges.
1822
+ }
1823
+ return false;
1824
+ }
1825
+
1826
+ function mirrorFileIntoShadowHome(sourcePath, destinationPath, tightenFile) {
1827
+ if (linkFileIntoShadowHome(sourcePath, destinationPath)) {
1828
+ return;
1829
+ }
1830
+ copyFileSync(sourcePath, destinationPath);
1831
+ tightenFile(destinationPath);
1832
+ }
1833
+
1834
+ function collectShadowHomeSyncFileNames(shadowCodexHome, syncFileNames) {
1835
+ try {
1836
+ for (const entry of readdirSync(shadowCodexHome, { withFileTypes: true })) {
1837
+ const name = entry.name;
1838
+ if (
1839
+ name === SHADOW_HOME_CONFIG_FILE ||
1840
+ name === SHADOW_HOME_SYNC_STATE_FILE ||
1841
+ syncFileNames.has(name)
1842
+ ) {
1843
+ continue;
1844
+ }
1845
+ const shadowPath = join(shadowCodexHome, name);
1846
+ let fileLike = entry.isFile();
1847
+ if (entry.isSymbolicLink()) {
1848
+ fileLike = isFileLike(shadowPath);
1849
+ }
1850
+ if (fileLike) {
1851
+ syncFileNames.add(name);
1852
+ }
1853
+ }
1854
+ } catch {
1855
+ // Best-effort; cleanup still syncs the known state files.
1856
+ }
1857
+ return syncFileNames;
1858
+ }
1859
+
1860
+ function syncCopiedShadowHomeDirectories(originalCodexHome, shadowCodexHome, names) {
1861
+ for (const name of names) {
1862
+ const shadowPath = join(shadowCodexHome, name);
1863
+ if (!isDirectoryLike(shadowPath)) {
1864
+ continue;
1865
+ }
1866
+ try {
1867
+ cpSync(shadowPath, join(originalCodexHome, name), {
1868
+ recursive: true,
1869
+ dereference: false,
1870
+ force: true,
1871
+ });
1872
+ } catch {
1873
+ // Best-effort sync-back; sibling directories and state files still run.
1874
+ }
1875
+ }
1876
+ }
1877
+
1878
+ function syncShadowHomeAuthBundle(
1879
+ originalCodexHome,
1880
+ shadowCodexHome,
1881
+ originalFileStates,
1882
+ tightenFile,
1883
+ skipSyncBackNames = new Set(),
1884
+ ) {
1885
+ const syncState = readShadowHomeSyncState(originalCodexHome);
1886
+ for (const name of SHADOW_HOME_STATE_FILES) {
1887
+ if (skipSyncBackNames.has(name)) {
1888
+ continue;
1889
+ }
1890
+ const shadowPath = join(shadowCodexHome, name);
1891
+ const shadowState = captureShadowHomeState(shadowPath);
1892
+ if (!shadowState.exists || shadowState.unreadable) {
1893
+ continue;
1894
+ }
1895
+ const originalPath = join(originalCodexHome, name);
1896
+ const originalSnapshot =
1897
+ originalFileStates.get(name) ?? { exists: false, content: null };
1898
+ const currentOriginalState = captureShadowHomeState(originalPath);
1899
+ let expectedDestinationState = originalSnapshot;
1900
+ if (!shadowHomeStateMatches(currentOriginalState, originalSnapshot)) {
1901
+ if (
1902
+ !canRebaseShadowHomeSyncState(
1903
+ syncState,
1904
+ name,
1905
+ originalSnapshot,
1906
+ currentOriginalState,
1907
+ )
1908
+ ) {
1909
+ continue;
1910
+ }
1911
+ expectedDestinationState = currentOriginalState;
1912
+ }
1913
+ if (
1914
+ expectedDestinationState.unreadable ||
1915
+ shadowHomeStateMatches(shadowState, expectedDestinationState)
1916
+ ) {
1917
+ continue;
1918
+ }
1919
+ if (syncShadowHomeStateFileBestEffort(
1920
+ shadowPath,
1921
+ originalPath,
1922
+ expectedDestinationState,
1923
+ tightenFile,
1924
+ )) {
1925
+ rememberShadowHomeSyncState(
1926
+ originalCodexHome,
1927
+ syncState,
1928
+ name,
1929
+ originalSnapshot,
1930
+ shadowState,
1931
+ );
1932
+ }
1933
+ }
1934
+ }
1935
+
1936
+ function syncAdditionalShadowHomeFiles(
1937
+ originalCodexHome,
1938
+ shadowCodexHome,
1939
+ names,
1940
+ originalFileStates,
1941
+ tightenFile,
1942
+ skipSyncBackNames = new Set(),
1943
+ ) {
1944
+ for (const name of names) {
1945
+ if (SHADOW_HOME_STATE_FILE_SET.has(name) || skipSyncBackNames.has(name)) {
1946
+ continue;
1947
+ }
1948
+ const shadowPath = join(shadowCodexHome, name);
1949
+ const shadowState = captureShadowHomeState(shadowPath);
1950
+ if (!shadowState.exists || shadowState.unreadable) {
1951
+ continue;
1952
+ }
1953
+
1954
+ const originalPath = join(originalCodexHome, name);
1955
+ const originalSnapshot =
1956
+ originalFileStates.get(name) ?? { exists: false, content: null };
1957
+ const currentOriginalState = captureShadowHomeState(originalPath);
1958
+ if (!shadowHomeStateMatches(currentOriginalState, originalSnapshot)) {
1959
+ continue;
1960
+ }
1961
+ if (shadowHomeStateMatches(shadowState, originalSnapshot)) {
1962
+ continue;
1963
+ }
1964
+ syncShadowHomeStateFileBestEffort(
1965
+ shadowPath,
1966
+ originalPath,
1967
+ originalSnapshot,
1968
+ tightenFile,
1969
+ );
1970
+ }
1971
+ }
1972
+
1973
+ function createShadowHomeMirror(
1974
+ originalCodexHome,
1975
+ shadowCodexHome,
1976
+ tightenFile,
1977
+ options = {},
1978
+ ) {
1979
+ const syncFileNames = new Set(SHADOW_HOME_STATE_FILES);
1980
+ const skipSyncBackNames = new Set(options.skipSyncBackNames ?? []);
1981
+ const originalFileStates = new Map();
1982
+ const copiedDirectoryNames = new Set();
1983
+ const rememberSyncFile = (name) => {
1984
+ if (!originalFileStates.has(name)) {
1985
+ originalFileStates.set(
1986
+ name,
1987
+ captureShadowHomeState(join(originalCodexHome, name)),
1988
+ );
1989
+ }
1990
+ syncFileNames.add(name);
1991
+ };
1992
+ for (const name of SHADOW_HOME_STATE_FILES) {
1993
+ rememberSyncFile(name);
1994
+ }
1995
+
1996
+ if (existsSync(originalCodexHome)) {
1997
+ for (const entry of readdirSync(originalCodexHome, { withFileTypes: true })) {
1998
+ const name = entry.name;
1999
+ if (
2000
+ name === SHADOW_HOME_CONFIG_FILE ||
2001
+ name === SHADOW_HOME_SYNC_STATE_FILE ||
2002
+ name === SHADOW_HOME_SYNC_LOCK_DIR
2003
+ ) {
2004
+ continue;
2005
+ }
2006
+ const isKnownStateFile = SHADOW_HOME_STATE_FILE_SET.has(name);
2007
+ const sourcePath = join(originalCodexHome, name);
2008
+ const destinationPath = join(shadowCodexHome, name);
2009
+ if (existsSync(destinationPath)) {
2010
+ continue;
2011
+ }
2012
+
2013
+ let directoryLike = entry.isDirectory();
2014
+ let fileLike = entry.isFile();
2015
+ if (entry.isSymbolicLink()) {
2016
+ directoryLike = isDirectoryLike(sourcePath);
2017
+ fileLike = !directoryLike && isFileLike(sourcePath);
2018
+ }
2019
+
2020
+ try {
2021
+ if (isKnownStateFile && !fileLike) {
2022
+ throw new Error(`Expected ${name} to be a file`);
2023
+ }
2024
+ if (directoryLike) {
2025
+ if (mirrorDirectoryIntoShadowHome(sourcePath, destinationPath) === "copied") {
2026
+ copiedDirectoryNames.add(name);
2027
+ }
2028
+ continue;
2029
+ }
2030
+ if (fileLike) {
2031
+ rememberSyncFile(name);
2032
+ if (isKnownStateFile) {
2033
+ copyFileSync(sourcePath, destinationPath);
2034
+ tightenFile(destinationPath);
2035
+ } else {
2036
+ mirrorFileIntoShadowHome(sourcePath, destinationPath, tightenFile);
2037
+ }
2038
+ }
2039
+ } catch (error) {
2040
+ if (isKnownStateFile) {
2041
+ throw error;
2042
+ }
2043
+ // A missing or locked optional home entry should not block runtime
2044
+ // launch; auth/config files still get handled explicitly.
2045
+ }
2046
+ }
2047
+ }
2048
+
2049
+ return () => {
2050
+ let releaseLock = () => {};
2051
+ try {
2052
+ const names = collectShadowHomeSyncFileNames(shadowCodexHome, syncFileNames);
2053
+ releaseLock = acquireShadowHomeSyncLock(originalCodexHome);
2054
+ syncShadowHomeAuthBundle(
2055
+ originalCodexHome,
2056
+ shadowCodexHome,
2057
+ originalFileStates,
2058
+ tightenFile,
2059
+ skipSyncBackNames,
2060
+ );
2061
+ syncCopiedShadowHomeDirectories(
2062
+ originalCodexHome,
2063
+ shadowCodexHome,
2064
+ copiedDirectoryNames,
2065
+ );
2066
+ syncAdditionalShadowHomeFiles(
2067
+ originalCodexHome,
2068
+ shadowCodexHome,
2069
+ names,
2070
+ originalFileStates,
2071
+ tightenFile,
2072
+ skipSyncBackNames,
2073
+ );
2074
+ } catch {
2075
+ // Best-effort only; runtime auth refreshes should not fail cleanup.
2076
+ } finally {
2077
+ releaseLock();
2078
+ }
2079
+ };
2080
+ }
2081
+
2082
+ function rewriteConfigTomlReasoningEffort(rawConfig, requestedModel) {
2083
+ const lineEnding = rawConfig.includes("\r\n") ? "\r\n" : "\n";
2084
+ let changed = false;
2085
+ const nextLines = rawConfig.split(/\r?\n/).map((line) => {
2086
+ const quotedMatch = line.match(
2087
+ /^(\s*model_reasoning_effort\s*=\s*)(["'])([^"']+)(\2.*)$/,
2088
+ );
2089
+ const bareMatch = quotedMatch
2090
+ ? null
2091
+ : line.match(
2092
+ /^(\s*model_reasoning_effort\s*=\s*)([^\s#]+)(\s*(?:#.*)?)$/,
2093
+ );
2094
+ if (!quotedMatch && !bareMatch) return line;
2095
+
2096
+ const prefix = quotedMatch?.[1] ?? bareMatch?.[1] ?? "";
2097
+ const openingQuote = quotedMatch?.[2] ?? "";
2098
+ const currentEffort = quotedMatch?.[3] ?? bareMatch?.[2] ?? "";
2099
+ const suffix = quotedMatch?.[4] ?? bareMatch?.[3] ?? "";
2100
+ const coercedEffort = coerceReasoningEffortForModel(
2101
+ requestedModel,
2102
+ currentEffort,
2103
+ );
2104
+ if (coercedEffort === currentEffort) {
2105
+ return line;
2106
+ }
2107
+
2108
+ changed = true;
2109
+ return quotedMatch
2110
+ ? `${prefix}${openingQuote}${coercedEffort}${suffix}`
2111
+ : `${prefix}${coercedEffort}${suffix}`;
2112
+ });
2113
+
2114
+ if (!changed) {
2115
+ return rawConfig;
2116
+ }
2117
+
2118
+ return ensureTrailingNewline(nextLines.join(lineEnding));
2119
+ }
2120
+
2121
+ function resolveOriginalMultiAuthDir(env) {
2122
+ const explicit = (env.CODEX_MULTI_AUTH_DIR ?? "").trim();
2123
+ if (explicit.length > 0) {
2124
+ return explicit;
2125
+ }
2126
+ return undefined;
2127
+ }
2128
+
2129
+ function parseRuntimeRotationProxyEnv(value) {
2130
+ if (value === undefined) return undefined;
2131
+ const normalized = value.trim().toLowerCase();
2132
+ if (normalized.length === 0) return undefined;
2133
+ if (normalized === "1" || normalized === "true" || normalized === "yes") {
2134
+ return true;
2135
+ }
2136
+ if (normalized === "0" || normalized === "false" || normalized === "no") {
2137
+ return false;
2138
+ }
2139
+ if (!warnedInvalidRuntimeRotationProxyEnv) {
2140
+ warnedInvalidRuntimeRotationProxyEnv = true;
2141
+ console.error(
2142
+ "codex-multi-auth: ignoring invalid CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY value. Expected 0/1, true/false, or yes/no.",
2143
+ );
2144
+ }
2145
+ return undefined;
2146
+ }
2147
+
2148
+ async function isRuntimeRotationProxyEnabled(rawArgs, baseEnv = process.env) {
2149
+ if ((baseEnv.CODEX_MULTI_AUTH_BYPASS ?? "").trim() === "1") {
2150
+ return false;
2151
+ }
2152
+ if (!shouldUseRuntimeRoutingForForwardedArgs(rawArgs)) {
2153
+ return false;
2154
+ }
2155
+
2156
+ const envOverride = parseRuntimeRotationProxyEnv(
2157
+ baseEnv.CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY,
2158
+ );
2159
+ if (envOverride !== undefined) {
2160
+ return envOverride;
2161
+ }
2162
+
2163
+ const configModule = await loadRuntimeRotationConfigModule();
2164
+ if (!configModule) {
2165
+ return false;
2166
+ }
2167
+ const pluginConfig = configModule.loadPluginConfig();
2168
+ return configModule.getCodexRuntimeRotationProxy(pluginConfig) === true;
2169
+ }
2170
+
2171
+ function createRuntimeRotationProxyClientApiKey() {
2172
+ return randomBytes(32).toString("hex");
2173
+ }
2174
+
2175
+ function omitRuntimeRotationShadowHomeStateFiles(shadowCodexHome) {
2176
+ for (const name of RUNTIME_ROTATION_SHADOW_HOME_OMIT_STATE_FILES) {
2177
+ const targetPath = join(shadowCodexHome, name);
2178
+ try {
2179
+ if (!existsSync(targetPath)) {
2180
+ continue;
2181
+ }
2182
+ if (isDirectoryLike(targetPath)) {
2183
+ removeDirectoryWithRetry(targetPath);
2184
+ } else {
2185
+ rmSync(targetPath, { force: true });
2186
+ }
2187
+ } catch {
2188
+ // Best-effort: stale official auth state should not block runtime rotation.
2189
+ }
2190
+ }
2191
+ }
2192
+
2193
+ function resolveRuntimeRotationProxyOriginalCodexHome(baseEnv) {
2194
+ const override = (baseEnv[APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV] ?? "").trim();
2195
+ return override || resolveCodexHomeDir(baseEnv);
2196
+ }
2197
+
2198
+ function createRuntimeRotationProxyCodexHome(
2199
+ baseEnv,
2200
+ proxyBaseUrl,
2201
+ clientApiKey,
2202
+ configTomlModule,
2203
+ ) {
2204
+ const originalCodexHome = resolveRuntimeRotationProxyOriginalCodexHome(baseEnv);
2205
+ const shadowCodexHome = mkdtempSync(join(tmpdir(), "codex-multi-auth-runtime-home-"));
2206
+ let syncShadowHomeStateBack = () => {};
2207
+ const cleanup = () => {
2208
+ try {
2209
+ removeDirectoryWithRetry(shadowCodexHome);
2210
+ } catch {
2211
+ // Best-effort cleanup only.
2212
+ }
2213
+ };
2214
+ const tightenShadowHomePermissions = (path) => {
2215
+ try {
2216
+ chmodSync(path, 0o600);
2217
+ } catch {
2218
+ // Best-effort only; permission semantics vary by platform.
2219
+ }
2220
+ };
2221
+
2222
+ try {
2223
+ syncShadowHomeStateBack = createShadowHomeMirror(
2224
+ originalCodexHome,
2225
+ shadowCodexHome,
2226
+ tightenShadowHomePermissions,
2227
+ {
2228
+ skipSyncBackNames: RUNTIME_ROTATION_SHADOW_HOME_OMIT_STATE_FILES,
2229
+ },
2230
+ );
2231
+ omitRuntimeRotationShadowHomeStateFiles(shadowCodexHome);
2232
+ const originalConfigPath = join(originalCodexHome, "config.toml");
2233
+ const rawConfig = existsSync(originalConfigPath)
2234
+ ? readFileSync(originalConfigPath, "utf8")
2235
+ : "";
2236
+ const runtimeConfig = configTomlModule.rewriteConfigTomlForRuntimeRotationProvider(
2237
+ rawConfig,
2238
+ proxyBaseUrl,
2239
+ clientApiKey,
2240
+ );
2241
+ const runtimeConfigPath = join(shadowCodexHome, "config.toml");
2242
+ writeFileSync(runtimeConfigPath, runtimeConfig, "utf8");
2243
+ tightenShadowHomePermissions(runtimeConfigPath);
2244
+ } catch (error) {
2245
+ cleanup();
2246
+ throw error;
2247
+ }
2248
+
2249
+ const forwardedEnv = {
2250
+ ...baseEnv,
2251
+ CODEX_HOME: shadowCodexHome,
2252
+ OPENAI_API_KEY: clientApiKey,
2253
+ };
2254
+ const originalMultiAuthDir = resolveOriginalMultiAuthDir(baseEnv);
2255
+ if (originalMultiAuthDir) {
2256
+ forwardedEnv.CODEX_MULTI_AUTH_DIR = originalMultiAuthDir;
2257
+ }
2258
+
2259
+ return {
2260
+ env: forwardedEnv,
2261
+ cleanup: () => {
2262
+ syncShadowHomeStateBack();
2263
+ cleanup();
2264
+ },
2265
+ };
2266
+ }
2267
+
2268
+ function appendNodeImportOption(nodeOptions, preloadPath) {
2269
+ const importOption = `--import=${pathToFileURL(preloadPath).href}`;
2270
+ const trimmed = (nodeOptions ?? "").trim();
2271
+ return trimmed.length > 0 ? `${trimmed} ${importOption}` : importOption;
2272
+ }
2273
+
2274
+ function createRuntimeRotationAppServerPreloadSource(wrapperScriptPath) {
2275
+ return [
2276
+ 'import { spawn } from "node:child_process";',
2277
+ 'import { basename } from "node:path";',
2278
+ 'import process from "node:process";',
2279
+ "",
2280
+ `const wrapperScriptPath = ${JSON.stringify(wrapperScriptPath)};`,
2281
+ `const accountLabelEnv = ${JSON.stringify(APP_SERVER_ACCOUNT_LABEL_ENV)};`,
2282
+ "const rawArgs = process.argv.slice(1);",
2283
+ "const firstArg = rawArgs[0] ?? \"\";",
2284
+ 'if (basename(firstArg).toLowerCase() === "app-server") {',
2285
+ ' const args = ["app-server", ...rawArgs.slice(1)];',
2286
+ " const env = {",
2287
+ " ...process.env,",
2288
+ ' CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY: "0",',
2289
+ " [accountLabelEnv]: \"1\",",
2290
+ " };",
2291
+ " const child = spawn(process.execPath, [wrapperScriptPath, ...args], {",
2292
+ " stdio: \"inherit\",",
2293
+ " env,",
2294
+ " });",
2295
+ " child.once(\"error\", (error) => {",
2296
+ ' console.error(`codex-multi-auth app-server shim failed: ${error instanceof Error ? error.message : String(error)}`);',
2297
+ " process.exit(1);",
2298
+ " });",
2299
+ " child.once(\"exit\", (code, signal) => {",
2300
+ " if (signal) {",
2301
+ ' process.exit(signal === "SIGINT" ? 130 : 1);',
2302
+ " return;",
2303
+ " }",
2304
+ " process.exit(typeof code === \"number\" ? code : 1);",
2305
+ " });",
2306
+ " await new Promise(() => undefined);",
2307
+ "}",
2308
+ "",
2309
+ ].join("\n");
2310
+ }
2311
+
2312
+ function sweepStaleRuntimeRotationAppServerShimDirs(shimRootDir) {
2313
+ let entries = [];
2314
+ try {
2315
+ entries = readdirSync(shimRootDir, { withFileTypes: true });
2316
+ } catch {
2317
+ return;
2318
+ }
2319
+ for (const entry of entries) {
2320
+ if (!entry.isDirectory() || !entry.name.startsWith(APP_SERVER_SHIM_HELPER_PREFIX)) {
2321
+ continue;
2322
+ }
2323
+ const pidText = entry.name.slice(APP_SERVER_SHIM_HELPER_PREFIX.length);
2324
+ const pid = Number.parseInt(pidText, 10);
2325
+ if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid || isProcessAlive(pid)) {
2326
+ continue;
2327
+ }
2328
+ try {
2329
+ removeDirectoryWithRetry(join(shimRootDir, entry.name));
2330
+ } catch {
2331
+ // Best-effort stale shim cleanup only.
2332
+ }
2333
+ }
2334
+ }
2335
+
2336
+ function installRuntimeRotationAppServerCliShim(forwardedEnv) {
2337
+ const shadowCodexHome = forwardedEnv.CODEX_HOME;
2338
+ if (!shadowCodexHome) {
2339
+ throw new Error("runtime app-server shim requires CODEX_HOME");
2340
+ }
2341
+ const multiAuthDir =
2342
+ resolveOriginalMultiAuthDir(forwardedEnv) ??
2343
+ join(resolveRuntimeRotationProxyOriginalCodexHome(forwardedEnv), "multi-auth");
2344
+ const shimRootDir = join(multiAuthDir, APP_SERVER_SHIM_DIR_NAME);
2345
+ sweepStaleRuntimeRotationAppServerShimDirs(shimRootDir);
2346
+ const shimDir = join(
2347
+ shimRootDir,
2348
+ `${APP_SERVER_SHIM_HELPER_PREFIX}${process.pid}`,
2349
+ );
2350
+ mkdirSync(shimDir, { recursive: true });
2351
+ const executableName = process.platform === "win32" ? "codex.exe" : "codex";
2352
+ const executablePath = join(shimDir, executableName);
2353
+ const preloadPath = join(shimDir, "codex-multi-auth-app-server-preload.mjs");
2354
+ try {
2355
+ try {
2356
+ rmSync(executablePath, { force: true });
2357
+ } catch {
2358
+ // Best-effort stale shim cleanup only.
2359
+ }
2360
+ try {
2361
+ linkSync(process.execPath, executablePath);
2362
+ } catch {
2363
+ copyFileSync(process.execPath, executablePath);
2364
+ }
2365
+ if (process.platform !== "win32") {
2366
+ chmodSync(executablePath, 0o755);
2367
+ }
2368
+ writeFileSync(
2369
+ preloadPath,
2370
+ createRuntimeRotationAppServerPreloadSource(fileURLToPath(import.meta.url)),
2371
+ { encoding: "utf8", mode: 0o600 },
2372
+ );
2373
+ try {
2374
+ chmodSync(preloadPath, 0o600);
2375
+ } catch {
2376
+ // Best-effort only; permission semantics vary by platform.
2377
+ }
2378
+ } catch (error) {
2379
+ try {
2380
+ removeDirectoryWithRetry(shimDir);
2381
+ } catch {
2382
+ // Preserve the original installation failure.
2383
+ }
2384
+ throw error;
2385
+ }
2386
+ forwardedEnv.CODEX_CLI_PATH = shimDir;
2387
+ forwardedEnv.NODE_OPTIONS = appendNodeImportOption(
2388
+ forwardedEnv.NODE_OPTIONS,
2389
+ preloadPath,
2390
+ );
2391
+ forwardedEnv.CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY = "0";
2392
+ forwardedEnv[APP_SERVER_ACCOUNT_LABEL_ENV] = "1";
2393
+ return shimDir;
2394
+ }
2395
+
2396
+ function resolveRuntimeRotationAppHelperStatusPath(env = process.env) {
2397
+ const multiAuthDir =
2398
+ resolveOriginalMultiAuthDir(env) ?? join(resolveCodexHomeDir(env), "multi-auth");
2399
+ return join(multiAuthDir, APP_RUNTIME_HELPER_STATUS_FILE);
2400
+ }
2401
+
2402
+ function writeOwnerOnlyJsonFileAtomicSync(targetPath, payload) {
2403
+ const targetDir = dirname(targetPath);
2404
+ mkdirSync(targetDir, { recursive: true });
2405
+ for (let attempt = 0; attempt <= SHADOW_HOME_CLEANUP_BACKOFF_MS.length; attempt += 1) {
2406
+ const tempPath = join(
2407
+ targetDir,
2408
+ [
2409
+ `.${basename(targetPath)}`,
2410
+ String(process.pid),
2411
+ String(Date.now()),
2412
+ randomBytes(4).toString("hex"),
2413
+ "tmp",
2414
+ ].join("."),
2415
+ );
2416
+ try {
2417
+ writeFileSync(tempPath, `${JSON.stringify(payload, null, 2)}\n`, {
2418
+ encoding: "utf8",
2419
+ mode: 0o600,
2420
+ });
2421
+ chmodSync(tempPath, 0o600);
2422
+ maybeThrowSimulatedShadowHomeSyncMetadataBusyError(targetPath);
2423
+ renameSync(tempPath, targetPath);
2424
+ chmodSync(targetPath, 0o600);
2425
+ return;
2426
+ } catch (error) {
2427
+ try {
2428
+ rmSync(tempPath, { force: true });
2429
+ } catch {
2430
+ // Preserve the original write failure.
2431
+ }
2432
+ if (
2433
+ isRetryableShadowHomeCleanupError(error) &&
2434
+ attempt < SHADOW_HOME_CLEANUP_BACKOFF_MS.length
2435
+ ) {
2436
+ sleepSync(SHADOW_HOME_CLEANUP_BACKOFF_MS[attempt]);
2437
+ continue;
2438
+ }
2439
+ throw error;
2440
+ }
2441
+ }
2442
+ }
2443
+
2444
+ function resolveRuntimeRotationAppHelperIdleMs(env = process.env) {
2445
+ const parsed = Number.parseInt(
2446
+ env.CODEX_MULTI_AUTH_APP_ROTATION_IDLE_MS ?? "",
2447
+ 10,
2448
+ );
2449
+ return Number.isFinite(parsed) && parsed > 0
2450
+ ? Math.max(50, parsed)
2451
+ : DEFAULT_APP_RUNTIME_HELPER_IDLE_MS;
2452
+ }
2453
+
2454
+ function resolveRuntimeRotationAppHelperOwnerPid(env = process.env) {
2455
+ const parsed = Number.parseInt(env[APP_RUNTIME_HELPER_OWNER_PID_ENV] ?? "", 10);
2456
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
2457
+ }
2458
+
2459
+ function isProcessAlive(pid) {
2460
+ try {
2461
+ process.kill(pid, 0);
2462
+ return true;
2463
+ } catch (error) {
2464
+ return error && typeof error === "object" && error.code === "EPERM";
2465
+ }
2466
+ }
2467
+
2468
+ function isRuntimeRotationAppHelperOwnerAlive(pid) {
2469
+ return isProcessAlive(pid);
2470
+ }
2471
+
2472
+ function resolveRuntimeRotationAppHelperDetachGraceMs(env = process.env) {
2473
+ const parsed = Number.parseInt(
2474
+ env.CODEX_MULTI_AUTH_APP_ROTATION_DETACH_GRACE_MS ?? "",
2475
+ 10,
2476
+ );
2477
+ return Number.isFinite(parsed) && parsed >= 0
2478
+ ? parsed
2479
+ : DEFAULT_APP_RUNTIME_HELPER_DETACH_GRACE_MS;
2480
+ }
2481
+
2482
+ function pickRuntimeRotationAppHelperEnv(env) {
2483
+ const picked = {
2484
+ CODEX_HOME: env.CODEX_HOME,
2485
+ OPENAI_API_KEY: env.OPENAI_API_KEY,
2486
+ };
2487
+ for (const name of [
2488
+ "CODEX_CLI_PATH",
2489
+ "NODE_OPTIONS",
2490
+ "CODEX_MULTI_AUTH_REAL_CODEX_BIN",
2491
+ "CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY",
2492
+ APP_SERVER_ACCOUNT_LABEL_ENV,
2493
+ ]) {
2494
+ if (env[name]) {
2495
+ picked[name] = env[name];
2496
+ }
2497
+ }
2498
+ if (env.CODEX_MULTI_AUTH_DIR) {
2499
+ picked.CODEX_MULTI_AUTH_DIR = env.CODEX_MULTI_AUTH_DIR;
2500
+ }
2501
+ return picked;
2502
+ }
2503
+
2504
+ function writeRuntimeRotationAppHelperStatus(payload, env = process.env) {
2505
+ try {
2506
+ const statusPath = resolveRuntimeRotationAppHelperStatusPath(env);
2507
+ writeOwnerOnlyJsonFileAtomicSync(statusPath, payload);
2508
+ } catch {
2509
+ // Best-effort status only; the helper must not fail because telemetry is unavailable.
2510
+ }
2511
+ }
2512
+
2513
+ function createRuntimeRotationAppHelperStatus({
2514
+ proxyServer,
2515
+ startedAt,
2516
+ idleTimeoutMs,
2517
+ lastActivityAt,
2518
+ state,
2519
+ }) {
2520
+ const proxyStatus =
2521
+ typeof proxyServer?.getStatus === "function" ? proxyServer.getStatus() : {};
2522
+ const lastAccountIndex = proxyStatus.lastAccountIndex ?? null;
2523
+ const lastAccountLabel =
2524
+ typeof proxyStatus.lastAccountLabel === "string" &&
2525
+ !proxyStatus.lastAccountLabel.includes("@")
2526
+ ? proxyStatus.lastAccountLabel
2527
+ : typeof lastAccountIndex === "number"
2528
+ ? `Account ${lastAccountIndex + 1}`
2529
+ : null;
2530
+ return {
2531
+ version: 1,
2532
+ kind: "codex-app-runtime-rotation-helper",
2533
+ state,
2534
+ pid: process.pid,
2535
+ startedAt,
2536
+ updatedAt: Date.now(),
2537
+ baseUrl: proxyServer?.baseUrl ?? null,
2538
+ idleTimeoutMs,
2539
+ idleExpiresAt: lastActivityAt + idleTimeoutMs,
2540
+ totalRequests: proxyStatus.totalRequests ?? 0,
2541
+ upstreamRequests: proxyStatus.upstreamRequests ?? 0,
2542
+ retries: proxyStatus.retries ?? 0,
2543
+ rotations: proxyStatus.rotations ?? 0,
2544
+ lastAccountIndex,
2545
+ lastAccountLabel,
2546
+ lastAccountId: proxyStatus.lastAccountId ?? null,
2547
+ lastAccountUpdatedAt: proxyStatus.lastAccountUpdatedAt ?? null,
2548
+ lastError: proxyStatus.lastError ?? null,
2549
+ };
2550
+ }
2551
+
2552
+ async function runRuntimeRotationAppHelper() {
2553
+ let proxyServer = null;
2554
+ let shadowContext = null;
2555
+ let appServerShimDir = null;
2556
+ let statusTimer = null;
2557
+ let closing = false;
2558
+ const startedAt = Date.now();
2559
+ const idleTimeoutMs = resolveRuntimeRotationAppHelperIdleMs();
2560
+ const ownerPid = resolveRuntimeRotationAppHelperOwnerPid();
2561
+ let lastActivityAt = startedAt;
2562
+ let lastRequestCount = 0;
2563
+
2564
+ const publishStatus = (state) => {
2565
+ writeRuntimeRotationAppHelperStatus(
2566
+ createRuntimeRotationAppHelperStatus({
2567
+ proxyServer,
2568
+ startedAt,
2569
+ idleTimeoutMs,
2570
+ lastActivityAt,
2571
+ state,
2572
+ }),
2573
+ );
2574
+ };
2575
+
2576
+ const cleanup = async (state = "stopped") => {
2577
+ if (closing) return;
2578
+ closing = true;
2579
+ if (statusTimer) {
2580
+ clearInterval(statusTimer);
2581
+ }
2582
+ try {
2583
+ if (appServerShimDir) {
2584
+ try {
2585
+ removeDirectoryWithRetry(appServerShimDir);
2586
+ } catch {
2587
+ // Best-effort shim cleanup only.
2588
+ }
2589
+ appServerShimDir = null;
2590
+ }
2591
+ shadowContext?.cleanup?.();
2592
+ } finally {
2593
+ try {
2594
+ await proxyServer?.close?.();
2595
+ } finally {
2596
+ publishStatus(state);
2597
+ }
2598
+ }
2599
+ };
2600
+
2601
+ const exitAfterCleanup = (state, exitCode) => {
2602
+ void cleanup(state).finally(() => {
2603
+ process.exit(exitCode);
2604
+ });
2605
+ };
2606
+
2607
+ process.once("SIGINT", () => exitAfterCleanup("stopped", 130));
2608
+ process.once("SIGTERM", () => exitAfterCleanup("stopped", 0));
2609
+ process.once("SIGHUP", () => exitAfterCleanup("stopped", 0));
2610
+
2611
+ try {
2612
+ const proxyModule = await loadRuntimeRotationProxyModule();
2613
+ if (!proxyModule) {
2614
+ throw new Error("runtime rotation proxy module is unavailable");
2615
+ }
2616
+ const configTomlModule = await loadRuntimeConfigTomlModule();
2617
+ if (!configTomlModule) {
2618
+ throw new Error("runtime rotation config helpers are unavailable");
2619
+ }
2620
+ const clientApiKey = createRuntimeRotationProxyClientApiKey();
2621
+ proxyServer = await proxyModule.startRuntimeRotationProxy({ clientApiKey });
2622
+ shadowContext = createRuntimeRotationProxyCodexHome(
2623
+ process.env,
2624
+ proxyServer.baseUrl,
2625
+ clientApiKey,
2626
+ configTomlModule,
2627
+ );
2628
+ appServerShimDir = installRuntimeRotationAppServerCliShim(shadowContext.env);
2629
+ lastRequestCount = proxyServer.getStatus?.().totalRequests ?? 0;
2630
+ publishStatus("running");
2631
+ process.stdout.write(
2632
+ `${JSON.stringify({
2633
+ type: "ready",
2634
+ pid: process.pid,
2635
+ baseUrl: proxyServer.baseUrl,
2636
+ statusPath: resolveRuntimeRotationAppHelperStatusPath(),
2637
+ env: pickRuntimeRotationAppHelperEnv(shadowContext.env),
2638
+ })}\n`,
2639
+ );
2640
+
2641
+ statusTimer = setInterval(() => {
2642
+ const currentTime = Date.now();
2643
+ const requestCount = proxyServer?.getStatus?.().totalRequests ?? 0;
2644
+ if (requestCount !== lastRequestCount) {
2645
+ lastRequestCount = requestCount;
2646
+ lastActivityAt = currentTime;
2647
+ }
2648
+ if (ownerPid && isRuntimeRotationAppHelperOwnerAlive(ownerPid)) {
2649
+ lastActivityAt = currentTime;
2650
+ }
2651
+ publishStatus("running");
2652
+ if (currentTime - lastActivityAt >= idleTimeoutMs) {
2653
+ exitAfterCleanup("idle-timeout", 0);
2654
+ }
2655
+ }, Math.min(1_000, Math.max(50, Math.floor(idleTimeoutMs / 2))));
2656
+ } catch (error) {
2657
+ process.stdout.write(
2658
+ `${JSON.stringify({
2659
+ type: "error",
2660
+ message: error instanceof Error ? error.message : String(error),
2661
+ })}\n`,
2662
+ );
2663
+ await cleanup("error");
2664
+ return 1;
2665
+ }
2666
+
2667
+ await new Promise(() => undefined);
2668
+ return 0;
2669
+ }
2670
+
2671
+ function waitForRuntimeRotationAppHelperExit(helper, timeoutMs = 2_000) {
2672
+ return new Promise((resolve) => {
2673
+ let settled = false;
2674
+ let timer = null;
2675
+ const finish = () => {
2676
+ if (settled) return;
2677
+ settled = true;
2678
+ if (timer) clearTimeout(timer);
2679
+ resolve();
2680
+ };
2681
+ timer = setTimeout(finish, timeoutMs);
2682
+ helper.once("close", finish);
2683
+ });
2684
+ }
2685
+
2686
+ function stopRuntimeRotationAppHelper(helper) {
2687
+ if (!helper || helper.killed) {
2688
+ return Promise.resolve();
2689
+ }
2690
+ try {
2691
+ helper.kill("SIGTERM");
2692
+ } catch {
2693
+ return Promise.resolve();
2694
+ }
2695
+ return waitForRuntimeRotationAppHelperExit(helper);
2696
+ }
2697
+
2698
+ function startRuntimeRotationAppHelper(baseContext) {
2699
+ const realCodexHome =
2700
+ baseContext.originalCodexHome ??
2701
+ resolveRuntimeRotationProxyOriginalCodexHome(baseContext.env);
2702
+ return new Promise((resolve, reject) => {
2703
+ let stdoutBuffer = "";
2704
+ let stderrBuffer = "";
2705
+ let settled = false;
2706
+ const helper = spawn(
2707
+ process.execPath,
2708
+ [fileURLToPath(import.meta.url), INTERNAL_RUNTIME_ROTATION_APP_HELPER_ARG],
2709
+ {
2710
+ env: {
2711
+ ...baseContext.env,
2712
+ [APP_RUNTIME_HELPER_OWNER_PID_ENV]: String(process.pid),
2713
+ [APP_RUNTIME_HELPER_REAL_CODEX_HOME_ENV]: realCodexHome,
2714
+ },
2715
+ stdio: ["ignore", "pipe", "pipe"],
2716
+ detached: true,
2717
+ },
2718
+ );
2719
+ let timeout = null;
2720
+ const finish = (result) => {
2721
+ if (settled) return;
2722
+ settled = true;
2723
+ if (timeout) clearTimeout(timeout);
2724
+ resolve(result);
2725
+ };
2726
+ const fail = (error) => {
2727
+ if (settled) return;
2728
+ settled = true;
2729
+ if (timeout) clearTimeout(timeout);
2730
+ void stopRuntimeRotationAppHelper(helper).finally(() => reject(error));
2731
+ };
2732
+ timeout = setTimeout(() => {
2733
+ fail(new Error("timed out waiting for runtime rotation app helper"));
2734
+ }, APP_RUNTIME_HELPER_LAUNCH_TIMEOUT_MS);
2735
+ helper.stdout?.setEncoding("utf8");
2736
+ helper.stdout?.on("data", (chunk) => {
2737
+ stdoutBuffer += chunk;
2738
+ const newlineIndex = stdoutBuffer.indexOf("\n");
2739
+ if (newlineIndex < 0) return;
2740
+ const line = stdoutBuffer.slice(0, newlineIndex).trim();
2741
+ try {
2742
+ const message = JSON.parse(line);
2743
+ if (message?.type === "ready" && message.env && message.pid) {
2744
+ finish({ helper, message });
2745
+ return;
2746
+ }
2747
+ fail(
2748
+ new Error(
2749
+ message?.message ??
2750
+ "runtime rotation app helper returned an invalid startup response",
2751
+ ),
2752
+ );
2753
+ } catch (error) {
2754
+ fail(error);
2755
+ }
2756
+ });
2757
+ helper.stderr?.setEncoding("utf8");
2758
+ helper.stderr?.on("data", (chunk) => {
2759
+ stderrBuffer += chunk;
2760
+ });
2761
+ helper.once("error", fail);
2762
+ helper.once("close", (code) => {
2763
+ if (settled) return;
2764
+ fail(
2765
+ new Error(
2766
+ `runtime rotation app helper exited before startup (code ${code ?? "unknown"}): ${stderrBuffer.trim()}`,
2767
+ ),
2768
+ );
2769
+ });
2770
+ });
2771
+ }
2772
+
2773
+ async function createRuntimeRotationAppHelperContext(baseContext, configTomlModule) {
2774
+ const startedAt = Date.now();
2775
+ const { helper, message } = await startRuntimeRotationAppHelper(baseContext);
2776
+ const helperEnv = message.env ?? {};
2777
+ const detachGraceMs = resolveRuntimeRotationAppHelperDetachGraceMs(baseContext.env);
2778
+
2779
+ const cleanup = async ({ exitCode } = {}) => {
2780
+ const livedMs = Date.now() - startedAt;
2781
+ if (exitCode === 0 && livedMs < detachGraceMs) {
2782
+ helper.stdout?.destroy();
2783
+ helper.stderr?.destroy();
2784
+ helper.unref();
2785
+ return;
2786
+ }
2787
+ await stopRuntimeRotationAppHelper(helper);
2788
+ };
864
2789
 
865
2790
  return {
866
- args: nextArgs,
867
- requestedModel,
2791
+ args: [
2792
+ ...baseContext.args,
2793
+ "-c",
2794
+ `model_provider=${configTomlModule.tomlStringLiteral(RUNTIME_ROTATION_PROXY_PROVIDER_ID)}`,
2795
+ ],
2796
+ env: {
2797
+ ...baseContext.env,
2798
+ ...helperEnv,
2799
+ },
2800
+ cleanup: async (details) => {
2801
+ try {
2802
+ await cleanup(details);
2803
+ } finally {
2804
+ baseContext.cleanup?.();
2805
+ }
2806
+ },
868
2807
  };
869
2808
  }
870
2809
 
871
- function resolveCodexHomeDir(env = process.env) {
872
- const override = (env.CODEX_HOME ?? "").trim();
873
- if (override.length > 0) return override;
874
- if (process.platform === "win32") {
875
- const homeDir = resolveWindowsUserHomeDir();
876
- if (homeDir) {
877
- return join(homeDir, ".codex");
878
- }
2810
+ async function createRuntimeRotationProxyContextIfEnabled(
2811
+ baseContext,
2812
+ rawArgs,
2813
+ ) {
2814
+ const enabled = await isRuntimeRotationProxyEnabled(rawArgs, baseContext.env);
2815
+ if (!enabled) {
2816
+ return baseContext;
879
2817
  }
880
- return join(env.HOME ?? homedir(), ".codex");
881
- }
882
2818
 
883
- function ensureTrailingNewline(value) {
884
- return value.endsWith("\n") ? value : `${value}\n`;
885
- }
2819
+ const configTomlModule = await loadRuntimeConfigTomlModule();
2820
+ if (!configTomlModule) {
2821
+ console.error(
2822
+ "codex-multi-auth runtime rotation config helpers are unavailable; continuing without runtime rotation.",
2823
+ );
2824
+ return baseContext;
2825
+ }
886
2826
 
887
- function captureShadowHomeState(filePath, options = {}) {
888
- try {
889
- if (!existsSync(filePath)) {
890
- return { exists: false, content: null };
891
- }
892
- if (options.rethrowRetryableReadErrors) {
893
- maybeThrowSimulatedShadowHomePreflightReadBusyError();
894
- }
895
- return {
896
- exists: true,
897
- content: readFileSync(filePath, "utf8"),
898
- };
899
- } catch (error) {
900
- if (options.rethrowRetryableReadErrors && isRetryableShadowHomeCleanupError(error)) {
901
- throw error;
902
- }
903
- return { exists: true, content: null, unreadable: true };
2827
+ if (isCodexAppCommand(rawArgs)) {
2828
+ return createRuntimeRotationAppHelperContext(baseContext, configTomlModule);
904
2829
  }
905
- }
906
2830
 
907
- function shadowHomeStateMatches(left, right) {
908
- return (
909
- left.exists === right.exists &&
910
- left.content === right.content &&
911
- Boolean(left.unreadable) === Boolean(right.unreadable)
912
- );
913
- }
2831
+ const proxyModule = await loadRuntimeRotationProxyModule();
2832
+ if (!proxyModule) {
2833
+ console.error(
2834
+ "codex-multi-auth runtime rotation proxy is unavailable; continuing without runtime rotation.",
2835
+ );
2836
+ return baseContext;
2837
+ }
914
2838
 
915
- function syncShadowHomeStateFile(
916
- sourcePath,
917
- destinationPath,
918
- expectedDestinationState,
919
- ) {
920
- const tempPath = join(
921
- dirname(destinationPath),
922
- `.${basename(destinationPath)}.codex-multi-auth-sync-${process.pid}.tmp`,
923
- );
2839
+ let proxyServer;
2840
+ let shadowContext;
924
2841
  try {
925
- mkdirSync(dirname(destinationPath), { recursive: true });
926
- copyFileSync(sourcePath, tempPath);
927
- renameFileWithRetry(tempPath, destinationPath, expectedDestinationState);
2842
+ const clientApiKey = createRuntimeRotationProxyClientApiKey();
2843
+ proxyServer = await proxyModule.startRuntimeRotationProxy({ clientApiKey });
2844
+ shadowContext = createRuntimeRotationProxyCodexHome(
2845
+ baseContext.env,
2846
+ proxyServer.baseUrl,
2847
+ clientApiKey,
2848
+ configTomlModule,
2849
+ );
928
2850
  } catch (error) {
929
2851
  try {
930
- rmSync(tempPath, { force: true });
2852
+ await proxyServer?.close?.();
931
2853
  } catch {
932
2854
  // Best-effort cleanup only.
933
2855
  }
934
- throw error;
935
- }
936
- }
937
-
938
- function rewriteConfigTomlReasoningEffort(rawConfig, requestedModel) {
939
- const lineEnding = rawConfig.includes("\r\n") ? "\r\n" : "\n";
940
- let changed = false;
941
- const nextLines = rawConfig.split(/\r?\n/).map((line) => {
942
- const quotedMatch = line.match(
943
- /^(\s*model_reasoning_effort\s*=\s*)(["'])([^"']+)(\2.*)$/,
944
- );
945
- const bareMatch = quotedMatch
946
- ? null
947
- : line.match(
948
- /^(\s*model_reasoning_effort\s*=\s*)([^\s#]+)(\s*(?:#.*)?)$/,
949
- );
950
- if (!quotedMatch && !bareMatch) return line;
951
-
952
- const prefix = quotedMatch?.[1] ?? bareMatch?.[1] ?? "";
953
- const openingQuote = quotedMatch?.[2] ?? "";
954
- const currentEffort = quotedMatch?.[3] ?? bareMatch?.[2] ?? "";
955
- const suffix = quotedMatch?.[4] ?? bareMatch?.[3] ?? "";
956
- const coercedEffort = coerceReasoningEffortForModel(
957
- requestedModel,
958
- currentEffort,
2856
+ console.error(
2857
+ `codex-multi-auth runtime rotation proxy failed to start; continuing without runtime rotation: ${error instanceof Error ? error.message : String(error)}`,
959
2858
  );
960
- if (coercedEffort === currentEffort) {
961
- return line;
962
- }
963
-
964
- changed = true;
965
- return quotedMatch
966
- ? `${prefix}${openingQuote}${coercedEffort}${suffix}`
967
- : `${prefix}${coercedEffort}${suffix}`;
968
- });
969
-
970
- if (!changed) {
971
- return rawConfig;
2859
+ return baseContext;
972
2860
  }
973
2861
 
974
- return ensureTrailingNewline(nextLines.join(lineEnding));
975
- }
2862
+ const cleanup = async () => {
2863
+ try {
2864
+ shadowContext.cleanup?.();
2865
+ } finally {
2866
+ try {
2867
+ await proxyServer.close();
2868
+ } finally {
2869
+ baseContext.cleanup?.();
2870
+ }
2871
+ }
2872
+ };
976
2873
 
977
- function resolveOriginalMultiAuthDir(env) {
978
- const explicit = (env.CODEX_MULTI_AUTH_DIR ?? "").trim();
979
- if (explicit.length > 0) {
980
- return explicit;
981
- }
982
- return undefined;
2874
+ return {
2875
+ args: [
2876
+ ...baseContext.args,
2877
+ "-c",
2878
+ `model_provider=${configTomlModule.tomlStringLiteral(RUNTIME_ROTATION_PROXY_PROVIDER_ID)}`,
2879
+ ],
2880
+ env: shadowContext.env,
2881
+ cleanup,
2882
+ proxyAppServerAccountRead: isCodexAppServerCommand(rawArgs),
2883
+ };
983
2884
  }
984
2885
 
985
2886
  async function loadRuntimeObservabilityModule() {
@@ -1020,8 +2921,17 @@ function consumesNextArg(arg) {
1020
2921
  "--config",
1021
2922
  "--enable",
1022
2923
  "--disable",
2924
+ "--listen",
1023
2925
  "--remote",
1024
2926
  "--remote-auth-token-env",
2927
+ "--ws-auth",
2928
+ "--ws-token-file",
2929
+ "--ws-token-sha256",
2930
+ "--ws-shared-secret-file",
2931
+ "--ws-issuer",
2932
+ "--ws-audience",
2933
+ "--ws-max-clock-skew-seconds",
2934
+ "--download-url",
1025
2935
  "-i",
1026
2936
  "--image",
1027
2937
  "-m",
@@ -1043,7 +2953,75 @@ function consumesNextArg(arg) {
1043
2953
  ]).has(arg);
1044
2954
  }
1045
2955
 
1046
- function shouldTrackForwardedRuntimeObservability(rawArgs) {
2956
+ function findForwardedCommand(rawArgs) {
2957
+ if (!Array.isArray(rawArgs) || rawArgs.length === 0) {
2958
+ return null;
2959
+ }
2960
+ for (let i = 0; i < rawArgs.length; i += 1) {
2961
+ const arg = rawArgs[i];
2962
+ if (typeof arg !== "string" || arg.length === 0) continue;
2963
+ if (arg === "--") {
2964
+ return i + 1 < rawArgs.length
2965
+ ? { command: rawArgs[i + 1], index: i + 1 }
2966
+ : null;
2967
+ }
2968
+ if (arg.startsWith("--config=")) {
2969
+ continue;
2970
+ }
2971
+ if (arg.startsWith("--") || (arg.startsWith("-") && arg !== "-")) {
2972
+ if (consumesNextArg(arg)) {
2973
+ i += 1;
2974
+ }
2975
+ continue;
2976
+ }
2977
+ return { command: arg, index: i };
2978
+ }
2979
+
2980
+ return null;
2981
+ }
2982
+
2983
+ function findForwardedSubcommand(rawArgs, commandIndex) {
2984
+ for (let i = commandIndex + 1; i < rawArgs.length; i += 1) {
2985
+ const arg = rawArgs[i];
2986
+ if (typeof arg !== "string" || arg.length === 0) continue;
2987
+ if (arg === "--") {
2988
+ return i + 1 < rawArgs.length ? rawArgs[i + 1] : null;
2989
+ }
2990
+ if (arg.startsWith("--config=")) {
2991
+ continue;
2992
+ }
2993
+ if (arg.startsWith("--") || (arg.startsWith("-") && arg !== "-")) {
2994
+ if (consumesNextArg(arg)) {
2995
+ i += 1;
2996
+ }
2997
+ continue;
2998
+ }
2999
+ return arg;
3000
+ }
3001
+ return null;
3002
+ }
3003
+
3004
+ function hasHelpFlagAfterCommand(rawArgs, commandIndex) {
3005
+ for (let i = commandIndex + 1; i < rawArgs.length; i += 1) {
3006
+ const arg = rawArgs[i];
3007
+ if (arg === "--") return false;
3008
+ if (arg === "--help" || arg === "-h" || arg === "help") return true;
3009
+ if (typeof arg === "string" && consumesNextArg(arg)) {
3010
+ i += 1;
3011
+ }
3012
+ }
3013
+ return false;
3014
+ }
3015
+
3016
+ function isCodexAppCommand(rawArgs) {
3017
+ return findForwardedCommand(rawArgs)?.command === "app";
3018
+ }
3019
+
3020
+ function isCodexAppServerCommand(rawArgs) {
3021
+ return findForwardedCommand(rawArgs)?.command === "app-server";
3022
+ }
3023
+
3024
+ function shouldUseRuntimeRoutingForForwardedArgs(rawArgs) {
1047
3025
  if (!Array.isArray(rawArgs) || rawArgs.length === 0) {
1048
3026
  return true;
1049
3027
  }
@@ -1051,7 +3029,12 @@ function shouldTrackForwardedRuntimeObservability(rawArgs) {
1051
3029
  return false;
1052
3030
  }
1053
3031
 
1054
- const requestCommands = new Set(["exec", "review", "resume", "fork"]);
3032
+ const command = findForwardedCommand(rawArgs);
3033
+ if (!command) {
3034
+ return true;
3035
+ }
3036
+
3037
+ const requestCommands = new Set(["exec", "review", "resume", "fork", "app"]);
1055
3038
  const nonRequestCommands = new Set([
1056
3039
  "help",
1057
3040
  "completion",
@@ -1059,7 +3042,6 @@ function shouldTrackForwardedRuntimeObservability(rawArgs) {
1059
3042
  "logout",
1060
3043
  "mcp",
1061
3044
  "mcp-server",
1062
- "app-server",
1063
3045
  "sandbox",
1064
3046
  "debug",
1065
3047
  "apply",
@@ -1068,36 +3050,41 @@ function shouldTrackForwardedRuntimeObservability(rawArgs) {
1068
3050
  "auth",
1069
3051
  ]);
1070
3052
 
1071
- for (let i = 0; i < rawArgs.length; i += 1) {
1072
- const arg = rawArgs[i];
1073
- if (typeof arg !== "string" || arg.length === 0) continue;
1074
- if (arg === "--") {
1075
- return i + 1 < rawArgs.length;
1076
- }
1077
- if (arg.startsWith("--config=")) {
1078
- continue;
1079
- }
1080
- if (arg.startsWith("--") || (arg.startsWith("-") && arg !== "-")) {
1081
- if (consumesNextArg(arg)) {
1082
- i += 1;
1083
- }
1084
- continue;
1085
- }
1086
- if (requestCommands.has(arg)) {
1087
- return true;
1088
- }
1089
- if (nonRequestCommands.has(arg)) {
3053
+ if (command.command === "app-server") {
3054
+ if (hasHelpFlagAfterCommand(rawArgs, command.index)) {
1090
3055
  return false;
1091
3056
  }
1092
- return true;
3057
+ const subcommand = findForwardedSubcommand(rawArgs, command.index);
3058
+ return !new Set(["help", "generate-ts", "generate-json-schema"]).has(
3059
+ subcommand ?? "",
3060
+ );
1093
3061
  }
1094
3062
 
3063
+ if (command.command === "app" && hasHelpFlagAfterCommand(rawArgs, command.index)) {
3064
+ return false;
3065
+ }
3066
+ if (requestCommands.has(command.command)) {
3067
+ return true;
3068
+ }
3069
+ if (nonRequestCommands.has(command.command)) {
3070
+ return false;
3071
+ }
1095
3072
  return true;
1096
3073
  }
1097
3074
 
3075
+ function shouldTrackForwardedRuntimeObservability(rawArgs) {
3076
+ return shouldUseRuntimeRoutingForForwardedArgs(rawArgs);
3077
+ }
3078
+
3079
+ function shouldCaptureForwardedOutputForArgs(rawArgs, env) {
3080
+ if (isCodexAppServerCommand(rawArgs)) {
3081
+ return false;
3082
+ }
3083
+ return shouldCaptureForwardedCodexOutput(env);
3084
+ }
3085
+
1098
3086
  function createRuntimeSnapshotChangeToken(snapshot) {
1099
3087
  return JSON.stringify({
1100
- updatedAt: snapshot?.updatedAt ?? null,
1101
3088
  responsesRequests: snapshot?.responsesRequests ?? null,
1102
3089
  authRefreshRequests: snapshot?.authRefreshRequests ?? null,
1103
3090
  diagnosticProbeRequests: snapshot?.diagnosticProbeRequests ?? null,
@@ -1170,21 +3157,25 @@ function createCompatibilityCodexHome(
1170
3157
  requestedModel,
1171
3158
  baseEnv = process.env,
1172
3159
  ) {
3160
+ const originalCodexHome = resolveCodexHomeDir(baseEnv);
1173
3161
  if (!requestedModel) {
1174
- return { args: processedArgs, env: baseEnv, cleanup: undefined };
3162
+ return {
3163
+ args: processedArgs,
3164
+ env: baseEnv,
3165
+ cleanup: undefined,
3166
+ originalCodexHome,
3167
+ };
1175
3168
  }
1176
3169
 
1177
- const originalCodexHome = resolveCodexHomeDir(baseEnv);
1178
3170
  const configPath = join(originalCodexHome, "config.toml");
1179
3171
  if (!existsSync(configPath)) {
1180
- return { args: processedArgs, env: baseEnv, cleanup: undefined };
3172
+ return {
3173
+ args: processedArgs,
3174
+ env: baseEnv,
3175
+ cleanup: undefined,
3176
+ originalCodexHome,
3177
+ };
1181
3178
  }
1182
- const originalShadowHomeState = new Map(
1183
- SHADOW_HOME_STATE_FILES.map((name) => [
1184
- name,
1185
- captureShadowHomeState(join(originalCodexHome, name)),
1186
- ]),
1187
- );
1188
3179
 
1189
3180
  const rawConfig = readFileSync(configPath, "utf8");
1190
3181
  const compatConfig = rewriteConfigTomlReasoningEffort(
@@ -1192,10 +3183,16 @@ function createCompatibilityCodexHome(
1192
3183
  requestedModel,
1193
3184
  );
1194
3185
  if (compatConfig === rawConfig) {
1195
- return { args: processedArgs, env: baseEnv, cleanup: undefined };
3186
+ return {
3187
+ args: processedArgs,
3188
+ env: baseEnv,
3189
+ cleanup: undefined,
3190
+ originalCodexHome,
3191
+ };
1196
3192
  }
1197
3193
 
1198
3194
  const shadowCodexHome = mkdtempSync(join(tmpdir(), "codex-multi-auth-home-"));
3195
+ let syncShadowHomeStateBack = () => {};
1199
3196
  const cleanup = () => {
1200
3197
  try {
1201
3198
  removeDirectoryWithRetry(shadowCodexHome);
@@ -1210,44 +3207,15 @@ function createCompatibilityCodexHome(
1210
3207
  // Best-effort only; permission semantics vary by platform.
1211
3208
  }
1212
3209
  };
1213
- const syncShadowHomeStateBack = () => {
1214
- for (const name of SHADOW_HOME_STATE_FILES) {
1215
- const shadowPath = join(shadowCodexHome, name);
1216
- const shadowState = captureShadowHomeState(shadowPath);
1217
- if (!shadowState.exists || shadowState.unreadable) {
1218
- continue;
1219
- }
1220
-
1221
- try {
1222
- const originalPath = join(originalCodexHome, name);
1223
- const originalSnapshot =
1224
- originalShadowHomeState.get(name) ?? { exists: false, content: null };
1225
- const currentOriginalState = captureShadowHomeState(originalPath);
1226
- if (!shadowHomeStateMatches(currentOriginalState, originalSnapshot)) {
1227
- continue;
1228
- }
1229
- if (shadowHomeStateMatches(shadowState, originalSnapshot)) {
1230
- continue;
1231
- }
1232
- syncShadowHomeStateFile(shadowPath, originalPath, originalSnapshot);
1233
- tightenShadowHomePermissions(originalPath);
1234
- } catch {
1235
- // Best-effort only; runtime auth refreshes should not fail cleanup.
1236
- }
1237
- }
1238
- };
1239
3210
  try {
3211
+ syncShadowHomeStateBack = createShadowHomeMirror(
3212
+ originalCodexHome,
3213
+ shadowCodexHome,
3214
+ tightenShadowHomePermissions,
3215
+ );
1240
3216
  const compatConfigPath = join(shadowCodexHome, "config.toml");
1241
3217
  writeFileSync(compatConfigPath, compatConfig, "utf8");
1242
3218
  tightenShadowHomePermissions(compatConfigPath);
1243
- for (const name of SHADOW_HOME_STATE_FILES) {
1244
- const sourcePath = join(originalCodexHome, name);
1245
- if (existsSync(sourcePath)) {
1246
- const destinationPath = join(shadowCodexHome, name);
1247
- copyFileSync(sourcePath, destinationPath);
1248
- tightenShadowHomePermissions(destinationPath);
1249
- }
1250
- }
1251
3219
  } catch (error) {
1252
3220
  cleanup();
1253
3221
  throw error;
@@ -1270,6 +3238,7 @@ function createCompatibilityCodexHome(
1270
3238
  args: processedArgs,
1271
3239
  env: forwardedEnv,
1272
3240
  cleanup: cleanupWithSync,
3241
+ originalCodexHome,
1273
3242
  };
1274
3243
  }
1275
3244
 
@@ -1609,10 +3578,16 @@ function ensureWindowsShellShimGuards() {
1609
3578
 
1610
3579
  async function main() {
1611
3580
  hydrateCliVersionEnv();
1612
- ensureWindowsShellShimGuards();
1613
3581
 
1614
3582
  const rawArgs = process.argv.slice(2);
3583
+ if (rawArgs[0] === INTERNAL_RUNTIME_ROTATION_APP_HELPER_ARG) {
3584
+ return runRuntimeRotationAppHelper();
3585
+ }
3586
+
1615
3587
  const normalizedArgs = normalizeAuthAlias(rawArgs);
3588
+ await autoUpdatePackageIfEnabled(rawArgs, normalizedArgs);
3589
+ ensureWindowsShellShimGuards();
3590
+
1616
3591
  const bypass = (process.env.CODEX_MULTI_AUTH_BYPASS ?? "").trim() === "1";
1617
3592
 
1618
3593
  if (!bypass && shouldHandleMultiAuthAuth(normalizedArgs)) {
@@ -1622,6 +3597,10 @@ async function main() {
1622
3597
  return 1;
1623
3598
  }
1624
3599
  const exitCode = await runCodexMultiAuthCli(normalizedArgs);
3600
+ await maybeInstallCodexAppLauncherAfterRotationEnable(
3601
+ normalizedArgs,
3602
+ normalizeExitCode(exitCode),
3603
+ );
1625
3604
  return normalizeExitCode(exitCode);
1626
3605
  } catch (error) {
1627
3606
  console.error(
@@ -1644,9 +3623,13 @@ async function main() {
1644
3623
  }
1645
3624
 
1646
3625
  await autoSyncManagerActiveSelectionIfEnabled();
1647
- return withForwardedRuntimeObservability(rawArgs, () =>
1648
- forwardToRealCodex(realCodexBin, rawArgs),
1649
- );
3626
+ try {
3627
+ return await withForwardedRuntimeObservability(rawArgs, () =>
3628
+ forwardToRealCodex(realCodexBin, rawArgs),
3629
+ );
3630
+ } finally {
3631
+ await autoSyncManagerActiveSelectionIfEnabled();
3632
+ }
1650
3633
  }
1651
3634
 
1652
3635
  const exitCode = await main();