Skip to content

Add runtime rotation proxy#438

Merged
ndycode merged 42 commits into
mainfrom
feature/runtime-rotation-proxy
Apr 25, 2026
Merged

Add runtime rotation proxy#438
ndycode merged 42 commits into
mainfrom
feature/runtime-rotation-proxy

Conversation

@ndycode
Copy link
Copy Markdown
Owner

@ndycode ndycode commented Apr 24, 2026

Summary

  • Adds a loopback-only Responses API runtime rotation proxy for official Codex sessions, with local client auth, strict Responses-path forwarding, bounded request bodies, account retry/rotation handling, stream stall protection, and redacted runtime status.
  • Wires Codex CLI, codex app, and codex app-server launches through the runtime rotation path while preserving the real Codex home via shadow-home mirroring and sync-back.
  • Adds detached app helper/router support plus codex auth rotation enable|disable|status management commands backed by shared runtime constants.

Review Cleanup

  • Strips stale upstream content-encoding before forwarding decoded Node fetch bodies to local Codex clients.
  • Retries transient shadow-home lock owner writes and removes the just-created lock directory if owner metadata cannot be written.
  • Resumes process.stdin during app-server protocol-proxy cleanup so retry attempts cannot inherit a paused stdin stream after child backpressure.

Regression Coverage

  • test/runtime-rotation-proxy.test.ts covers decoded upstream response forwarding without stale compression headers.
  • test/codex-bin-wrapper.test.ts covers transient and persistent shadow sync lock owner-write failures.
  • test/codex-bin-wrapper.test.ts now asserts app-server protocol cleanup resumes process.stdin after listener teardown.

Validation

  • git diff --check
  • npm.cmd test -- test/runtime-rotation-proxy.test.ts test/codex-bin-wrapper.test.ts (101 tests)
  • npm.cmd test -- test/codex-bin-wrapper.test.ts (80 tests)
  • npm.cmd run typecheck
  • npm.cmd run lint

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this PR wires a loopback-only responses-api rotation proxy into codex CLI and app sessions, adds shadow-home mirroring with per-file compare-and-swap sync-back, app-bind/router startup files for macOS and Windows, a protocol proxy that rewrites account/read responses for app-server sessions, and the codex auth rotation management commands. it also consolidates runtime constants and centralises TOML rewriting into lib/runtime/config-toml.ts.

the previous greptile review cleanup landed several substantive fixes: acquireShadowHomeSyncLock now rolls back the lock dir on owner-write failure; removeStaleShadowHomeSyncLock handles a null ownerPid with an age gate; the orphan-lock stale-age constant was raised to 2 s; process.stdin.resume() is called in cleanupProtocolProxy; the shim dir is cleaned in the helper's cleanup(); Windows .cmd % escaping is implemented; closeServer now destroys all tracked sockets; and the cold-start ephemeral-port bypass is blocked in codex-app-router.js.

Confidence Score: 4/5

safe to merge; the prior critical auth-bypass and race-condition issues are resolved, leaving only moderate quality concerns

the previously flagged P0/P1 issues (proxy auth bypass, orphaned lock dir, stdin.resume missing, shim accumulation, duplicate constant, Windows % escaping) are all addressed. the remaining findings are P2: stopRouter's missing SIGKILL escalation, a macOS launchd KeepAlive restart race on unbind, and the temp-file name collision in syncShadowHomeStateFile. none of these block correctness for the vast majority of sessions.

lib/runtime/app-bind.ts (stopRouter SIGKILL gap and macOS launchd KeepAlive race), scripts/codex.js (syncShadowHomeStateFile temp-name collision)

Important Files Changed

Filename Overview
lib/runtime-rotation-proxy.ts new 1107-line loopback proxy: timing-safe client auth, path allow-listing, body size cap, per-socket tracking for clean shutdown, stream-stall timeout, session affinity, and rotation budget — all look correct
lib/runtime/app-bind.ts new file managing router spawn, state, backup, macOS plist, and Windows .cmd startup; stopRouter lacks SIGKILL escalation and the macOS plist has KeepAlive=true without a launchctl bootout on unbind, creating a brief launchd restart race
scripts/codex.js ~2 k-line additions covering shadow-home mirroring, per-file CAS sync-back with rebase metadata, lock acquisition with rollback, app-helper lifecycle, shim install/sweep, and protocol proxy for app-server auth rewriting — concurrent sync-back race properly addressed via canRebaseShadowHomeSyncState
scripts/codex-app-router.js detached router process with log-bound interval, status file writes at 0o600, and null-state guard that blocks ephemeral-port binding — looks solid
lib/runtime/config-toml.ts centralised TOML provider injection/restore logic with string-literal escaping; replaces duplicated inline rewrites
lib/runtime-constants.ts single-source-of-truth for shared runtime constants; eliminates the previously-flagged duplicate RUNTIME_ROTATION_PROXY_PROVIDER_ID definition

Sequence Diagram

sequenceDiagram
    participant Wrapper as codex.js wrapper
    participant Proxy as RuntimeRotationProxy
    participant Shadow as ShadowHome
    participant Router as codex-app-router
    participant CLI as codex CLI
    participant Upstream as OpenAI API

    Note over Wrapper: codex exec/review/fork
    Wrapper->>Proxy: startRuntimeRotationProxy({clientApiKey})
    Proxy-->>Wrapper: {baseUrl, close, getStatus}
    Wrapper->>Shadow: createRuntimeRotationProxyCodexHome()
    Shadow-->>Wrapper: {env CODEX_HOME=shadow, cleanup()}
    Wrapper->>CLI: spawn with shadow home + proxy config.toml
    CLI->>Proxy: POST /backend/api/v1/responses Bearer clientApiKey
    Proxy->>Proxy: timingSafeEqual auth check
    Proxy->>Upstream: POST with real Bearer token
    Upstream-->>Proxy: streaming SSE
    Proxy-->>CLI: forward (socket.destroy on shutdown)

    Note over Wrapper: codex app (helper path)
    Wrapper->>Router: spawn app-helper detached
    Router->>Proxy: startRuntimeRotationProxy
    Router->>Shadow: createRuntimeRotationProxyCodexHome
    Router-->>Wrapper: {type:ready, baseUrl, env, pid}
    Wrapper->>CLI: spawn codex app with helperEnv

    Note over Shadow: on session exit
    Shadow->>Shadow: acquireShadowHomeSyncLock
    Shadow->>Shadow: syncShadowHomeAuthBundle (CAS + rebase)
    Shadow->>Shadow: releaseLock + rmdir shadowCodexHome
Loading

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/runtime/app-bind.ts
Line: 583-593

Comment:
**`stopRouter` silently abandons a still-running router after 2 s**

`stopRouter` sends `SIGTERM` and polls 20 × 100 ms, then returns without error if the process is still alive. the caller (`unbindCodexApp`) proceeds to restore `config.toml` and delete the state file while the router may still be serving. on the next `bindCodexApp` that reuses the same port, the old router holds the port open and the bind fails.

with the socket-destroy fix in `closeServer` this window is small, but it's still open under heavy load or antivirus-induced i/o delays. a `SIGKILL` escalation after the polling loop would close it:

```ts
for (let attempt = 0; attempt < 20; attempt += 1) {
  if (!isProcessAlive(router.pid)) return;
  await new Promise((resolve) => setTimeout(resolve, 100));
}
// escalate: process did not exit within 2 s
try { process.kill(router.pid, "SIGKILL"); } catch { /* already dead */ }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: lib/runtime/app-bind.ts
Line: 504-514

Comment:
**macOS `KeepAlive=true` launchd race on unbind**

`removeAppBindStartup` deletes the plist file but does not call `launchctl bootout` (or `launchctl unload` on older macOS). because `KeepAlive=true` is set, launchd immediately respawns the router when `stopRouter`'s SIGTERM kills it. the respawned process starts before the state file is removed, briefly serving requests with the old (soon-to-be-restored) config. the window closes as soon as `unbindCodexApp` deletes the state file and the router exits, but a concurrent codex app session during that window would talk to a proxy with a stale rotation key.

consider adding a `launchctl bootout gui/$(id -u) <label>` call in `unbindCodexApp` before `stopRouter` on macOS, or switching `KeepAlive` to a threshold-based restart policy that prevents immediate re-launch.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: scripts/codex.js
Line: 1574

Comment:
**temp file name collides on PID reuse — windows antivirus risk**

`syncShadowHomeStateFile` builds the temp path as `.auth.json.codex-multi-auth-sync-<pid>.tmp` with only the PID as a differentiator. if a previous session exited, its PID is recycled, and a new session copies to the same temp name while an antivirus lock holds the old file, the new session's `copyFileSync` fails or silently wins the wrong file. `writeOwnerOnlyJsonFileAtomicSync` already uses a 4-byte random suffix — applying the same pattern here eliminates the collision:

How can I resolve this? If you propose a fix, please make it concise.

Reviews (34): Last reviewed commit: "Resume stdin after app protocol cleanup" | Re-trigger Greptile

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

adds an opt-in runtime rotation proxy and a new cli surface codex auth rotation that forwards official responses through a rotating-account proxy, updates plugin config/schema and wrapper app-bind/launcher flows, and adds extensive runtime, integration, and unit tests.

Changes

Cohort / File(s) Summary
documentation
README.md, docs/configuration.md, docs/development/CONFIG_FIELDS.md, docs/features.md, docs/reference/commands.md, docs/reference/settings.md, docs/releases/v1.3.1.md
documents codex auth rotation, codexRuntimeRotationProxy config and env overrides (CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY, CODEX_MULTI_AUTH_APP_BIND_INSTALL), runtime rotation proxy behavior, and packaged-app bind semantics.
config & schema
lib/config.ts, lib/schemas.ts
adds codexRuntimeRotationProxy config getter and includes codexRuntimeRotationProxy in plugin schema. see lib/config.ts:1, lib/schemas.ts:1.
cli & routing
lib/codex-manager.ts, lib/codex-manager/commands/rotation.ts, lib/codex-manager/help.ts, scripts/codex-routing.js
implements codex auth rotation command family (`enable
runtime rotation proxy
lib/runtime-rotation-proxy.ts, lib/index.ts, lib/constants.ts, lib/runtime-constants.ts
adds startRuntimeRotationProxy with account selection, refresh queuing, retries/rotation on 429/401/5xx, cooldowns, streaming forward, status API and exported types; re-exports from index. see lib/runtime-rotation-proxy.ts:1.
wrapper integration
scripts/codex.js, scripts/codex-app-router.js, scripts/codex-routing.js
wrapper creates shadow CODEX_HOME, rewrites config.toml to a runtime-proxy provider, injects experimental bearer token, and can spawn a detached app helper/router for codex app and app-server forwarding. see scripts/codex.js:1.
app bind & launcher
lib/runtime/app-bind.ts, scripts/codex-app-launcher.js, scripts/postinstall.js, package.json
implements app bind/unbind, toml rewrite/restore, router lifecycle/status, cross-platform launcher install/remove, postinstall auto-bind gating, and adds codex-multi-auth-app-launcher bin. see lib/runtime/app-bind.ts:1, scripts/codex-app-launcher.js:1, scripts/postinstall.js:1.
observability & status
lib/codex-manager/commands/status.ts, lib/runtime/runtime-observability.ts
status command storage handling refined; observability snapshot extended with last-account fields and reported in status output. see lib/codex-manager/commands/status.ts:1, lib/runtime/runtime-observability.ts:1.
tests & harness
test/runtime-rotation-proxy.test.ts, test/codex-bin-wrapper.test.ts, test/codex-manager-rotation-command.test.ts, test/app-bind.test.ts, test/*
adds extensive unit and e2e tests for the proxy, wrapper forwarding, rotation CLI, app-bind, launcher, postinstall, and updates many existing tests to include new config flag. see test/runtime-rotation-proxy.test.ts:1, test/codex-bin-wrapper.test.ts:1.
build & tooling
vitest.config.ts, scripts/check-pack-budget-lib.js, scripts/codex-multi-auth.js
adds vitest shebang-stripping plugin, safety improvements in pack checks, and minor CLI arg-safety fixes. see vitest.config.ts:1.

Sequence Diagram(s)

sequenceDiagram
    participant client as client
    participant proxy as runtime-rotation-proxy
    participant acctmgr as account-manager
    participant oauth as oauth-refresh
    participant upstream as responses-upstream

    client->>proxy: POST /responses (wrapper token)
    proxy->>proxy: validate wrapper auth
    proxy->>proxy: parse model & session key
    proxy->>acctmgr: select account (affinity/preference)
    acctmgr->>acctmgr: check cooldown & token validity

    alt needs refresh
        acctmgr->>oauth: queued refresh
        oauth-->>acctmgr: refreshed token or error
    end

    proxy->>upstream: forward request with account auth
    alt success stream
        upstream-->>proxy: stream response
        proxy-->>client: stream response (strip hop-by-hop headers)
        proxy->>acctmgr: record success & update observability
    else transient errors (429/401/5xx/network)
        upstream-->>proxy: error / retry-after
        proxy->>acctmgr: mark cooldown / rotate
        proxy->>acctmgr: try next account
        proxy->>upstream: retry forwarding
    else pool exhausted
        proxy-->>client: json pool-exhausted (retry_after_ms)
    end
Loading

estimated code review effort

🎯 4 (complex) | ⏱️ ~60 minutes

specific review flags

missing regression tests:

  • session-affinity persistence across multiple client requests is not explicitly covered. see test/runtime-rotation-proxy.test.ts:1.
  • concurrent shadow CODEX_HOME usage and cleanup races are not exercised. see test/codex-bin-wrapper.test.ts:1 and scripts/codex.js:1.
  • account cooldown and persistence across proxy restarts lacks dedicated tests. see lib/runtime-rotation-proxy.ts:1 and test/runtime-rotation-proxy.test.ts:1.

windows edge cases:

  • windows file permission, rename/chmod behavior, and ntfs semantics could break shadowing or startup artifact creation. check scripts/codex.js:1 and lib/runtime/app-bind.ts:1.
  • app-launcher shortcut retargeting, pinned/taskbar behavior, and shell:AppsFolder reporting need more validation. see scripts/codex-app-launcher.js:1 and test/app-bind.test.ts:1.
  • postinstall auto-bind heuristics may misdetect packaged desktop installs on windows. see scripts/postinstall.js:1.

concurrency risks:

  • rotation state (lastAccountIndex, rotation trackers) may be mutated concurrently by parallel requests without clear synchronization. inspect lib/runtime-rotation-proxy.ts:1.
  • streaming while performing retries/cleanup can race with abort handling and may leak rotation state. inspect lib/runtime-rotation-proxy.ts:1.
  • shadow CODEX_HOME creation/cleanup in wrapper forwarding can collide across parallel forwarded execs. inspect scripts/codex.js:1.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning PR title does not follow conventional commits format; missing type prefix and exceeds lowercase imperative requirement for clarity. Use format: feat: add runtime rotation proxy (26 chars, lowercase, imperative). Title must start with type (feat/fix/chore/docs/refactor/test).
Docstring Coverage ⚠️ Warning Docstring coverage is 13.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR description is complete and covers all template sections: summary, what changed, validation steps, docs checklist items are addressed, risk/rollback noted. Greptile review summary included with specific files and sequence diagram.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/runtime-rotation-proxy
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feature/runtime-rotation-proxy

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread scripts/codex.js Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
test/schemas.test.ts (1)

33-77: 🧹 Nitpick | 🔵 Trivial

no negative-type regression for codexRuntimeRotationProxy.

test/schemas.test.ts:36 only exercises the happy path. the schema allows any truthy value today but if someone accidentally widens it to z.unknown() or z.any() the tests stay green. add a PluginConfigSchema.safeParse({ codexRuntimeRotationProxy: "yes" }).success === false assertion alongside the existing codexMode wrong-type case so schema drift on this new flag gets caught.

as per coding guidelines test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/schemas.test.ts` around lines 33 - 77, Add a negative-type test for
codexRuntimeRotationProxy: in test/schemas.test.ts add an assertion that
PluginConfigSchema.safeParse({ codexRuntimeRotationProxy: "yes" }).success is
false (similar to the existing codexMode wrong-type case) so the schema rejects
string/invalid types; ensure you reference PluginConfigSchema and
codexRuntimeRotationProxy and keep the test deterministic with vitest as the
other assertions.
docs/reference/settings.md (1)

139-150: ⚠️ Potential issue | 🟡 Minor

env override listed, but the codexRuntimeRotationProxy pluginConfig key is not documented in the backend controls table.

docs/reference/settings.md:188 advertises CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY as a common operator override, but the Rotation & Quota table at docs/reference/settings.md:139-149 never lists codexRuntimeRotationProxy (default false) as a setting, nor is there any explanation of what the proxy does, its precedence vs. env, or an upgrade note. operators reading this page in isolation will have no idea what the env var toggles.

as per coding guidelines docs/**: keep README, SECURITY, and docs consistent with actual CLI flags and workflows. whenever behavior changes, require updated upgrade notes and mention new npm scripts.

proposed additions
 ### Rotation & Quota
 
 | Key | Default | Effect |
 | --- | --- | --- |
+| `codexRuntimeRotationProxy` | `false` | Launch a localhost Responses proxy so forwarded Codex sessions rotate accounts between requests. Toggled via `codex auth rotation enable|disable`. |
 | `preemptiveQuotaEnabled` | `true` | Defer requests before remaining quota is critically low |

Also applies to: 188-188

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/settings.md` around lines 139 - 150, Add documentation for the
codexRuntimeRotationProxy pluginConfig key and the corresponding environment
variable CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY: update the Rotation & Quota
table to include `codexRuntimeRotationProxy` (default `false`), add a short
description of what the proxy does (e.g., routes rotation decisions through a
runtime proxy), specify precedence rules (pluginConfig overrides env unless
explicitly documented otherwise), and include a brief upgrade note describing
behavioral changes and any npm/CLI implications; reference the exact symbols
`codexRuntimeRotationProxy` and `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY` so
operators can find the mapping and precedence.
test/codex-manager-cli.test.ts (1)

944-969: ⚠️ Potential issue | 🟡 Minor

add a companion regression for state: "empty" with a windows-style storage path.

this new case covers intentional-reset, but the adjacent empty branch is not explicitly asserted here. please add one auth list regression that uses state: "empty" and a c:/... path to lock behavior for windows path formatting too. see test/codex-manager-cli.test.ts:946 and lib/storage.ts:1317.

proposed test add
+	it("prints empty account status for auth list when storage is missing (windows path)", async () => {
+		loadAccountsMock.mockResolvedValueOnce(null);
+		getStoragePathMock.mockReturnValue("C:/mock/openai-codex-accounts.json");
+		inspectStorageHealthMock.mockResolvedValueOnce({
+			state: "empty",
+			path: "C:/mock/openai-codex-accounts.json",
+			resetMarkerPath: "C:/mock/openai-codex-accounts.json.intentional-reset",
+			walPath: "C:/mock/openai-codex-accounts.json.wal",
+			hasResetMarker: false,
+			hasWal: false,
+			details: "storage file is missing",
+		});
+		const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
+		const { runCodexMultiAuthCli } = await import("../lib/codex-manager.js");
+
+		const exitCode = await runCodexMultiAuthCli(["auth", "list"]);
+
+		expect(exitCode).toBe(0);
+		expect(logSpy).toHaveBeenCalledWith(expect.stringContaining("No accounts configured"));
+		expect(logSpy).toHaveBeenCalledWith("Storage: C:/mock/openai-codex-accounts.json");
+		expect(logSpy).toHaveBeenCalledWith("Storage health: empty");
+	});

as per coding guidelines, test/**: “tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/codex-manager-cli.test.ts` around lines 944 - 969, Add a new Vitest
regression test alongside the existing "prints empty account status for auth
list" that mocks inspectStorageHealthMock to return state: "empty" and a
Windows-style path (e.g. "c:\\mock\\openai-codex-accounts.json"), mock
loadAccountsMock to resolve to null, call runCodexMultiAuthCli(["auth","list"])
and assert exitCode 0, that console.log was called with the empty-account
message and the Windows storage path output, and that setStoragePathMock was
called with null; use the same mocking helpers (loadAccountsMock,
inspectStorageHealthMock, setStoragePathMock) and runCodexMultiAuthCli to ensure
Windows path formatting in lib/storage.ts is covered.
scripts/codex.js (1)

389-449: 🧹 Nitpick | 🔵 Trivial

runtime proxy is spawned inside the unsupported-model retry loop — up to 4 server lifecycles per invocation.

scripts/codex.js:394-446 retries the whole forward (including createRuntimeRotationProxyContextIfEnabled) up to four times for model-fallback recovery. each retry starts a fresh node http server on an ephemeral port and tears it down. not broken, but it amplifies any listen/close race, and on windows it also churns the shadow CODEX_HOME tmpdir cleanup path that's already racy under EBUSY.

consider hoisting the proxy context above the retry loop and only swapping currentArgs on retry. scoped to this PR, at minimum add a comment documenting that each attempt spins a new server so the next reader doesn't chase a port leak.

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/development/CONFIG_FIELDS.md`:
- Line 204: Update the docs table row for the
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY environment variable to list the
accepted/explicit values (e.g., "1|true") so operators know the exact opt-in
tokens; ensure the documented values match the parsing/validation logic in
lib/config.ts and the expectations asserted in test/config.test.ts to keep
behavior consistent across code, tests, and release notes.

In `@docs/features.md`:
- Line 27: Update the Runtime rotation proxy capability row in docs/features.md
to indicate it is opt-in and disabled by default; change the description for the
"Runtime rotation proxy" row to append a short note like "(opt-in, disabled by
default via codexRuntimeRotationProxy=false)" so readers won’t assume it’s
enabled for all forwarded sessions — refer to the capability row text for
"Runtime rotation proxy" and the configuration symbol codexRuntimeRotationProxy
in lib/config.ts when making this wording tweak.

In `@docs/reference/commands.md`:
- Line 61: The table row contains unescaped pipe characters in the command
string `codex auth rotation enable|disable|status`, which breaks the Markdown
table; fix it by escaping each pipe (e.g., change to `codex auth rotation
enable\|disable\|status`) or replace pipes with the HTML entity `&#124;` within
that table cell so the row renders as a single cell; update the line containing
the `codex auth rotation enable|disable|status` command accordingly.

In `@docs/releases/v1.3.1.md`:
- Around line 26-31: Update the Runtime Rotation Proxy release notes to
explicitly reference the regression tests and related docs changes: mention the
CLI flags/flags `codexRuntimeRotationProxy`,
`CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=1`, and commands `codex auth rotation
enable|disable|status`, and add explicit test file references such as
test/runtime-rotation-proxy.test.ts and test/codex-bin-wrapper.test.ts (include
line or section pointers) so readers can verify coverage for concurrency and
Windows edge cases; also update docs/upgrade notes to reflect the new behavior
and add the new npm script entries referenced by this feature in the docs to
keep README/security/docs consistent.

In `@lib/codex-manager/commands/rotation.ts`:
- Line 57: The code silently calls deps.setStoragePath(null) in rotation.ts
before loading accounts, which mutates shared state for a read-only/status
subcommand; either remove that call, or scope it by saving and restoring the
previous value (e.g., const prev = deps.getStoragePath?.();
deps.setStoragePath(null); ...deps.setStoragePath(prev);) or replace it by
passing an explicit local storage override to the account-loading call instead
of mutating deps; also add a one-line comment explaining why a reset is required
if you choose to keep it.
- Around line 55-68: The status output in printRotationStatus currently uses
getCodexRuntimeRotationProxy(config) to derive the displayed "Runtime rotation
proxy" state but the runtime actually resolves env-first (per scripts/codex.js);
update printRotationStatus to compute the effective state using the same
precedence: check the CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY env (use a shared
parseBooleanEnv helper hoisted out of formatEnvOverride) and if unset fall back
to getCodexRuntimeRotationProxy(config), then log that effective value as the
headline; keep formatEnvOverride for showing overrides but ensure both functions
call the same parseBooleanEnv utility so the reported state matches runtime
behavior.

In `@lib/codex-manager/help.ts`:
- Around line 23-24: Help output for the "codex auth rotation" subcommand only
shows "status" and omits the "enable" and "disable" subcommands; update the help
text in lib/codex-manager/help.ts (the array/section that contains the
"Diagnostics:" entry and the "codex auth rotation status" line) to either list
"codex auth rotation enable" and "codex auth rotation disable" alongside
"status" or replace the single-line entry with a concise pointer such as "codex
auth rotation (enable|disable|status) — see 'codex auth rotation --help' so
operators can discover all toggles without editing pluginConfig by hand.

In `@lib/runtime-rotation-proxy.ts`:
- Around line 547-557: The shared RuntimeRotationProxyStatus object is mutated
concurrently across overlapping handleRequest executions causing non-atomic
pairs (lastError/lastAccountIndex) to interleave; change handleRequest to
capture per-request local counters/state (e.g., localTotalRequestsDelta,
localUpstreamRequestsDelta, localRetries, localRotations, localStreamsStarted,
and a local lastError/lastAccountIndex pair) and apply them back to the shared
status only once (atomic reduce) at the end of the request, or alternatively
store lastError and lastAccountIndex together (as a single object/tuple) so
getStatus() reads a consistent pair; update lib/runtime-rotation-proxy.ts around
the status initialization and handleRequest sites to use the per-request
snapshot-and-reduce approach and add a Promise.all-based concurrency test to
ensure no interleaving regressions.
- Around line 683-712: The 401 (upstream.status === HTTP_STATUS.UNAUTHORIZED)
branch in lib/runtime-rotation-proxy.ts is inconsistent with the 5xx branch
because it records failures and cool-downs but does not call
accountManager.refundToken(refreshed.account, context.family, context.model);
decide and fix by either (A) adding the same refundToken call in the 401 branch
(and incrementing status/retries/rotations as done for 5xx) so token bookkeeping
is consistent, or (B) explicitly document with an inline comment in the
UNAUTHORIZED branch (and likewise in the earlier 429 branch) that no refund is
performed because quota was consumed upstream/this response legitimately used
the token; update both branches (upstream.status === HTTP_STATUS.UNAUTHORIZED
and the 429 handling) accordingly to avoid future confusion.
- Around line 105-125: The createOutboundHeaders function contains a redundant
branch that re-sets OPENAI_HEADERS.ACCOUNT_ID to accountId when
account.accountId !== accountId; remove that no-op guard and assignment (the
second headers.set(OPENAI_HEADERS.ACCOUNT_ID, accountId) and its surrounding if
block) since ACCOUNT_ID is already set earlier in createOutboundHeaders and
accountId is resolved via resolveAccountId; leave the initial headers.set(...)
and other header logic intact.
- Around line 127-133: Replace the insecure === comparisons in
isAuthorizedClient with a constant-time comparison using Node's
crypto.timingSafeEqual: when extracting the bearer token (bearerMatch) or the
x-api-key header, convert both the candidate and clientApiKey to Buffers (e.g.,
Buffer.from(..., "utf8")), first check lengths and only call timingSafeEqual
when lengths match, otherwise treat as non-match; update isAuthorizedClient to
perform this buffered, length-checked timing-safe comparison for both the bearer
token branch and the x-api-key branch.
- Around line 144-150: readRequestBody currently accumulates the entire request
with no cap; change readRequestBody to enforce a 64 * 1024 * 1024 byte limit by
summing chunk lengths as it reads and throw or return a distinct error when
exceeded; ensure the upstream handler in the same module catches that error and
responds with 413 (use HTTP_STATUS.PAYLOAD_TOO_LARGE if you add
PAYLOAD_TOO_LARGE = 413 to the HTTP_STATUS enum, or use the literal 413), and
add a vitest in runtime-rotation-proxy.test.ts that posts a body larger than the
limit and asserts the response status is 413 to cover the error path.

In `@scripts/codex.js`:
- Around line 1036-1050: The parseRuntimeRotationProxyEnv function currently
logs an error every time it sees a malformed
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY, causing repeated warnings across
invocations; add a module-scoped "alreadyWarnedInvalidRuntimeRotationProxy"
boolean and only call console.error the first time (set the flag true after
logging) so subsequent calls to
parseRuntimeRotationProxyEnv/isRuntimeRotationProxyEnabled are silent; update
references to parseRuntimeRotationProxyEnv (and any callers like
isRuntimeRotationProxyEnabled) to use the same guarded behavior so the warning
is emitted once per process.
- Around line 407-413: The current early return when
createRuntimeRotationProxyContextIfEnabled yields null makes proxy failures
fatal; change createRuntimeRotationProxyContextIfEnabled (and its callers in
scripts/codex.js) to return the original compatibility context on
proxy-unavailable paths and only cause process exit when
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY_STRICT=1 is set; remove the return 1
branch at the call site and instead log a single warning when falling back; add
two vitest regressions in test/codex-bin-wrapper.test.ts that stub
startRuntimeRotationProxy to throw and assert the wrapper still forwards to the
real codex (fallback), and a sibling test that sets
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY_STRICT=1 and asserts the process exits
with code 1.
- Around line 1099-1125: The table-detection regex used in
rewriteTopLevelModelProvider (and the mirrored one in
removeRuntimeRotationProviderBlock) doesn't match TOML array-of-tables like
[[server]], causing model_provider to be appended into the wrong section; update
the regex to detect both single- and double-bracket headers (e.g. use a pattern
that allows one or two opening and closing brackets such as
/^\s*\[{1,2}[^\]]+\]{1,2}\s*$/) and apply this change in both
rewriteTopLevelModelProvider and removeRuntimeRotationProviderBlock so the
rewrittenLine is injected before any table/array-of-tables header; additionally
add a regression test that uses a file with a leading [[profiles.foo]] header to
assert model_provider is inserted at the top-level (not nested) when calling
rewriteTopLevelModelProvider/rewriteConfigTomlForRuntimeRotationProxy.

In `@test/codex-bin-wrapper.test.ts`:
- Around line 161-193: The fixture module created by
createRuntimeRotationProxyFixtureModule returns getStatus: () => ({}) which
doesn't match the RuntimeRotationProxyStatus contract and hides fallback
behavior; update the generated startRuntimeRotationProxy export to return a
status object containing startedAt, totalRequests, upstreamRequests, retries,
rotations, streamsStarted, lastError, and lastAccountIndex (use reasonable
defaults/zeroes and strings/nulls) and ensure the returned object shape matches
the real implementation in lib/runtime-rotation-proxy.ts (and usage in
test/runtime-rotation-proxy.test.ts); additionally add a negative test case that
simulates the module failing to start (e.g., make startRuntimeRotationProxy
throw or remove the module) to exercise the scripts/codex.js fallback path so
the regression is locked in.

In `@test/codex-manager-status-command.test.ts`:
- Around line 69-88: The test only covers restoreReason "intentional-reset" and
drops the return-value assertion; add tests exercising the other restoreReason
branches ("empty-storage" and "missing-storage") by creating deps via
createStatusDeps with loadAccounts returning version 3, empty accounts and
restoreReason set to each value, and mock inspectStorageHealth to return
undefined so those reasons map to the "empty" state; call runStatusCommand(deps)
and assert both that deps.logInfo was called with "No accounts configured." (or
the expected empty-state message) and that the command result equals 0 (to match
sibling tests).

In `@test/runtime-rotation-proxy.test.ts`:
- Around line 138-360: Add tests to cover the three missing regression cases and
tighten the pool-exhausted assertion: 1) Add a test that sends x-api-key:
runtime-secret (no Authorization header) against startRuntimeRotationProxy to
exercise the branch that checks headers.get("x-api-key") === clientApiKey. 2)
Add a concurrency test that fires N postResponses concurrently against
startProxy and then assert
accountManager.consumeToken/refundToken/markRateLimitedWithReason behavior and
proxy.getStatus() counters to ensure no negative budgets or duplicate account
picks. 3) Add a token-refresh race test that uses startProxy with accounts whose
expiresAt < now and issues two simultaneous requests so
ensureFreshAccessToken/queuedRefresh dedupes (assert saveAccountsMock was called
exactly once and both responses succeed). Finally, in the pool-exhausted test
replace the loose retry_after_ms > 0 check with an exact equality/assertion
against accountManager.getMinWaitTimeForFamily(family, model) to validate
writePoolExhausted returns the correct value; use helpers postResponses,
startProxy/startRuntimeRotationProxy and reference AccountManager and
saveAccountsMock to locate relevant mocks.

---

Outside diff comments:
In `@docs/reference/settings.md`:
- Around line 139-150: Add documentation for the codexRuntimeRotationProxy
pluginConfig key and the corresponding environment variable
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY: update the Rotation & Quota table to
include `codexRuntimeRotationProxy` (default `false`), add a short description
of what the proxy does (e.g., routes rotation decisions through a runtime
proxy), specify precedence rules (pluginConfig overrides env unless explicitly
documented otherwise), and include a brief upgrade note describing behavioral
changes and any npm/CLI implications; reference the exact symbols
`codexRuntimeRotationProxy` and `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY` so
operators can find the mapping and precedence.

In `@test/codex-manager-cli.test.ts`:
- Around line 944-969: Add a new Vitest regression test alongside the existing
"prints empty account status for auth list" that mocks inspectStorageHealthMock
to return state: "empty" and a Windows-style path (e.g.
"c:\\mock\\openai-codex-accounts.json"), mock loadAccountsMock to resolve to
null, call runCodexMultiAuthCli(["auth","list"]) and assert exitCode 0, that
console.log was called with the empty-account message and the Windows storage
path output, and that setStoragePathMock was called with null; use the same
mocking helpers (loadAccountsMock, inspectStorageHealthMock, setStoragePathMock)
and runCodexMultiAuthCli to ensure Windows path formatting in lib/storage.ts is
covered.

In `@test/schemas.test.ts`:
- Around line 33-77: Add a negative-type test for codexRuntimeRotationProxy: in
test/schemas.test.ts add an assertion that PluginConfigSchema.safeParse({
codexRuntimeRotationProxy: "yes" }).success is false (similar to the existing
codexMode wrong-type case) so the schema rejects string/invalid types; ensure
you reference PluginConfigSchema and codexRuntimeRotationProxy and keep the test
deterministic with vitest as the other assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c4aeb4c6-4189-4828-a856-ab980cbb4e4d

📥 Commits

Reviewing files that changed from the base of the PR and between 93774b5 and f22c96b.

📒 Files selected for processing (24)
  • README.md
  • docs/configuration.md
  • docs/development/CONFIG_FIELDS.md
  • docs/features.md
  • docs/reference/commands.md
  • docs/reference/settings.md
  • docs/releases/v1.3.1.md
  • lib/codex-manager.ts
  • lib/codex-manager/commands/rotation.ts
  • lib/codex-manager/commands/status.ts
  • lib/codex-manager/help.ts
  • lib/config.ts
  • lib/index.ts
  • lib/runtime-rotation-proxy.ts
  • lib/schemas.ts
  • scripts/codex-routing.js
  • scripts/codex.js
  • test/codex-bin-wrapper.test.ts
  • test/codex-manager-cli.test.ts
  • test/codex-manager-rotation-command.test.ts
  • test/codex-manager-status-command.test.ts
  • test/plugin-config.test.ts
  • test/runtime-rotation-proxy.test.ts
  • test/schemas.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (README.md)

Project storage paths should use ~/.codex/multi-auth/ as the default root directory, with per-project accounts stored under ~/.codex/multi-auth/projects/<project-key>/

Use CODEX_MULTI_AUTH_DIR environment variable to allow users to override the default settings/accounts root directory

Respect CODEX_MODE=0/1 environment variable to disable/enable Codex mode at runtime

Implement CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0/1 to allow users to opt in/out of live Responses proxy rotation for forwarded Codex sessions

Support CODEX_TUI_V2=0/1 to disable/enable TUI v2, and CODEX_TUI_COLOR_PROFILE and CODEX_TUI_GLYPHS for TUI customization

Allow CODEX_AUTH_FETCH_TIMEOUT_MS and CODEX_AUTH_STREAM_STALL_TIMEOUT_MS environment variables to override request and stream timeout defaults

Dashboard navigation should support hotkeys: Up/Down for movement, Enter to select, 1-9 for quick switch, / for search, ? for help toggle, and Q for back/cancel

Account details view should support hotkeys: S to set current account, R to refresh/re-login, E to enable/disable, and D to delete

Files:

  • lib/codex-manager/help.ts
  • test/schemas.test.ts
  • scripts/codex-routing.js
  • lib/index.ts
  • test/codex-manager-status-command.test.ts
  • lib/schemas.ts
  • lib/config.ts
  • lib/codex-manager.ts
  • lib/codex-manager/commands/status.ts
  • test/codex-manager-cli.test.ts
  • test/plugin-config.test.ts
  • test/codex-bin-wrapper.test.ts
  • test/runtime-rotation-proxy.test.ts
  • test/codex-manager-rotation-command.test.ts
  • lib/codex-manager/commands/rotation.ts
  • scripts/codex.js
  • lib/runtime-rotation-proxy.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/codex-manager/help.ts
  • lib/index.ts
  • lib/schemas.ts
  • lib/config.ts
  • lib/codex-manager.ts
  • lib/codex-manager/commands/status.ts
  • lib/codex-manager/commands/rotation.ts
  • lib/runtime-rotation-proxy.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/schemas.test.ts
  • test/codex-manager-status-command.test.ts
  • test/codex-manager-cli.test.ts
  • test/plugin-config.test.ts
  • test/codex-bin-wrapper.test.ts
  • test/runtime-rotation-proxy.test.ts
  • test/codex-manager-rotation-command.test.ts
docs/**

⚙️ CodeRabbit configuration file

keep README, SECURITY, and docs consistent with actual CLI flags and workflows. whenever behavior changes, require updated upgrade notes and mention new npm scripts.

Files:

  • docs/reference/settings.md
  • docs/features.md
  • docs/development/CONFIG_FIELDS.md
  • docs/releases/v1.3.1.md
  • docs/reference/commands.md
  • docs/configuration.md
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:39:51.027Z
Learning: Implement reliability behavior: disable whole-pool replay by default when every account is rate-limited, use bounded outbound request budgets, trigger cooldown on repeated cross-account 5xx bursts, and stagger proactive refresh
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:39:51.027Z
Learning: Maintain project-scoped account storage and isolation, allowing per-project account configurations while respecting the global multi-auth pool
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:39:51.027Z
Learning: Implement health-aware account selection with automatic failover across the multi-account OAuth pool
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:39:51.027Z
Learning: Support multi-auth probe visibility and runtime counters in `codex auth status` and `codex auth report` output for operational observability
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:02.283Z
Learning: Plugin runtime config source selection must follow resolution precedence: (1) Unified settings pluginConfig from settings.json when present and valid, (2) Fallback file config from CODEX_MULTI_AUTH_CONFIG_PATH when unified settings absent/invalid, (3) Hardcoded defaults. Environment variables override individual runtime settings after source selection.
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:02.283Z
Learning: Use stable environment overrides (CODEX_MULTI_AUTH_DIR, CODEX_MULTI_AUTH_CONFIG_PATH, CODEX_MODE, CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY, CODEX_TUI_V2, CODEX_TUI_COLOR_PROFILE, CODEX_TUI_GLYPHS, CODEX_AUTH_FETCH_TIMEOUT_MS, CODEX_AUTH_STREAM_STALL_TIMEOUT_MS) for common operational workflows
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:02.283Z
Learning: Advanced and internal environment overrides (CODEX_MULTI_AUTH_SYNC_CODEX_CLI, CODEX_MULTI_AUTH_REAL_CODEX_BIN, CODEX_MULTI_AUTH_BYPASS, CODEX_CLI_ACCOUNTS_PATH, CODEX_CLI_AUTH_PATH, refresh lease tuning variables) should only be used during debugging, controlled benchmarking, or maintainer workflows
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:20.558Z
Learning: Avoid manually editing lease/state files while the CLI is running, as cross-process refresh coordination relies on these files
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:20.558Z
Learning: When enabling `backgroundResponses` compatibility switch, test one known `background: true` request end to end before rolling across shared automation
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:20.558Z
Learning: Leave `backgroundResponses` disabled for existing stateless pipelines that do not intentionally send `background: true` requests
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:20.558Z
Learning: Enable `backgroundResponses` only for callers that need stateful background responses and can accept forced `store=true`, preserved input item IDs, and loss of stateless-only defaults such as fast-session trimming
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:29.527Z
Learning: Support project-scoped account pools for repo-specific account state separation
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:29.527Z
Learning: Implement linked-worktree identity sharing so the same repository can share account state across worktrees
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:29.527Z
Learning: Implement account dedupe and identity normalization to avoid duplicate saved account rows
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:29.527Z
Learning: Support backup and WAL recovery for safer persistence when local writes are interrupted or partially applied
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:29.527Z
Learning: Implement runtime rotation proxy to rotate managed accounts between Response requests without restarting sessions
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:29.527Z
Learning: Implement plugin runtime that uses the same account pool for request transformation, token refresh, retry handling, and capability-aware account selection
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:40.542Z
Learning: Use canonical command family `codex auth ...` for primary operations with compatibility alias support for `codex multi auth ...`, `codex multi-auth ...`, and `codex multiauth ...`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:40.542Z
Learning: Use `codex auth login --manual` or `codex auth login --no-browser` to force manual callback flow instead of launching a browser
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:40.542Z
Learning: Set `CODEX_AUTH_NO_BROWSER=1` to suppress browser launch in automation/headless sessions; false-like values such as `0` and `false` do not disable browser launch by themselves
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:40.542Z
Learning: In non-TTY/manual shells, pass the full redirect URL on stdin for `codex auth login --manual`, for example: `echo 'http://127.0.0.1:1455/auth/callback?code=...' | codex auth login --manual`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:40.542Z
Learning: Implement non-TTY fallback for auth flows to degrade to deterministic text behavior in non-TTY or host-managed sessions (including `CODEX_TUI=1`, `CODEX_DESKTOP=1`, `TERM_PROGRAM=codex`, or `ELECTRON_RUN_AS_NODE=1`)
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:40.542Z
Learning: In non-TTY fallback, `codex auth login` should default to add-account mode, skip the extra 'add another account' prompt, and auto-pick the default workspace selection when a follow-up choice is needed
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:40.542Z
Learning: Include `--explain` flag in `forecast` and `report` commands to include reasoning details (forecast text/JSON, report text)
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:40.542Z
Learning: Implement `codex auth rotation` command to manage the opt-in runtime Responses proxy with `enable`, `disable`, and `status` subcommands
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:40.542Z
Learning: When `codex auth rotation enable` is invoked, persist `codexRuntimeRotationProxy=true`; when `disable` is invoked, persist `codexRuntimeRotationProxy=false`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:40.542Z
Learning: Implement `codex auth verify` as a unified entry point for installation self-tests with `--paths`, `--flagged`, and `--all` flags; `verify-flagged` should continue to work as a back-compat alias
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:58.439Z
Learning: Use environment variable `CODEX_MULTI_AUTH_DIR` to change the settings file root directory from the default `~/.codex/multi-auth/`.
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:58.439Z
Learning: Use stable environment overrides like `CODEX_MODE`, `CODEX_TUI_V2`, `CODEX_TUI_COLOR_PROFILE`, `CODEX_TUI_GLYPHS`, `CODEX_AUTH_FETCH_TIMEOUT_MS`, and `CODEX_AUTH_STREAM_STALL_TIMEOUT_MS` for common operator configuration.
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:58.439Z
Learning: After making changes to settings, run validation commands: `codex auth status`, `codex auth check`, and `codex auth forecast --live` to ensure configuration is correct.
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:40:58.439Z
Learning: For most environments, enable: smart sort (`menuSortEnabled`), auto-fetch limits (`menuAutoFetchLimits`), live sync (`liveAccountSync`), session affinity (`sessionAffinity`), preemptive quota deferral (`preemptiveQuotaEnabled`), and proactive refresh guardian (`proactiveRefreshGuardian`).
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:41:00.614Z
Learning: Run npm build as part of the validation process for releases
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T13:41:00.614Z
Learning: Run test suite including model-map.test.ts, request-transformer.test.ts, codex-bin-wrapper.test.ts, config.test.ts, capability-policy.test.ts, fetch-helpers.test.ts, and index.test.ts as part of release validation
🪛 markdownlint-cli2 (0.22.1)
docs/reference/commands.md

[warning] 61-61: Table column count
Expected: 2; Actual: 4; Too many cells, extra data will be missing

(MD056, table-column-count)

🔇 Additional comments (10)
scripts/codex-routing.js (1)

14-14: lgtm.

adds rotation to the routed auth subcommand allowlist in scripts/codex-routing.js:14, matching the dispatcher branch and the aliases exercised in test/codex-manager-rotation-command.test.ts.

lib/schemas.ts (1)

27-27: lgtm.

optional boolean at lib/schemas.ts:27 aligns with DEFAULT_PLUGIN_CONFIG.codexRuntimeRotationProxy = false in lib/config.ts:154-223 and flows through sanitizePluginConfigRecord without breaking older configs that omit the field.

lib/index.ts (1)

35-35: star-export is safe—no symbol collisions found.

the barrel re-export at lib/index.ts:35 was flagged for potential collisions with sibling modules (session-affinity, refresh-guardian, etc.). verification shows all exported symbols use consistent module-specific prefixes: RuntimeRotationProxy*, SessionAffinity*, RefreshGuardian*, RefreshLease*, PreemptiveQuota*, LiveAccountSync*, CircuitBreaker*. no overlapping names like *Options or start* across module boundaries. the export is safe.

docs/development/CONFIG_FIELDS.md (1)

47-47: good config field addition for runtime proxy toggle.

this matches the new runtime rotation control surface and keeps config docs aligned with runtime wiring (lib/config.ts:line, lib/codex-manager/commands/rotation.ts:line).

based on learnings: use stable environment overrides (CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY) for common operational workflows.

test/codex-manager-cli.test.ts (3)

9-10: good mock addition.

inspectStorageHealthMock is declared cleanly and fits the suite’s existing mock pattern. see test/codex-manager-cli.test.ts:9.


183-184: mock wiring is correct.

binding inspectStorageHealth in the storage module mock is the right integration point for this cli behavior. see test/codex-manager-cli.test.ts:183.


661-836: beforeeach reset/default is solid and reduces async bleed risk.

resetting and priming inspectStorageHealthMock here keeps runs deterministic and avoids cross-test interference when tests execute with overlapping async work. see test/codex-manager-cli.test.ts:661 and test/codex-manager-cli.test.ts:828.

lib/codex-manager.ts (1)

337-3397: rotation command dispatch wiring looks good.

the new auth rotation branch is cleanly wired and passes the right config/storage deps for enable/disable/status without changing other command flows. reference: lib/codex-manager.ts:337.

lib/config.ts (1)

806-814: runtime rotation config resolution is correctly implemented.

the getter and explain metadata follow the expected env/config/default precedence and expose the new stable override cleanly. reference: lib/config.ts:806.

Also applies to: 1628-1632

test/plugin-config.test.ts (1)

1117-1131: good regression coverage for setting precedence.

this test directly locks in env > config > default behavior for runtime rotation proxy and prevents future precedence regressions. reference: test/plugin-config.test.ts:1117.

Comment thread docs/development/CONFIG_FIELDS.md Outdated
Comment thread docs/features.md Outdated
Comment thread docs/reference/commands.md Outdated
Comment thread docs/releases/v1.3.1.md
Comment thread lib/codex-manager/commands/rotation.ts Outdated
Comment thread scripts/codex.js
Comment thread scripts/codex.js Outdated
Comment thread test/codex-bin-wrapper.test.ts
Comment thread test/codex-manager-status-command.test.ts
Comment thread test/runtime-rotation-proxy.test.ts
Comment thread lib/runtime-rotation-proxy.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

♻️ Duplicate comments (6)
docs/features.md (1)

27-27: ⚠️ Potential issue | 🟡 Minor

capability row still reads as always-on; past review already asked for the opt-in qualifier.

this row in docs/features.md:27 was flagged on a prior commit and the wording hasn't moved — readers skimming the capability table will still assume runtime rotation is live on every forwarded session. codexRuntimeRotationProxy defaults to false in lib/config.ts, so the entry needs the opt-in / default-off qualifier to stay truthful and match the docs/configuration.md description.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/features.md` at line 27, Update the features table row to indicate
runtime rotation is opt-in by adding a qualifier like "opt-in / disabled by
default (codexRuntimeRotationProxy = false)" and/or "requires `codex auth
rotation enable`" so it matches the default in lib/config.ts and the
docs/configuration.md; reference the feature name codexRuntimeRotationProxy and
the existing CLI hint `codex auth rotation enable` (the row currently "Runtime
rotation proxy") when editing docs/features.md to make the default-off behavior
explicit.
docs/reference/commands.md (1)

61-61: ⚠️ Potential issue | 🟡 Minor

escape the pipe characters in the rotation command table row.

line 61 uses raw | inside a table cell, so markdown parsers split the row into extra columns. this makes the advanced command table render incorrectly even though the command set is valid (lib/codex-manager/commands/rotation.ts:294-313).

proposed docs fix
-| `codex auth rotation enable|disable|status|bind-app|unbind-app` | Manage the opt-in runtime Responses proxy for live Codex account rotation |
+| `codex auth rotation enable\|disable\|status\|bind-app\|unbind-app` | Manage the opt-in runtime Responses proxy for live Codex account rotation |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/commands.md` at line 61, The table row contains unescaped pipe
characters in the cell holding the rotation subcommands (the string shown as
codex auth rotation enable|disable|status|bind-app|unbind-app) which breaks the
Markdown table; edit the commands.md table row to escape each pipe as \| so the
entire command remains a single cell. Locate the row displaying the rotation
commands (ref: rotation.ts command set in lib/codex-manager/commands/rotation.ts
for the exact subcommand list) and replace each '|' with '\|' in that table cell
so the Markdown renders correctly.
test/codex-manager-status-command.test.ts (1)

116-135: ⚠️ Potential issue | 🟡 Minor

cover the remaining restorereason branches in status tests.

line 116 only exercises "intentional-reset". lib/codex-manager/commands/status.ts:85-88 still has explicit "empty-storage" and "missing-storage" paths that can regress silently. also keep the exit-code assertion parity (result === 0) in this block.

proposed test follow-up
 it("prints intentional reset state from empty storage metadata", async () => {
   const deps = createStatusDeps({
@@
   });
 
-  await runStatusCommand(deps);
+  const result = await runStatusCommand(deps);
+  expect(result).toBe(0);
@@
   expect(deps.logInfo).toHaveBeenCalledWith(
     "Storage health: intentional-reset",
   );
 });
+
+it.each(["empty-storage", "missing-storage"] as const)(
+  "treats %s restore metadata as empty health when inspector is unavailable",
+  async (reason) => {
+    const deps = createStatusDeps({
+      loadAccounts: vi.fn(async () => ({
+        version: 3,
+        activeIndex: 0,
+        activeIndexByFamily: {},
+        accounts: [],
+        restoreReason: reason,
+      })),
+      inspectStorageHealth: undefined,
+    });
+    const result = await runStatusCommand(deps);
+    expect(result).toBe(0);
+    expect(deps.logInfo).toHaveBeenCalledWith("No accounts configured.");
+    expect(deps.logInfo).toHaveBeenCalledWith("Storage health: empty");
+  },
+);

as per coding guidelines test/**: “tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/codex-manager-status-command.test.ts` around lines 116 - 135, Add
deterministic tests for the remaining restoreReason branches ("empty-storage"
and "missing-storage") in the same style as the existing "intentional-reset"
test: createStatusDeps with loadAccounts returning restoreReason set to
"empty-storage" and "missing-storage", call runStatusCommand(deps), assert
deps.logInfo was called with the corresponding human message (matching the
messages produced in lib/codex-manager/commands/status.ts for those branches)
and also assert the command result/exit code equals 0; use the same helpers
(createStatusDeps, runStatusCommand, deps.logInfo) so the new tests cover all
restoreReason paths and include the result === 0 parity check.
test/runtime-rotation-proxy.test.ts (1)

139-348: ⚠️ Potential issue | 🟠 Major

the proxy suite still misses the x-api-key, concurrent-request, and refresh-race regressions.

at test/runtime-rotation-proxy.test.ts:139-348, the suite only exercises single-request bearer flows. it still leaves three risky paths in lib/runtime-rotation-proxy.ts unpinned: the x-api-key auth branch, concurrent request handling against shared account state, and queued refresh dedupe when two expired-token requests race. the pool-exhaustion assertion is also still loose (retry_after_ms > 0) instead of checking the exact wait returned by the account manager. please add deterministic vitest cases for those paths before shipping this proxy. as per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/runtime-rotation-proxy.test.ts` around lines 139 - 348, The test suite
is missing deterministic regression tests for the x-api-key auth path,
concurrent-request account-state updates, refresh-token race dedupe, and a
strict pool-exhaustion retry_after_ms assertion; add vitest cases that (1)
exercise the x-api-key branch by sending a /responses request with x-api-key
instead of Bearer and assert the upstream call contains x-api-key and is
authorized via that branch (use startRuntimeRotationProxy or startProxy and
postResponses), (2) reproduce concurrent requests against shared AccountManager
by issuing parallel postResponses calls (Promise.all) and assert account
rotation and accountManager state updates are consistent (reference
AccountManager, getAccountByIndex and proxy.getStatus), (3) reproduce two
simultaneous expired-token requests that trigger token refresh and ensure the
refresh is deduped (simulate upstream responses in createRecordingFetch that
return 401/expired then successful token, assert only one refresh attempt and
both client responses succeed), and (4) tighten the pool-exhaustion test to
assert payload.error.retry_after_ms equals the exact value computed by
AccountManager/createStorage (use deterministic createStorage(now, ...) to
control timing) rather than just >0; ensure all tests use vitest
deterministically, the existing helpers (createRecordingFetch, createStorage,
startProxy/startRuntimeRotationProxy, postResponses) and never depend on real
secrets or nondeterministic timers.
scripts/codex.js (2)

1600-1639: ⚠️ Potential issue | 🔴 Critical

proxy startup failures still hard-stop forwarded codex sessions.

at scripts/codex.js:1604-1639, missing dist output, proxy bind failure, or shadow-home setup failure returns null, and scripts/codex.js:444-450 then exits 1 instead of forwarding with the already-built compatibility context. that turns an opt-in routing feature into a hard availability dependency for every forwarded exec / app-server / app session. please fall back to baseContext by default and reserve the hard stop for an explicit strict override, with a regression in test/codex-bin-wrapper.test.ts for fallback vs strict mode. based on learnings: support CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0/1 to opt in to live Codex Responses routing through the localhost account-rotation proxy.

Also applies to: 444-450

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/codex.js` around lines 1600 - 1639, The current
createRuntimeRotationProxyContextIfEnabled flow returns null on any proxy
startup or shadow-home error which causes caller hard-exit; change it to fall
back to returning baseContext (not null) on failures from
loadRuntimeRotationProxyModule, proxyModule.startRuntimeRotationProxy,
createRuntimeRotationProxyCodexHome, or proxyServer.close cleanup, and only
return null (or propagate a fatal error) if an explicit strict mode env/flag is
set (implement a new boolean check for CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY
that accepts "0"/"1" and a separate strict override like
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY_STRICT); keep
createRuntimeRotationAppHelperContext behavior for app commands, ensure
baseContext.cleanup is still called on best-effort cleanup, and add/update tests
in test/codex-bin-wrapper.test.ts to assert fallback-to-baseContext vs
strict-mode null/exit behavior and that opt-in via
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=1 triggers proxy attempts.

1119-1165: ⚠️ Potential issue | 🟠 Major

the toml header matcher still misses [[...]], so model_provider can be rewritten into the wrong section.

at scripts/codex.js:1129 and scripts/codex.js:1147, both helpers only recognize single-bracket TOML tables. configs that begin with [[profiles.foo]] or any other array-of-tables header will push the injected model_provider under that table instead of the top level, and the runtime proxy provider will never activate. match one-or-two bracket headers in both places and add a vitest regression in test/codex-bin-wrapper.test.ts with a leading [[...]] block.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/codex.js` around lines 1119 - 1165, The TOML header regex in
removeRuntimeRotationProviderBlock and rewriteTopLevelModelProvider only matches
single-bracket tables and misses array-of-tables like [[...]], so update both
header checks (the /^\s*\[[^\]]+\]\s*$/ tests in
removeRuntimeRotationProviderBlock and the isTable test in
rewriteTopLevelModelProvider) to match one-or-two-bracket headers (e.g.
/^\s*(\[\[|\[)[^\]]+(\]\]|\])\s*$/) so injected model_provider goes at
top-level, and add a vitest regression in codex-bin-wrapper.test.ts that
includes a leading [[...]] block to verify the behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/codex-manager/commands/rotation.ts`:
- Around line 154-170: formatAppRuntimeHelperStatus currently treats
isProcessAlive(status.pid) as sufficient proof the helper is the active router;
that can be false-positive on Windows when PIDs are reused and a stale
runtime-rotation-app-helper.json remains. Fix by gating liveness on status
freshness: in function formatAppRuntimeHelperStatus call
readAppRuntimeHelperStatus and reject a status if updatedAt or idleExpiresAt is
stale (e.g., older than a safe TTL or idleExpiresAt <= now), and/or require a
persisted instance token from the helper (augment readAppRuntimeHelperStatus to
return instanceToken and validate it) before trusting
isProcessAlive(status.pid); keep formatHelperLastAccount usage but ensure it
does not log sensitive tokens/emails. Add a vitest regression that writes a
stale runtime-rotation-app-helper.json with a reused PID and asserts
formatAppRuntimeHelperStatus reports “not running”; include tests that simulate
Windows EBUSY file contention on writes and 429-like retry behavior for any new
queue logic, and update tests to verify no tokens or emails appear in logs.

In `@lib/runtime-rotation-proxy.ts`:
- Around line 660-672: The loop currently uses accountManager.getAccountCount()
as the retry ceiling which allows a single request to iterate the entire pool;
change the per-request rotation budget to a bounded value (e.g., const
perRequestBudget = Math.min(accountManager.getAccountCount(), 3) or make 3–5
configurable) and replace the while condition to stop after
attemptedIndexes.size >= perRequestBudget while still keeping getAccountCount()
as the absolute safety cap elsewhere; when the loop stops due to hitting this
perRequestBudget, set exhaustionReason = "budget" (instead of "no-account") and
emit the same pool_exhausted path with reason: "budget" so callers/logging know
it was a per-request cap, referencing exhaustionReason, attemptedIndexes,
chooseAccount, accountManager.getAccountCount(), sessionAffinityStore and
context.sessionKey to locate the logic.
- Around line 205-213: The response currently emits a raw email in headers via
headers["x-codex-multi-auth-account-email"] using
sanitizeHeaderValue(accountIdentity.email), which leaks soft PII; remove that
header emission or replace it with a non-reversible derived value (e.g.,
HMAC/SHA256 hash) before setting the header; locate the block that checks
accountIdentity and modifies headers (references: accountIdentity,
sanitizeHeaderValue, headers["x-codex-multi-auth-account-email"]) and either
delete the line that sets the email header or compute and set a hashed/redacted
value instead, keeping index/label/accountId behavior unchanged.
- Around line 722-729: The fetch to upstream in the request handling path lacks
timeouts and the streaming reader has no stall detection; wrap the fetchImpl
call in runtime-rotation-proxy.ts with an AbortController wired to
getFetchTimeoutMs(pluginConfig) so the fetch is aborted on timeout (the existing
catch will handle refund/rotate), and add a stall timer inside
forwardStreamingResponse that resets on each received chunk and aborts the
reader (calling onStreamError()) when getStreamStallTimeoutMs(pluginConfig)
elapses with no data; also add a vitest in test/runtime-rotation-proxy.test.ts
that stubs fetchImpl to never resolve and asserts rotation and pool-exhausted
behavior occurs within the fetch timeout window.

In `@lib/runtime/app-bind.ts`:
- Around line 484-503: spawnRouter currently starts the router with stdio:
"ignore" which discards stdout/stderr while the mac/windows persistent launchers
point their StandardOutPath/StandardErrorPath to state.logPath; change
spawnRouter to open state.logPath in append mode (e.g.
fs.openSync(state.logPath, "a")) and pass stdio: ["ignore", logFd, logFd] (using
the opened fd) when calling spawn so CLI-launched router writes to the same log
file as the persistent agents; ensure the opened fd is a stable integer passed
into spawn and closed when no longer needed.
- Around line 392-406: findAvailablePort currently closes the probe socket
before spawnRouter binds, allowing a TOCTOU race; change the flow in
bindCodexAppRuntimeRotation to perform a bounded retry loop: call
findAvailablePort, invoke spawnRouter with that port, then call
waitForRouterStatus (modify waitForRouterStatus to return a boolean success
flag) and if it returns false retry findAvailablePort+spawnRouter up to N
attempts before failing; ensure spawnRouter and bindCodexAppRuntimeRotation
surface a hard error (instead of returning a "bound" message) when
waitForRouterStatus remains false, and include the waitForRouterStatus result in
the AppBindResult.message so failures are visible.
- Around line 593-602: The current direct writeFile calls that overwrite
paths.configPath, paths.backupPath and paths.statePath in
lib/runtime/app-bind.ts must be replaced with an atomic-write helper: write to a
temp file in the same directory, fsync the file descriptor, fsync the directory,
then rename to the final path (to survive AV/locks and power loss) and handle
transient EPERM/EBUSY by retrying a few times with backoff; update the write at
the bind side (the writeFile for paths.configPath at the spot shown) and the
corresponding unbind writes (the other writeFile sites referenced in your
comment) to use this helper; add a Vitest in test/app-bind.test.ts that
stubs/throws EBUSY for the initial direct write attempt and asserts the
atomic-write helper retries and leaves a valid intact config.toml (i.e., no
truncated file) and that the backup remains intact if rename succeeds.
- Around line 100-116: The removal logic in removeRuntimeRotationProviderBlock
erroneously stops skipping on any section header (e.g.
`[model_providers.codex-multi-auth-runtime-proxy.http_headers]`) instead of
treating subtables as part of the same block; update the stop condition so that
while skipping you only stop when you encounter a new section whose header does
NOT begin with `model_providers.${RUNTIME_ROTATION_PROXY_PROVIDER_ID}` (use a
regex that matches section headers and checks the prefix), keep the rest of the
function unchanged, and add a vitest in test/app-bind.test.ts that supplies a
config containing `[model_providers.${RUNTIME_ROTATION_PROXY_PROVIDER_ID}]` plus
a subtable like
`[model_providers.${RUNTIME_ROTATION_PROXY_PROVIDER_ID}.http_headers]` and
assert the entire group (both the provider block and its subtables) is removed.
- Around line 186-204: restoreConfigTomlFromAppBind currently forces LF line
endings (via ensureTrailingNewline) and loses CRLF round-trips; change
restoreConfigTomlFromAppBind to detect and preserve the original file's
lineEnding (reuse the same detection logic as rewriteConfigTomlForAppBind) and
ensureTrailingNewline respects that detected lineEnding instead of always using
"\n". Also wrap all raw writeFile calls invoked during bind/unbind (the places
that call writeFile from this module) with the existing withFileOperationRetry
helper (same behavior used in test/app-bind.test.ts) so EBUSY/file-lock errors
are retried; update tests to add a CRLF round-trip case (write with "\r\n" and
assert restore preserves "\r\n"), and add tests for idempotent re-bind,
provider-only-ending-without-trailing-newline, and restore when original lacked
top-level model_provider.

In `@package.json`:
- Line 104: The postinstall script currently only skips bind logic when
!isGlobalNpmInstall(env) or !rotationEnabled, but it should also detect CI
environments and npm_config_ignore_scripts; update scripts/postinstall.js to
short-circuit when CI is detected (check common CI envs like CI, GITHUB_ACTIONS,
GITLAB_CI, CIRCLECI, and process.env.npm_config_ignore_scripts) in the same
guard area where isGlobalNpmInstall(env) and rotationEnabled are evaluated
(refer to the existing checks around lines referenced by isGlobalNpmInstall and
rotationEnabled); then add a unit/integration test in
test/install-codex-auth.test.ts that sets CI=true and GITHUB_ACTIONS=true (and
toggles npm_config_ignore_scripts) to assert the postinstall path is a no-op
under CI.

In `@scripts/codex-app-launcher.js`:
- Around line 399-415: The code grabs the last non-empty stdout line into output
and calls JSON.parse(output) without context; wrap the parse in a try/catch
around the JSON.parse call (referencing the output variable and the place where
runCommand is awaited) and on failure throw a new Error that includes the
original parse error.message plus the raw output string so the caller (which
logs error.message) receives full diagnostic context.
- Around line 345-352: The current createMacLauncherScript uses JSON.stringify
to quote plan values which is unsafe for POSIX shells; modify
createMacLauncherScript to use the existing shell-quoter quoteDesktopExec for
all embedded values: quote the working directory, quote plan.commandPath, and
quote each entry of plan.commandArgs (map quoteDesktopExec over plan.commandArgs
and join with spaces) so the generated lines become cd <quoted-workingDirectory>
and exec <quoted-commandPath> <quoted-args>; keep the same array->join("\n")
output but replace JSON.stringify usages with quoteDesktopExec calls.
- Around line 79-81: The helper resolveWindowsDesktopDir in
scripts/codex-app-launcher.js currently hardcodes join(home, "Desktop"), which
fails for OneDrive-redirected or policy-redirected desktops; change resolution
to query the authoritative Windows shell folder instead (invoke PowerShell to
read [Environment]::GetFolderPath('Desktop') or run reg query
"HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" /v
Desktop) and return that path from resolveWindowsDesktopDir so the launcher uses
the real desktop root; also add a regression test in
test/install-codex-auth.test.ts that stubs OneDrive/overrides home and asserts
the resolved roots include the redirected desktop path returned by the new
resolution logic.
- Around line 242-294: The backup routine is incorrectly capturing
already-managed shortcuts as "original" because it only checks
$BackupByPath.ContainsKey($Path) before adding backups; change the logic in the
routing loop that populates $BackupsToWrite/$BackupByPath (the block around
$BackupByPath.ContainsKey($Path) and the creation of $Backup) to first inspect
the shortcut sentinel (e.g. $Shortcut.Description and $Shortcut.TargetPath) and
only record the current shortcut as the original if it is NOT already managed by
our wrapper (i.e. Description != 'Launch Codex through codex-multi-auth runtime
rotation' and TargetPath != our known wrapper path); leave existing backups
untouched otherwise and only add new entries when the shortcut is unmanaged so
restores can recover real original targets.

In `@scripts/codex-app-router.js`:
- Around line 85-93: The host selection currently accepts stateRecord.host or
args.host without restricting to loopback, which can expose the proxy; change
the host validation in scripts/codex-app-router.js after computing host to
assert it is loopback-only (accept "localhost", "127.0.0.1", "::1" and their
bracketed/hostname forms) and fail startup (throw or process.exit with an error)
if not loopback; ensure the subsequent binding that uses the host variable (the
router/proxy bind code) will only receive a validated loopback host. Also add a
regression test in test/app-bind.test.ts that starts the router/initialization
with a non-loopback host (e.g., "0.0.0.0" or "192.168.0.1") and asserts startup
is rejected before binding when codexRuntimeRotationProxy is enabled.

In `@scripts/codex.js`:
- Around line 1214-1238: The current syncShadowHomeStateBack loop operates
per-file (SHADOW_HOME_STATE_FILES) causing cross-process interleaving; change it
to acquire a cross-process bundle lease/lock (e.g., a lease file in
shadowCodexHome) before comparing/writing any files so the whole bundle is
synced atomically. Implement: in syncShadowHomeStateBack acquire a lease/lock
file (with timeout and safe fail if not acquired), then read
originalShadowHomeState and captureShadowHomeState for all names, decide the
bundle update, write bundle updates to temp files and perform atomic renames to
originalCodexHome, call tightenShadowHomePermissions, and finally release the
lease; ensure syncShadowHomeStateFile is only used internally after the lease is
held or refactor its calls into the bundle update path, and keep best-effort
catch to not crash if lease operations fail.

---

Duplicate comments:
In `@docs/features.md`:
- Line 27: Update the features table row to indicate runtime rotation is opt-in
by adding a qualifier like "opt-in / disabled by default
(codexRuntimeRotationProxy = false)" and/or "requires `codex auth rotation
enable`" so it matches the default in lib/config.ts and the
docs/configuration.md; reference the feature name codexRuntimeRotationProxy and
the existing CLI hint `codex auth rotation enable` (the row currently "Runtime
rotation proxy") when editing docs/features.md to make the default-off behavior
explicit.

In `@docs/reference/commands.md`:
- Line 61: The table row contains unescaped pipe characters in the cell holding
the rotation subcommands (the string shown as codex auth rotation
enable|disable|status|bind-app|unbind-app) which breaks the Markdown table; edit
the commands.md table row to escape each pipe as \| so the entire command
remains a single cell. Locate the row displaying the rotation commands (ref:
rotation.ts command set in lib/codex-manager/commands/rotation.ts for the exact
subcommand list) and replace each '|' with '\|' in that table cell so the
Markdown renders correctly.

In `@scripts/codex.js`:
- Around line 1600-1639: The current createRuntimeRotationProxyContextIfEnabled
flow returns null on any proxy startup or shadow-home error which causes caller
hard-exit; change it to fall back to returning baseContext (not null) on
failures from loadRuntimeRotationProxyModule,
proxyModule.startRuntimeRotationProxy, createRuntimeRotationProxyCodexHome, or
proxyServer.close cleanup, and only return null (or propagate a fatal error) if
an explicit strict mode env/flag is set (implement a new boolean check for
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY that accepts "0"/"1" and a separate
strict override like CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY_STRICT); keep
createRuntimeRotationAppHelperContext behavior for app commands, ensure
baseContext.cleanup is still called on best-effort cleanup, and add/update tests
in test/codex-bin-wrapper.test.ts to assert fallback-to-baseContext vs
strict-mode null/exit behavior and that opt-in via
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=1 triggers proxy attempts.
- Around line 1119-1165: The TOML header regex in
removeRuntimeRotationProviderBlock and rewriteTopLevelModelProvider only matches
single-bracket tables and misses array-of-tables like [[...]], so update both
header checks (the /^\s*\[[^\]]+\]\s*$/ tests in
removeRuntimeRotationProviderBlock and the isTable test in
rewriteTopLevelModelProvider) to match one-or-two-bracket headers (e.g.
/^\s*(\[\[|\[)[^\]]+(\]\]|\])\s*$/) so injected model_provider goes at
top-level, and add a vitest regression in codex-bin-wrapper.test.ts that
includes a leading [[...]] block to verify the behavior.

In `@test/codex-manager-status-command.test.ts`:
- Around line 116-135: Add deterministic tests for the remaining restoreReason
branches ("empty-storage" and "missing-storage") in the same style as the
existing "intentional-reset" test: createStatusDeps with loadAccounts returning
restoreReason set to "empty-storage" and "missing-storage", call
runStatusCommand(deps), assert deps.logInfo was called with the corresponding
human message (matching the messages produced in
lib/codex-manager/commands/status.ts for those branches) and also assert the
command result/exit code equals 0; use the same helpers (createStatusDeps,
runStatusCommand, deps.logInfo) so the new tests cover all restoreReason paths
and include the result === 0 parity check.

In `@test/runtime-rotation-proxy.test.ts`:
- Around line 139-348: The test suite is missing deterministic regression tests
for the x-api-key auth path, concurrent-request account-state updates,
refresh-token race dedupe, and a strict pool-exhaustion retry_after_ms
assertion; add vitest cases that (1) exercise the x-api-key branch by sending a
/responses request with x-api-key instead of Bearer and assert the upstream call
contains x-api-key and is authorized via that branch (use
startRuntimeRotationProxy or startProxy and postResponses), (2) reproduce
concurrent requests against shared AccountManager by issuing parallel
postResponses calls (Promise.all) and assert account rotation and accountManager
state updates are consistent (reference AccountManager, getAccountByIndex and
proxy.getStatus), (3) reproduce two simultaneous expired-token requests that
trigger token refresh and ensure the refresh is deduped (simulate upstream
responses in createRecordingFetch that return 401/expired then successful token,
assert only one refresh attempt and both client responses succeed), and (4)
tighten the pool-exhaustion test to assert payload.error.retry_after_ms equals
the exact value computed by AccountManager/createStorage (use deterministic
createStorage(now, ...) to control timing) rather than just >0; ensure all tests
use vitest deterministically, the existing helpers (createRecordingFetch,
createStorage, startProxy/startRuntimeRotationProxy, postResponses) and never
depend on real secrets or nondeterministic timers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9b4dfcf7-8b3b-457a-a71f-c88959b2005e

📥 Commits

Reviewing files that changed from the base of the PR and between f22c96b and dffbe2e.

📒 Files selected for processing (26)
  • README.md
  • docs/configuration.md
  • docs/features.md
  • docs/reference/commands.md
  • docs/reference/settings.md
  • docs/releases/v1.3.1.md
  • lib/codex-manager.ts
  • lib/codex-manager/commands/rotation.ts
  • lib/codex-manager/commands/status.ts
  • lib/runtime-rotation-proxy.ts
  • lib/runtime/app-bind.ts
  • lib/runtime/runtime-observability.ts
  • package.json
  • scripts/codex-app-launcher.js
  • scripts/codex-app-router.js
  • scripts/codex.js
  • scripts/postinstall.js
  • test/app-bind.test.ts
  • test/codex-bin-wrapper.test.ts
  • test/codex-manager-rotation-command.test.ts
  • test/codex-manager-status-command.test.ts
  • test/documentation.test.ts
  • test/install-codex-auth.test.ts
  • test/package-bin.test.ts
  • test/runtime-observability.test.ts
  • test/runtime-rotation-proxy.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{json,js,ts}

📄 CodeRabbit inference engine (README.md)

Store OAuth account credentials in project-scoped directories under ~/.codex/multi-auth/projects/<project-key>/ with central settings in ~/.codex/multi-auth/settings.json

Files:

  • test/documentation.test.ts
  • test/package-bin.test.ts
  • lib/runtime/runtime-observability.ts
  • package.json
  • test/runtime-observability.test.ts
  • lib/codex-manager/commands/status.ts
  • scripts/codex-app-router.js
  • lib/codex-manager.ts
  • test/codex-manager-status-command.test.ts
  • test/install-codex-auth.test.ts
  • test/codex-manager-rotation-command.test.ts
  • test/app-bind.test.ts
  • lib/codex-manager/commands/rotation.ts
  • scripts/postinstall.js
  • test/runtime-rotation-proxy.test.ts
  • scripts/codex-app-launcher.js
  • lib/runtime-rotation-proxy.ts
  • test/codex-bin-wrapper.test.ts
  • lib/runtime/app-bind.ts
  • scripts/codex.js
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/documentation.test.ts
  • test/package-bin.test.ts
  • test/runtime-observability.test.ts
  • test/codex-manager-status-command.test.ts
  • test/install-codex-auth.test.ts
  • test/codex-manager-rotation-command.test.ts
  • test/app-bind.test.ts
  • test/runtime-rotation-proxy.test.ts
  • test/codex-bin-wrapper.test.ts
docs/**

⚙️ CodeRabbit configuration file

keep README, SECURITY, and docs consistent with actual CLI flags and workflows. whenever behavior changes, require updated upgrade notes and mention new npm scripts.

Files:

  • docs/reference/settings.md
  • docs/features.md
  • docs/releases/v1.3.1.md
  • docs/reference/commands.md
  • docs/configuration.md
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/runtime/runtime-observability.ts
  • lib/codex-manager/commands/status.ts
  • lib/codex-manager.ts
  • lib/codex-manager/commands/rotation.ts
  • lib/runtime-rotation-proxy.ts
  • lib/runtime/app-bind.ts
**/commands/**/*.{ts,js}

📄 CodeRabbit inference engine (README.md)

Implement codex auth command toolkit with subcommands: login, status, check, list, switch, forecast, verify-flagged, verify, fix, doctor, why-selected, and rotation status

Files:

  • lib/codex-manager/commands/status.ts
  • lib/codex-manager/commands/rotation.ts
**/commands/status.{ts,js}

📄 CodeRabbit inference engine (README.md)

Surface recent runtime request metrics in codex auth status text output and expose machine-readable cooldown/runtime snapshot in codex auth report --json

Files:

  • lib/codex-manager/commands/status.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:23.228Z
Learning: Override configuration root with `CODEX_MULTI_AUTH_DIR` environment variable for custom storage paths
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:23.228Z
Learning: Disable whole-pool replay by default when all accounts are rate-limited to prevent infinite retry loops
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:23.228Z
Learning: Implement bounded outbound request budget per active request to prevent a single prompt from walking the full account pool indefinitely
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:23.228Z
Learning: Trigger short cooldown after repeated cross-account 5xx bursts instead of continuing aggressive account rotation
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:23.228Z
Learning: Stagger proactive token refresh to reduce background refresh bursts across the account pool
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:23.228Z
Learning: Validate configuration after changes by running `codex auth status`, `codex auth check`, and `codex auth forecast --live`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:23.228Z
Learning: Implement non-destructive experimental settings flows with preview-before-apply sync, destination-only account preservation, and fail-safe backup filename collision handling
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:23.228Z
Learning: Keep Responses background mode as opt-in only; enable `backgroundResponses` in settings or via `CODEX_AUTH_BACKGROUND_RESPONSES=1` only for callers that intentionally send `background: true`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Use `CODEX_MULTI_AUTH_CONFIG_PATH` environment variable to load configuration from an alternate path when unified settings are absent or invalid
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Use `CODEX_MULTI_AUTH_DIR` environment variable to override the root directory for plugin-managed runtime files
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Implement configuration resolution precedence: unified settings `pluginConfig` from `settings.json` first, fallback to `CODEX_MULTI_AUTH_CONFIG_PATH`, then hardcoded defaults
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Environment variables override individual runtime settings after config source selection
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Resolve dashboard display values from persisted `dashboardDisplaySettings` and then normalize defaults
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Support `CODEX_MODE=0/1` to disable or enable Codex mode
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Support `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0/1` to opt in to live Codex Responses routing through the localhost account-rotation proxy
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Support `CODEX_TUI_V2=0/1` to disable or enable TUI v2
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Support `CODEX_TUI_COLOR_PROFILE` environment variable to select color profile (truecolor, ansi256, or ansi16)
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Support `CODEX_TUI_GLYPHS` environment variable to select glyph mode (ascii, unicode, or auto)
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Support `CODEX_AUTH_FETCH_TIMEOUT_MS` environment variable to override HTTP request timeout
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Support `CODEX_AUTH_STREAM_STALL_TIMEOUT_MS` environment variable to override stream stall timeout
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: When `codexRuntimeRotationProxy` is enabled, the wrapper starts a localhost-only Responses proxy for forwarded official Codex sessions and replaces outbound auth headers with the selected managed account
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: `codex auth rotation enable` binds the packaged desktop app to a persistent localhost router by backing up the real Codex `config.toml`, writing the `codex-multi-auth-runtime-proxy` provider, starting the router, and installing a startup entry
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: `codex auth rotation disable` and `codex auth rotation unbind-app` stop the router, remove the startup entry, and restore the backed-up Codex config
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Set `CODEX_MULTI_AUTH_APP_BIND_INSTALL=0` to skip install/update self-heal, or `CODEX_MULTI_AUTH_APP_BIND_INSTALL=1` to force it
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: Set `CODEX_MULTI_AUTH_APP_LAUNCHER_INSTALL=0` before enabling rotation to skip shortcut routing, or run `codex-multi-auth-app-launcher --remove` to restore backed-up Windows shortcuts or remove the managed macOS wrapper
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:58:37.059Z
Learning: The wrapper and plugin should try GPT model aliases directly and only fall back to `gpt-5.4` after a real ChatGPT Codex unsupported-model response
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: After enabling `backgroundResponses`, test one known `background: true` request end-to-end before rolling across shared automation
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_MULTI_AUTH_DIR` to override the custom root directory for settings, accounts, cache, and logs
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_MULTI_AUTH_CONFIG_PATH` to specify an alternate config file input
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_MODE` to toggle Codex mode
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY` to toggle opt-in localhost Responses proxy for forwarded Codex sessions
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_TUI_V2` to toggle TUI v2
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_TUI_COLOR_PROFILE` to set TUI color profile
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_TUI_GLYPHS` to set TUI glyph mode
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_AUTH_FETCH_TIMEOUT_MS` to override request timeout
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_AUTH_STREAM_STALL_TIMEOUT_MS` to override stream stall timeout
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_MULTI_AUTH_SYNC_CODEX_CLI` to toggle Codex CLI state sync
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_MULTI_AUTH_REAL_CODEX_BIN` to force official Codex binary path
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use environment variable `CODEX_MULTI_AUTH_BYPASS` to bypass local auth handling
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use temp-file + rename semantics for storage writes to handle transient `EPERM`/`EBUSY` errors on Windows
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Use lease/state files for cross-process refresh coordination; avoid manually editing those files while the CLI is running
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Combine `fs.watch` with polling fallback to handle Windows file watcher edge cases for live account sync
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:01.564Z
Learning: Allow backup/WAL artifacts to exist briefly during storage writes and recovery as part of normal safety behavior
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Store account data in canonical local data root at `~/.codex/multi-auth`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement project-scoped account pools for repo-specific account state separation
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Support linked-worktree identity sharing to allow the same repository to share account state across worktrees
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Persist quota cache between runs for faster forecast and dashboard visibility
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement multi-account dashboard login via `codex auth login` command to add and manage multiple OAuth identities
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement onboarding backup restore to restore latest named backup or allow manual selection when recovering saved accounts
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement account dedupe and identity normalization in login flow to avoid duplicate saved account rows
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement explicit active-account switching via `codex auth switch <index>` command to pick current account by index
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement fast and deep health checks via `codex auth check` command to verify pool usability
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement flagged-account verification and restore via `codex auth verify-flagged` to recover sidelined accounts
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement readiness and risk forecast via `codex auth forecast` command to suggest the best next account
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Support live quota probe mode via `codex auth forecast --live` using live headers for stronger decisions
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement JSON report output via `codex auth report --live --json` for automation and support workflows
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement runtime rotation proxy via `codex auth rotation enable` to allow managed accounts rotation between requests without session restart
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement safe repair workflow via `codex auth fix` to detect and repair local storage inconsistencies
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement diagnostics with optional repair via `codex auth doctor` command to inspect and fix common failures
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement backup and WAL recovery at storage runtime for safer persistence during interrupted or partially applied local writes
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Keep OAuth login browser-first by default, while supporting `--manual`, `--no-browser`, and `CODEX_AUTH_NO_BROWSER=1` fallback options for browser-restricted shells
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Support manual/non-TTY login by accepting the full callback URL on stdin for automation and host-managed shells
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement quick switch and search hotkeys in dashboard for faster navigation
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement account action hotkeys in dashboard for per-account set, refresh, toggle, and delete shortcuts
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement in-dashboard settings hub for runtime and display tuning without editing files directly
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: Implement experimental settings hotkeys in dashboard for sync preview, backup export, and refresh-guard tuning
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:16.545Z
Learning: When implementing plugin-host runtime, use the same account pool for request transformation, token refresh, retry handling, session affinity, and capability-aware account selection
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:35.344Z
Learning: The codex-multi-auth documentation structure should include references to: features.md, public-api.md, error-contracts.md, settings.md, and troubleshooting.md
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:54.636Z
Learning: Configuration settings file location should default to `~/.codex/multi-auth/settings.json` unless `CODEX_MULTI_AUTH_DIR` environment variable is set
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:54.636Z
Learning: Recommended defaults for most environments include: smart sort enabled, auto-fetch limits enabled, live sync enabled, session affinity enabled, preemptive quota deferral enabled, and proactive refresh guardian enabled
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:54.636Z
Learning: After configuration changes, validation should be performed using: `codex auth status`, `codex auth check`, and `codex auth forecast --live` commands
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T14:59:54.636Z
Learning: Environment variable overrides for stable configurations should include: CODEX_MULTI_AUTH_DIR, CODEX_MULTI_AUTH_CONFIG_PATH, CODEX_MODE, CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY, CODEX_MULTI_AUTH_APP_BIND_INSTALL, CODEX_TUI_V2, CODEX_TUI_COLOR_PROFILE, CODEX_TUI_GLYPHS, CODEX_AUTH_FETCH_TIMEOUT_MS, CODEX_AUTH_STREAM_STALL_TIMEOUT_MS
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T15:00:02.010Z
Learning: Preserve deterministic fallback behavior for older official Codex runtimes and non-entitled or quota-limited accounts
🔇 Additional comments (4)
lib/runtime-rotation-proxy.ts (4)

139-141: still a dead branch.

lib/runtime-rotation-proxy.ts:136 already sets ACCOUNT_ID to accountId, and accountId is the exact value re-written here. the guard is a strict no-op and will bait a future reader into mis-editing it.


145-151: constant-time compare still missing.

lib/runtime-rotation-proxy.ts:149-150 still uses === on the bearer and x-api-key. loopback + 32-byte secret keeps the attack surface small, but any local process (ci agents, sidecar extensions, snoopy dev tools) can probe. crypto.timingSafeEqual with length guard is trivial.


217-223: still no body-size cap.

lib/runtime-rotation-proxy.ts:219-221 drains every chunk into memory with no ceiling; any loopback caller (including the default clientApiKey === null branch) can exhaust heap. same 64 MiB cap + 413 response still applies, plus the missing regression test in test/runtime-rotation-proxy.test.ts.


766-795: 401 still does not refund; 5xx does.

lib/runtime-rotation-proxy.ts:766-778 (401) skips refundToken, lib/runtime-rotation-proxy.ts:781-795 (5xx) calls it, and lib/runtime-rotation-proxy.ts:745-764 (429) also skips intentionally. pick one policy and drop a single-line comment in each branch so the next person doesn't "fix" the divergence in the wrong direction. if the intent is "401 never charged upstream quota, so refund", mirror the 5xx branch.

Comment thread lib/codex-manager/commands/rotation.ts
Comment thread lib/runtime-rotation-proxy.ts Outdated
Comment thread lib/runtime-rotation-proxy.ts
Comment thread lib/runtime-rotation-proxy.ts Outdated
Comment thread lib/runtime/app-bind.ts Outdated
Comment thread scripts/codex-app-launcher.js
Comment thread scripts/codex-app-launcher.js
Comment thread scripts/codex-app-launcher.js
Comment thread scripts/codex-app-router.js Outdated
Comment thread scripts/codex.js Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/codex.js (1)

537-601: ⚠️ Potential issue | 🔴 Critical

no error listeners on stdin/stdout/stderr streams in protocol-proxy path — unhandled stream errors crash the wrapper.

scripts/codex.js:548 calls child.stdin.write(chunk) with only a destroyed guard, but node streams emit error events for write failures (epipe when child exits, econnreset on unusual builds). there's a drain listener at line 586 handling backpressure, but no error listener on stdin, stdout, or stderr. child.once("error") at line 603 catches spawn errors only, not stream errors. an unhandled error on any of these streams becomes uncaughtException and kills the wrapper mid-session.

same applies to child.stdout/stderr — both lack error listeners. for a feature designed to keep rotation alive across long codex app-server sessions, this is a serious reliability gap, especially on windows where pipe teardown races are common.

add child.stdin?.on("error", onChildStdinError), mirror for stdout/stderr (log via console.error, close proxy), register in cleanupProtocolProxy closure, and add vitest regression that kills the mocked child mid-stream and asserts the wrapper exits cleanly without throwing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/codex.js` around lines 537 - 601, The protocol-proxy path is missing
stream "error" handlers which can crash the wrapper; add error listeners for
child.stdin, child.stdout, and child.stderr (e.g., onChildStdinError,
onChildStdoutError, onChildStderrError) that log the error (console.error) and
call closeChildStdin()/protocolProxy.flushOutput()/cleanupProtocolProxy() as
appropriate, register these listeners where process.stdin.on/... and
child.stdout/on(...) are currently attached, and remove them inside
cleanupProtocolProxy so they don't leak; also add a vitest regression that
simulates killing the child mid-stream and asserts the wrapper exits cleanly (no
uncaught exceptions) to cover the new handlers.
♻️ Duplicate comments (6)
test/codex-bin-wrapper.test.ts (1)

169-226: ⚠️ Potential issue | 🟠 Major

fixture still drifts from RuntimeRotationProxyStatus and negative-path is uncovered.

test/codex-bin-wrapper.test.ts:196-209 builds a status missing startedAt and streamsStarted, both declared on RuntimeRotationProxyStatus at lib/runtime-rotation-proxy.ts:31-53. today only totalRequests is read in scripts/codex.js:1714, but the app-helper tests at :921-927 already lean on other fields; as the shim grows to read streamsStarted the fixture will return undefined and silently pass.

 			"function buildStatus() {",
 			"  return {",
+			"    startedAt: readOptionalNumberEnv('CODEX_MULTI_AUTH_TEST_PROXY_STARTED_AT') ?? 0,",
 			"    totalRequests: readOptionalNumberEnv('CODEX_MULTI_AUTH_TEST_PROXY_REQUESTS') ?? 0,",
 			"    upstreamRequests: 0,",
 			"    retries: 0,",
 			"    rotations: readOptionalNumberEnv('CODEX_MULTI_AUTH_TEST_PROXY_ROTATIONS') ?? 0,",
+			"    streamsStarted: readOptionalNumberEnv('CODEX_MULTI_AUTH_TEST_PROXY_STREAMS_STARTED') ?? 0,",

separately: there is still no wrapper-surface test where startRuntimeRotationProxy rejects (module missing or throws). scripts/codex.js:1700-1722 has a fallback path ("runtime rotation proxy module is unavailable"); without a negative test you cannot catch a regression where codex exec dies instead of degrading. add a fixture variant that throws inside startRuntimeRotationProxy (or skips writing dist/lib/runtime-rotation-proxy.js) and assert exit status + non-proxy forwarding.

as per coding guidelines for test/**: "demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior" — the proxy-start failure path is a concurrency-adjacent reliability gap worth locking in.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/codex-bin-wrapper.test.ts` around lines 169 - 226, The test fixture
created by createRuntimeRotationProxyFixtureModule returns a status missing
startedAt and streamsStarted (fields defined on RuntimeRotationProxyStatus) and
lacks a negative test for startRuntimeRotationProxy failure; update the fixture
module written in test/codex-bin-wrapper.test.ts so buildStatus() includes
startedAt and streamsStarted (populate from CODEX_MULTI_AUTH_TEST_PROXY_* env
vars or sensible defaults) to match RuntimeRotationProxyStatus, and add a new
test variant that simulates the module failing to start (either by writing a
module whose startRuntimeRotationProxy throws or by omitting
dist/lib/runtime-rotation-proxy.js) and assert codex exec falls back to the
"runtime rotation proxy module is unavailable" path (check exit status and
non-proxy forwarding behavior exercised in scripts/codex.js handling around
startRuntimeRotationProxy).
test/runtime-rotation-proxy.test.ts (1)

139-380: ⚠️ Potential issue | 🔴 Critical

still missing concurrency, token-refresh-race, and x-api-key regression coverage.

re-reading the file end-to-end against the past review: none of the four gaps are closed.

  1. x-api-key auth unreachable in tests. lib/runtime-rotation-proxy.ts:148-154 has a distinct headers.get("x-api-key") === clientApiKey branch. the only auth test at test/runtime-rotation-proxy.test.ts:139-174 sends Bearer at line 168; the x-api-key branch is untested.
  2. no concurrent inbound requests. Promise.all does not appear in the suite. lib/runtime-rotation-proxy.ts runs the handler concurrently and mutates shared status counters + calls consumeToken/refundToken/markRateLimitedWithReason in parallel. fire N requests simultaneously, assert counters and account picks stay consistent (no negative budgets, no duplicate picks).
  3. no token-refresh dedup test. every account uses expiresAt: now + 3_600_000 at test/runtime-rotation-proxy.test.ts:46, so ensureFreshAccessTokenqueuedRefresh dedup contract never fires. set expiresAt < now() and fire two concurrent requests; assert saveAccountsMock is called exactly once and both responses succeed.
  4. pool-exhaustion assertion still loose. :350 only checks retry_after_ms > 0. pin it to accountManager.getMinWaitTimeForFamily(family, model) so regressions in writePoolExhausted surface.

per coding guidelines for test/**: "demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior." the first two categories are still uncovered on the proxy's hottest paths.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/runtime-rotation-proxy.test.ts` around lines 139 - 380, Add tests
exercising the missing branches and races: (1) add a case that authenticates
using x-api-key (send header "x-api-key" equal to clientApiKey) to hit the
headers.get("x-api-key") === clientApiKey branch when calling
startRuntimeRotationProxy/postResponses; (2) create a concurrency test that
fires many simultaneous postResponses via Promise.all against
startProxy/startRuntimeRotationProxy and assert AccountManager state (no
negative budgets, unique account picks, and proxy.getStatus() counters) and that
consumeToken/refundToken paths behave deterministically; (3) add a
token-refresh-race test by using createStorage or AccountManager to set an
account's expiresAt < now and then issue two concurrent requests, asserting
ensureFreshAccessToken/queuedRefresh dedup behavior by checking the accounts
persistence mock (saveAccountsMock) was called exactly once and both requests
succeed; (4) tighten the pool-exhaustion assertion by comparing the returned
payload.error.retry_after_ms to accountManager.getMinWaitTimeForFamily(family,
model) (rather than just > 0) so writePoolExhausted regressions are caught.
scripts/codex.js (3)

1316-1362: ⚠️ Potential issue | 🔴 Critical

array-of-tables table regex is still broken — model_provider ends up nested under the last [[…]] block.

scripts/codex.js:1326 (removeRuntimeRotationProviderBlock) and scripts/codex.js:1344 (rewriteTopLevelModelProvider) both still use /^\s*\[[^\]]+\]\s*$/, which does not match [[profiles.foo]] or any other TOML array-of-tables header. if a user's config.toml opens with an array-of-tables (common in custom profile configs), rewriteTopLevelModelProvider skips the header detection, falls through to the trailing append branch at line 1357-1359, and silently nests model_provider = "codex-multi-auth-runtime-proxy" under the last [[…]] block. codex ignores it and the proxy never activates — rotation looks enabled in codex auth rotation status but traffic still hits the real provider.

fix both regexes to /^\s*\[\[?[^\]]+\]\]?\s*$/ and land a regression fixture with a leading [[profiles.foo]] header against rewriteConfigTomlForRuntimeRotationProxy. as per coding guidelines (lib/**: "focus on auth rotation …"), this needs a cited vitest regression before merge.

🛠 proposed fix
-		if (skipping && /^\s*\[[^\]]+\]\s*$/.test(line)) {
+		if (skipping && /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line)) {
 			skipping = false;
 		}
-		const isTable = /^\s*\[[^\]]+\]\s*$/.test(line);
+		const isTable = /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/codex.js` around lines 1316 - 1362, The TOML table regex in
removeRuntimeRotationProviderBlock and rewriteTopLevelModelProvider is too
narrow and misses array-of-tables headers like [[profiles.foo]]; update the
regex in both functions (removeRuntimeRotationProviderBlock,
rewriteTopLevelModelProvider) from /^\s*\[[^\]]+\]\s*$/ to
/^\s*\[\[?[^\]]+\]\]?\s*$/ so table headers and array-of-tables are detected
properly, and add a vitest regression test for
rewriteConfigTomlForRuntimeRotationProxy that uses a config starting with a
leading [[profiles.foo]] header to assert model_provider gets placed at top
level (not nested).

636-642: ⚠️ Potential issue | 🔴 Critical

proxy unavailability still hard-exits 1 despite being marked addressed.

the past "✅ Addressed in commit c2c1ec6" marker is misleading — scripts/codex.js:640-642 still early-returns exit 1 when createRuntimeRotationProxyContextIfEnabled yields null, and scripts/codex.js:1900-1904 + scripts/codex.js:1916-1927 still produce null on dist-missing / port contention / startup throws. there is no CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY_STRICT opt-in, no fallback to baseContext, and no vitest regression that stubs startRuntimeRotationProxy to throw and asserts the wrapper still forwards to the real codex.

the operational consequence is unchanged: a partial npm run build, transient fs hiccup, or port collision flips rotation from an opt-in perf feature into "codex refuses to run." please either actually implement the fallback-by-default behavior requested earlier, or update the resolution marker and document the strict-by-design contract in docs/reference/commands.md:154 so operators know to expect codex <anything> to die when rotation is enabled and dist is half-built.

Also applies to: 1887-1951

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/codex.js` around lines 636 - 642, When
createRuntimeRotationProxyContextIfEnabled(...) returns null the caller
currently returns exit code 1; change this to fall back to the original
baseContext unless a new opt-in strict mode is set (env var
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY_STRICT). Specifically, update the caller
around runtimeProxyContext to: if runtimeProxyContext is null and
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY_STRICT is truthy, log an error and exit
1; otherwise log a warning and continue using baseContext; alternatively move
this logic inside createRuntimeRotationProxyContextIfEnabled or have
startRuntimeRotationProxy surface errors rather than causing null
silently—ensure all callers (including the one invoking
startRuntimeRotationProxy) respect the same strict opt-in and that tests are
added to simulate startRuntimeRotationProxy throwing and assert the wrapper
falls back to baseContext.

1416-1441: ⚠️ Potential issue | 🔴 Critical

shadow-home sync-back still per-file — two wrapper sessions can publish a mixed auth bundle.

scripts/codex.js:1416-1441 still iterates SHADOW_HOME_STATE_FILES and calls syncShadowHomeStateFile independently per name. two overlapping sessions (very plausible for codex app + codex exec from the same operator) can interleave renames, leaving the real CODEX_HOME with auth.json from one session and accounts.json / .codex-global-state.json from another. the PR summary itself calls this out under "Concurrent shadow-home sync race in scripts/codex.js affecting concurrent codex app sessions."

per-file snapshot-equality checks don't close the window — they only reject the rename when the destination drifted; they don't make the three writes atomic as a bundle. acquire a cross-process lease file inside originalCodexHome (or a stable multi-auth dir) before the loop, capture + write all three in the held lease, then release. regression test must spin up two concurrent syncShadowHomeStateBack invocations against shared tmp dirs and assert the final bundle matches one source, not a mix.

based on learnings: "Cross-process refresh coordination relies on lease/state files; avoid manually editing those files while the CLI is running to prevent synchronization conflicts" — apply the same pattern here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/codex.js` around lines 1416 - 1441, The per-file sync in
syncShadowHomeStateBack allows interleaved sessions to produce mixed auth files;
fix by acquiring a cross-process lease (e.g., a lock file inside
originalCodexHome or a stable multi-auth dir) before iterating
SHADOW_HOME_STATE_FILES, then while holding the lease capture the current
snapshots, validate originalSnapshot equality, and write all three files as an
atomic bundle (replace per-file calls to syncShadowHomeStateFile with a single
bundled write + tightenShadowHomePermissions on the bundle), release the lease
after completion; add a regression test that spawns two concurrent
syncShadowHomeStateBack calls against shared temp dirs and asserts the final
CODEX_HOME bundle is entirely from one source (no mixed files).
lib/codex-manager/commands/rotation.ts (1)

154-170: ⚠️ Potential issue | 🟠 Major

isProcessAlive(pid) still lies on windows when the helper pid gets recycled.

lib/codex-manager/commands/rotation.ts:157-160 still gates liveness on process.kill(pid, 0) plus the state string. on windows, pid reuse is routine — once the helper exits, a reused pid against a stale runtime-rotation-app-helper.json (state="running", old updatedAt) keeps printing Codex app helper: running, pid=… during an incident. operators running codex auth rotation status mid-incident get the exact wrong answer.

gate on status freshness: reject the status when status.updatedAt is older than, say, max(2 * pollInterval, 5_000) ms, and/or when status.idleExpiresAt is in the past. the helper already updates at setInterval(…, min(1000, idle/2)) in scripts/codex.js:1725-1735, so a 5s staleness threshold is safe. stronger fix: persist an instance token (random bytes or boot id) in the status file and write the live process's token into a pid-adjacent file so you can verify the pid belongs to this helper.

regression test must write a runtime-rotation-app-helper.json with a stale updatedAt and the current process.pid (the vitest runner), then assert formatAppRuntimeHelperStatus returns "Codex app helper: not running". as per coding guidelines (lib/**: "focus on auth rotation, windows filesystem IO, and concurrency"), this regression is required.

🪟 freshness gate sketch
-	const alive = isProcessAlive(status.pid);
-	if (!alive || status.state === "stopped" || status.state === "idle-timeout") {
+	const stalenessMs = 5_000;
+	const isFresh = status.updatedAt !== null && now - status.updatedAt <= stalenessMs;
+	const alive = isFresh && isProcessAlive(status.pid);
+	if (!alive || status.state === "stopped" || status.state === "idle-timeout") {
 		return "Codex app helper: not running";
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/codex-manager/commands/rotation.ts` around lines 154 - 170,
formatAppRuntimeHelperStatus currently trusts isProcessAlive(status.pid) and
state but can misreport on Windows due to PID reuse; change it to reject a
status if status.updatedAt is older than a freshnessThreshold (use max(2 *
pollInterval, 5000) ms, or at least 5000ms when pollInterval isn't accessible)
or if status.idleExpiresAt is in the past before reporting "running"; update
formatAppRuntimeHelperStatus (and readAppRuntimeHelperStatus usage) to check
status.updatedAt and status.idleExpiresAt first, then fall back to
isProcessAlive(status.pid) and state; add a regression test that writes a
runtime-rotation-app-helper.json with a stale updatedAt but the current
process.pid (the test runner) and assert formatAppRuntimeHelperStatus returns
"Codex app helper: not running".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/codex-manager/help.ts`:
- Around line 23-26: The help text currently shows "codex auth rotation
<enable|disable|status>" but omits the runtime proxy management subcommands;
update the help entry that contains the literal string "codex auth rotation
<enable|disable|status>" in help.ts to list all supported subcommands — enable,
disable, status, bind-app, and unbind-app — so the displayed line becomes "codex
auth rotation <enable|disable|status|bind-app|unbind-app>" (or equivalent
human-readable form) and ensure any surrounding documentation strings in the
same file reflect those five options.

In `@lib/logger.ts`:
- Line 49: Add a regression test to the "sanitizeValue edge cases" suite in
test/logger.test.ts that asserts sanitizeValue masks keys related to
experimental bearer tokens: include a case for the snake_case key
"experimental_bearer_token" and additional cases for normalized variations
"experimentalBearerToken" and "experimental-bearer-token"; ensure the assertions
verify the returned value is masked (not the raw token) and reference the
sanitizeValue function from lib/logger.ts (which recently added
"experimentalbearertoken" to the sensitive keys) and the source usage in
lib/runtime/app-bind.ts so future changes won't reintroduce leaks.

In `@lib/runtime-rotation-proxy.ts`:
- Line 49: The status object currently persists raw email via identity.email
into lastAccountEmail and runtime-observability state; change the code in
getStatus()/status construction (where identity.email is written, and the
runtime-observability mutation) to remove or redact the email (e.g., delete
lastAccountEmail or replace with a deterministic hash/placeholder) so no raw
email is stored or returned by getStatus(); keep index, label, accountId intact
and, if an email is needed for on-demand display, resolve it at read-time from
the in-memory account manager instead of persisting. Update tests: add a
regression in test/runtime-rotation-proxy.test.ts asserting getStatus() does not
include raw email (no '@') and add a test in test/app-bind.test.ts that reads
the persisted status file and asserts the snapshot contains no '@' characters or
raw email entries. Ensure you update references around symbols:
lastAccountEmail, getStatus(), identity.email, and runtime-observability.

In `@lib/runtime/app-bind.ts`:
- Around line 272-286: Wrap every raw fs write in the bind/unbind hot paths to
use the existing withFileOperationRetry helper: replace direct writeFile calls
that create the startup artifacts, plist/.cmd startup scripts, backup, state
file, config.toml rewrite and restore-path writes in lib/runtime/app-bind.ts
with await withFileOperationRetry(() => writeFile(...)) so transient
EBUSY/EPERM/EACCES are retried; update both bindCodexAppRuntimeRotation and
unbindCodexAppRuntimeRotation code paths to call withFileOperationRetry for the
writes that currently run raw (startup artifact writes, backup, state/config
writes, restore writes and fallback restore). Add Vitest cases in
test/app-bind.test.ts that stub writeFile to throw { code: "EBUSY" } for the
first N-1 attempts and assert bindCodexAppRuntimeRotation and
unbindCodexAppRuntimeRotation succeed, and a case where EBUSY persists past
FILE_RETRY_MAX_ATTEMPTS to assert the error is propagated.
- Around line 654-694: The code writes a config/state with baseUrl using port 0
when maybeStartRouter returns false; fix by resolving a valid non-zero port
before writing boundConfig/state: after calling maybeStartRouter(state, options)
if startedRouter is false, call waitForRouterStatus(state.statusPath) (or read
existingState/status) and use readPortFromBaseUrl(router?.baseUrl ?? null, port)
to update port and baseUrl (falling back to formatBaseUrl only if port is
non-zero), recompute boundConfig = rewriteConfigTomlForAppBind(content, baseUrl,
clientApiKey) and update state.boundConfigHash/port/baseUrl/updatedAt before
writing paths.configPath and paths.statePath; also add a test in
test/app-bind.test.ts to cover spawnDetached: false ensuring bind refuses or
uses existing state's port.

In `@scripts/codex-app-router.js`:
- Around line 110-117: The current port validation accepts non-integers and
out-of-range values (e.g., 3.14 or 70000); update the check around the computed
port variable to require an integer in the valid TCP port range: use
Number.isInteger(port) && port >= 0 && port <= 65535 (instead of
Number.isFinite/port < 0) and throw a clear error (referencing port, stateRecord
and args.port) so tampered state files are rejected before calling
startRuntimeRotationProxy/server.listen.
- Around line 141-157: The cleanup handler can be invoked multiple times by
rapid signals; make it idempotent by adding a single-shot guard (e.g., a boolean
flag like isCleaning) checked at the top of cleanup and set immediately to true
so subsequent invocations return early; inside cleanup still
clearInterval(timer), await proxyServer?.close() but only if not already closed
or if the guard indicates first run (catch and swallow or log close errors to
avoid unhandled rejection), and always call writeCurrentStatus(state) in the
finally; keep process.once registrations but rely on the single-shot guard to
prevent double-close and racing writeCurrentStatus calls.

In `@test/app-bind.test.ts`:
- Around line 231-254: The test currently passes a hardcoded relative script
path "scripts/codex-app-router.js" to spawnSync which breaks when the cwd is not
the repo root; update the spawnSync call to resolve the script path relative to
the test file (e.g. derive an absolute path from the test module directory using
path.resolve or fileURLToPath(import.meta.url) and use that instead of the raw
string) so spawnSync can always find the script, and adjust the assertions on
the spawn result: replace expect(result.status).toBe(1) with
expect(result.status).not.toBe(0) and add expect(result.error).toBeUndefined()
to ensure the process actually executed rather than spawn failing; keep
references to spawnSync, process.execPath, createTempRoot, statusPath,
result.status and result.error when making these changes.

In `@test/codex-manager-rotation-command.test.ts`:
- Around line 159-243: Add four deterministic vitest regression cases to
test/codex-manager-rotation-command.test.ts: (1) a concurrent-run test that
calls runRotationCommand(["enable"]) twice in overlapping async operations and
asserts savePluginConfigMock and bindCodexAppMock were called in a consistent
order and expected counts; (2) a stale-helper-status test that writes a
runtime-rotation-app-helper.json fixture with state:"running" and a reused pid
(simulate a non-existent process) then runs runRotationCommand(["status"]) and
expects the status output to include "Codex app helper: not running"; (3) a
Windows-EBUSY regression that simulates EBUSY/429 from savePluginConfigMock or
bindCodexAppMock (mock them to throw EBUSY-like errors on first attempt and
succeed on retry) and asserts the command recovers and saves/binds as expected;
and (4) an invalid-env-value test that sets
process.env.CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY="whatever", runs
runRotationCommand(["status"]) and asserts the output contains "Env override:
invalid (whatever)"; use existing helpers (createDeps, createStorage,
setStoragePathMock, infos, errors, savePluginConfigMock, bindCodexAppMock,
unbindCodexAppMock) and runRotationCommand to locate insertion points and keep
tests deterministic by seeding time and storage where needed.

In `@vitest.config.ts`:
- Around line 16-26: The shebang stripper currently looks for any "/scripts/"
segment in module ids (plugin name 'strip-script-shebangs-for-vitest',
transform) which can match dependencies; change the check to only match files
under this repo's scripts directory by computing the repo scripts root (e.g.,
path.join(process.cwd(), 'scripts') or process.cwd() + path.sep + 'scripts') and
require id.startsWith(thatRoot + path.sep) (or otherwise ensure the id is inside
the repo root scripts folder) before stripping the shebang; keep the other
checks (code.startsWith('#!')) intact.

---

Outside diff comments:
In `@scripts/codex.js`:
- Around line 537-601: The protocol-proxy path is missing stream "error"
handlers which can crash the wrapper; add error listeners for child.stdin,
child.stdout, and child.stderr (e.g., onChildStdinError, onChildStdoutError,
onChildStderrError) that log the error (console.error) and call
closeChildStdin()/protocolProxy.flushOutput()/cleanupProtocolProxy() as
appropriate, register these listeners where process.stdin.on/... and
child.stdout/on(...) are currently attached, and remove them inside
cleanupProtocolProxy so they don't leak; also add a vitest regression that
simulates killing the child mid-stream and asserts the wrapper exits cleanly (no
uncaught exceptions) to cover the new handlers.

---

Duplicate comments:
In `@lib/codex-manager/commands/rotation.ts`:
- Around line 154-170: formatAppRuntimeHelperStatus currently trusts
isProcessAlive(status.pid) and state but can misreport on Windows due to PID
reuse; change it to reject a status if status.updatedAt is older than a
freshnessThreshold (use max(2 * pollInterval, 5000) ms, or at least 5000ms when
pollInterval isn't accessible) or if status.idleExpiresAt is in the past before
reporting "running"; update formatAppRuntimeHelperStatus (and
readAppRuntimeHelperStatus usage) to check status.updatedAt and
status.idleExpiresAt first, then fall back to isProcessAlive(status.pid) and
state; add a regression test that writes a runtime-rotation-app-helper.json with
a stale updatedAt but the current process.pid (the test runner) and assert
formatAppRuntimeHelperStatus returns "Codex app helper: not running".

In `@scripts/codex.js`:
- Around line 1316-1362: The TOML table regex in
removeRuntimeRotationProviderBlock and rewriteTopLevelModelProvider is too
narrow and misses array-of-tables headers like [[profiles.foo]]; update the
regex in both functions (removeRuntimeRotationProviderBlock,
rewriteTopLevelModelProvider) from /^\s*\[[^\]]+\]\s*$/ to
/^\s*\[\[?[^\]]+\]\]?\s*$/ so table headers and array-of-tables are detected
properly, and add a vitest regression test for
rewriteConfigTomlForRuntimeRotationProxy that uses a config starting with a
leading [[profiles.foo]] header to assert model_provider gets placed at top
level (not nested).
- Around line 636-642: When createRuntimeRotationProxyContextIfEnabled(...)
returns null the caller currently returns exit code 1; change this to fall back
to the original baseContext unless a new opt-in strict mode is set (env var
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY_STRICT). Specifically, update the caller
around runtimeProxyContext to: if runtimeProxyContext is null and
CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY_STRICT is truthy, log an error and exit
1; otherwise log a warning and continue using baseContext; alternatively move
this logic inside createRuntimeRotationProxyContextIfEnabled or have
startRuntimeRotationProxy surface errors rather than causing null
silently—ensure all callers (including the one invoking
startRuntimeRotationProxy) respect the same strict opt-in and that tests are
added to simulate startRuntimeRotationProxy throwing and assert the wrapper
falls back to baseContext.
- Around line 1416-1441: The per-file sync in syncShadowHomeStateBack allows
interleaved sessions to produce mixed auth files; fix by acquiring a
cross-process lease (e.g., a lock file inside originalCodexHome or a stable
multi-auth dir) before iterating SHADOW_HOME_STATE_FILES, then while holding the
lease capture the current snapshots, validate originalSnapshot equality, and
write all three files as an atomic bundle (replace per-file calls to
syncShadowHomeStateFile with a single bundled write +
tightenShadowHomePermissions on the bundle), release the lease after completion;
add a regression test that spawns two concurrent syncShadowHomeStateBack calls
against shared temp dirs and asserts the final CODEX_HOME bundle is entirely
from one source (no mixed files).

In `@test/codex-bin-wrapper.test.ts`:
- Around line 169-226: The test fixture created by
createRuntimeRotationProxyFixtureModule returns a status missing startedAt and
streamsStarted (fields defined on RuntimeRotationProxyStatus) and lacks a
negative test for startRuntimeRotationProxy failure; update the fixture module
written in test/codex-bin-wrapper.test.ts so buildStatus() includes startedAt
and streamsStarted (populate from CODEX_MULTI_AUTH_TEST_PROXY_* env vars or
sensible defaults) to match RuntimeRotationProxyStatus, and add a new test
variant that simulates the module failing to start (either by writing a module
whose startRuntimeRotationProxy throws or by omitting
dist/lib/runtime-rotation-proxy.js) and assert codex exec falls back to the
"runtime rotation proxy module is unavailable" path (check exit status and
non-proxy forwarding behavior exercised in scripts/codex.js handling around
startRuntimeRotationProxy).

In `@test/runtime-rotation-proxy.test.ts`:
- Around line 139-380: Add tests exercising the missing branches and races: (1)
add a case that authenticates using x-api-key (send header "x-api-key" equal to
clientApiKey) to hit the headers.get("x-api-key") === clientApiKey branch when
calling startRuntimeRotationProxy/postResponses; (2) create a concurrency test
that fires many simultaneous postResponses via Promise.all against
startProxy/startRuntimeRotationProxy and assert AccountManager state (no
negative budgets, unique account picks, and proxy.getStatus() counters) and that
consumeToken/refundToken paths behave deterministically; (3) add a
token-refresh-race test by using createStorage or AccountManager to set an
account's expiresAt < now and then issue two concurrent requests, asserting
ensureFreshAccessToken/queuedRefresh dedup behavior by checking the accounts
persistence mock (saveAccountsMock) was called exactly once and both requests
succeed; (4) tighten the pool-exhaustion assertion by comparing the returned
payload.error.retry_after_ms to accountManager.getMinWaitTimeForFamily(family,
model) (rather than just > 0) so writePoolExhausted regressions are caught.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 00eac433-619b-4937-9aba-a10e89965094

📥 Commits

Reviewing files that changed from the base of the PR and between dffbe2e and 3ec7e6e.

📒 Files selected for processing (19)
  • docs/configuration.md
  • docs/development/CONFIG_FIELDS.md
  • docs/features.md
  • docs/reference/commands.md
  • lib/codex-manager/commands/rotation.ts
  • lib/codex-manager/help.ts
  • lib/logger.ts
  • lib/runtime-rotation-proxy.ts
  • lib/runtime/app-bind.ts
  • scripts/codex-app-launcher.js
  • scripts/codex-app-router.js
  • scripts/codex.js
  • scripts/postinstall.js
  • test/app-bind.test.ts
  • test/codex-bin-wrapper.test.ts
  • test/codex-manager-rotation-command.test.ts
  • test/install-codex-auth.test.ts
  • test/runtime-rotation-proxy.test.ts
  • vitest.config.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (3)
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/codex-manager/help.ts
  • lib/logger.ts
  • lib/codex-manager/commands/rotation.ts
  • lib/runtime/app-bind.ts
  • lib/runtime-rotation-proxy.ts
docs/**

⚙️ CodeRabbit configuration file

keep README, SECURITY, and docs consistent with actual CLI flags and workflows. whenever behavior changes, require updated upgrade notes and mention new npm scripts.

Files:

  • docs/features.md
  • docs/development/CONFIG_FIELDS.md
  • docs/configuration.md
  • docs/reference/commands.md
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/codex-manager-rotation-command.test.ts
  • test/app-bind.test.ts
  • test/install-codex-auth.test.ts
  • test/codex-bin-wrapper.test.ts
  • test/runtime-rotation-proxy.test.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:23.023Z
Learning: Implement account health-aware selection and automatic failover in multi-account OAuth pool management
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:23.023Z
Learning: Use bounded outbound request budgets to prevent a single prompt from consuming the entire account pool
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:23.023Z
Learning: Implement short cooldown periods instead of aggressive rotation when repeated cross-account 5xx bursts occur
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:23.023Z
Learning: Stagger proactive refresh operations to reduce background refresh bursts
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:23.023Z
Learning: Disable whole-pool replay by default when every account is rate-limited
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:23.023Z
Learning: Validate configuration after changes using `codex auth status`, `codex auth check`, and `codex auth forecast --live`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:23.023Z
Learning: Design experimental settings features to be non-destructive by default: sync previews before apply, preserve destination-only accounts, and fail safely on backup filename collisions
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:35.682Z
Learning: Runtime configuration is resolved from unified settings at `~/.codex/multi-auth/settings.json`, optional override files via `CODEX_MULTI_AUTH_CONFIG_PATH`, and environment variables in a defined precedence order
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:35.682Z
Learning: Use stable environment overrides (`CODEX_MULTI_AUTH_DIR`, `CODEX_MULTI_AUTH_CONFIG_PATH`, `CODEX_MODE`, `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY`, `CODEX_TUI_V2`, `CODEX_TUI_COLOR_PROFILE`, `CODEX_TUI_GLYPHS`, `CODEX_AUTH_FETCH_TIMEOUT_MS`, `CODEX_AUTH_STREAM_STALL_TIMEOUT_MS`) for day-to-day workflows in most environments
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:35.682Z
Learning: Advanced and internal environment overrides (`CODEX_MULTI_AUTH_SYNC_CODEX_CLI`, `CODEX_MULTI_AUTH_REAL_CODEX_BIN`, `CODEX_MULTI_AUTH_BYPASS`, etc.) should only be used when debugging, controlled benchmarking, or in maintainer workflows
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:35.682Z
Learning: Use `codex auth status`, `codex auth list`, `codex auth check`, and `codex auth forecast --live` commands to validate effective configuration
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:55.586Z
Learning: When enabling `backgroundResponses=true`, ensure that requests become stateful (`store=true`) instead of stateless, and test one known `background: true` request end-to-end before rolling out to shared automation
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:55.586Z
Learning: Environment variable `CODEX_MULTI_AUTH_DIR` should be used to specify a custom root directory for settings, accounts, cache, and logs when needed
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:55.586Z
Learning: Environment variable `CODEX_MULTI_AUTH_CONFIG_PATH` can be used to specify an alternate config file path for input
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:55.586Z
Learning: Environment variable `CODEX_MODE` should be used to toggle Codex mode at runtime
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:55.586Z
Learning: Environment variable `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY` should be set to `1` or `true` to enable opt-in localhost Responses proxy for forwarded Codex sessions, or `0` or `false` to disable
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:10:55.586Z
Learning: Cross-process refresh coordination relies on lease/state files; avoid manually editing those files while the CLI is running to prevent synchronization conflicts
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:08.525Z
Learning: Store all multi-auth account state and configuration under the canonical local data root at `~/.codex/multi-auth`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:08.525Z
Learning: Implement project-scoped account pools to allow repo-specific account state separation when needed
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:08.525Z
Learning: Support linked-worktree identity sharing to allow the same repository to share account state across worktrees
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:08.525Z
Learning: Persist quota cache to enable faster forecast and dashboard visibility between runs
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:08.525Z
Learning: Implement safe repair workflow via `codex auth fix` command to detect and repair known local storage inconsistencies
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:08.525Z
Learning: Provide diagnostics and optional repair capability via `codex auth doctor` command for inspecting and fixing common failures
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:08.525Z
Learning: Implement backup and WAL (Write-Ahead Logging) recovery in the storage runtime to ensure safer persistence when local writes are interrupted or partially applied
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:08.525Z
Learning: Support browser-first OAuth flow via `codex auth login` with manual fallback options (`--manual`, `--no-browser`, `CODEX_AUTH_NO_BROWSER=1`) for browser-restricted shells
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:08.525Z
Learning: Accept the full callback URL on stdin for manual/non-TTY login to support automation and host-managed shells without requiring local browser handoff
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:08.525Z
Learning: Implement optional plugin-host runtime integration for request transformation, token refresh, retry handling, and capability-aware account selection
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Use `codex auth ...` as the canonical command family for authentication operations
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Support compatibility aliases for auth commands: `codex multi auth ...`, `codex multi-auth ...`, and `codex multiauth ...`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Primary auth commands include: `codex auth login` (interactive dashboard), `codex auth status` (runtime/account summary), and `codex auth check` (account health check)
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Support daily-use commands: `codex auth list`, `codex auth switch <index>`, `codex auth forecast`, and `codex auth best`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Implement repair commands: `codex auth verify-flagged`, `codex auth verify`, `codex auth fix`, `codex auth doctor`, `codex auth config explain`, `codex auth init-config`, and `codex auth debug bundle`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Support `--json` flag for machine-readable output on: verify-flagged, verify, why-selected, best, forecast, report, fix, doctor, config explain, and debug bundle
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Support `--dry-run` flag on verify-flagged, verify (with `--flagged`/`--all`), fix, and doctor commands to preview changes without writing to storage
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Use `--live` flag on best, forecast, report, and fix commands to enable live probe before decisions or output
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Support `--explain` flag on forecast and report commands to include reasoning details in both text and JSON output
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: The `codex auth why-selected` command should return exit code 0 when an account is selected, and exit code 1 when no account can be selected
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: JSON output for `codex auth why-selected` must include: command, mode, ok, availableCount, totalCount, quotaKey, config, selected (with index, email, accountId, health, score, etc.), candidates array, and runtimeSnapshot only with `--last` flag
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: The `codex auth rotation` command must support: enable, disable, status, bind-app, and unbind-app subcommands to manage runtime Responses proxy
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: When runtime rotation is enabled, create a temporary shadow config.toml with custom provider named `codex-multi-auth-runtime-proxy` and start a 127.0.0.1 proxy on a random port
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Support `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=1` environment variable to enable the proxy for the current process without changing persistent settings
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: The packaged desktop app bind must back up the real Codex config.toml, write the custom provider to real Codex home, start a localhost-only router, and install a user login startup entry
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: In non-TTY or host-managed sessions, auth flows must degrade to deterministic text behavior instead of interactive UI
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Dashboard hotkey Up/Down must move selection, Enter must select/open, 1-9 must quick switch accounts, / must search, ? must toggle help, Q must go back
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Account Details hotkeys: S (set current), R (refresh), E (enable/disable), D (delete), Q (back)
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Settings screen hotkeys must be panel-specific with different bindings for Account List View, Summary Line, Menu Behavior, Color Theme, and Backend Controls
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: In non-TTY/manual shells, `codex auth login` must default to add-account mode, skip the extra 'add another account' prompt, and auto-pick the default workspace selection
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Support `--manual` and `--no-browser` flags on `codex auth login` to skip browser launch and use manual callback flow
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: Respect `CODEX_AUTH_NO_BROWSER=1` environment variable to suppress browser launch for automation/headless sessions, but do not suppress on false-like values like 0 or false
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: The `codex` wrapper must route `codex auth ...` and compatibility aliases to the multi-auth runtime, and forward all other commands to official openai/codex CLI
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:28.456Z
Learning: `codex --version` must report the official openai/codex CLI version, while `codex-multi-auth --version` and `codex-multi-auth -v` must report the wrapper package version
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:44.290Z
Learning: Experimental shortcuts should include: `1` for sync preview, `2` for named backup export, `3` for toggle refresh guard, `[` or `-` to decrease refresh interval, `]` or `+` to increase refresh interval, `S` to save and return, `Q` to go back, and `A` to apply in sync review
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:44.290Z
Learning: Named backup export should prompt for filename, append `.json` when omitted, reject separators, traversal (`..`), `.rotate.`, `.tmp`, and `.wal` suffixes, and fail safely on collisions
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:44.290Z
Learning: Live account sync preview should always be shown before apply, blocked target states should not apply changes, destination active selection should be preserved, and destination-only accounts should be preserved by merge
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:44.290Z
Learning: Validation of configuration changes should be performed using `codex auth status`, `codex auth check`, and `codex auth forecast --live` commands
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:44.290Z
Learning: Stable environment overrides should support: `CODEX_MULTI_AUTH_DIR`, `CODEX_MULTI_AUTH_CONFIG_PATH`, `CODEX_MODE`, `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY`, `CODEX_MULTI_AUTH_APP_BIND_INSTALL`, `CODEX_TUI_V2`, `CODEX_TUI_COLOR_PROFILE`, `CODEX_TUI_GLYPHS`, `CODEX_AUTH_FETCH_TIMEOUT_MS`, `CODEX_AUTH_STREAM_STALL_TIMEOUT_MS`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:49.737Z
Learning: Run `npm run build` to validate the codebase before release
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:49.737Z
Learning: Run test suite with `npm test -- test/model-map.test.ts test/request-transformer.test.ts test/codex-bin-wrapper.test.ts test/config.test.ts test/capability-policy.test.ts test/fetch-helpers.test.ts test/index.test.ts` to validate release
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:49.737Z
Learning: Implement fallback behavior in wrapper to retry with `gpt-5.4` only after real upstream unsupported-model responses on ChatGPT-backed Codex surfaces
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:49.737Z
Learning: Handle both ChatGPT unsupported-model errors and upstream no-access variant (`does not exist or you do not have access`) in wrapper retry path
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-24T18:11:49.737Z
Learning: Keep `gpt-5.5` and `gpt-5.5-pro` as first-attempt models instead of silently down-mapping ahead of time
🔇 Additional comments (6)
lib/logger.ts (1)

86-111: lgtm — sanitization logic correctly handles the new sensitive key.

the normalization at lib/logger.ts:100 correctly maps experimental_bearer_tokenexperimentalbearertoken, and masking at lib/logger.ts:102 will prevent bearer token leakage when app-bind config objects (containing the field from lib/runtime/app-bind.ts:194) are logged during rotation enable/bind operations.

docs/features.md (1)

27-27: lgtm.

opt-in framing and the default-off note address the prior feedback; capability row is now self-explanatory without cross-referencing lib/config.ts.

docs/development/CONFIG_FIELDS.md (1)

47-47: lgtm.

default-false row and env-value hint line up with resolveBooleanSetting in lib/config.ts:780 and getCodexRuntimeRotationProxy at lib/config.ts:806. addresses the prior comment about operator ambiguity.

Also applies to: 204-204

test/install-codex-auth.test.ts (1)

205-437: lgtm on launcher + postinstall gate coverage.

windows redirected-desktop / onedrive test at test/install-codex-auth.test.ts:258, macos managed-wrapper test at :280, and the postinstall gate matrix at :360 lock down the new planner/gate logic nicely. deterministic, no live network.

scripts/codex-app-launcher.js (1)

96-108: four prior review points cleanly addressed — LGTM.

  • scripts/codex-app-launcher.js:96-108 now resolves onedrive-redirected desktops via OneDrive / OneDriveConsumer / OneDriveCommercial, with the js-side home/Desktop as fallback.
  • scripts/codex-app-launcher.js:255-256 additionally queries [Environment]::GetFolderPath('Desktop') inside the powershell plan, which is the authoritative shell folder lookup for redirected/enterprise images.
  • scripts/codex-app-launcher.js:300-301 gates backup writes on the AlreadyManaged sentinel (our $ManagedDescription or our exact target/args), so a pruned app-shortcuts.json no longer turns our wrapper into the "original" target on next re-route.
  • scripts/codex-app-launcher.js:380-390 uses quotePosixShell for the mac launcher script, killing the JSON.stringify-as-shell-quoter landmine.
  • scripts/codex-app-launcher.js:452-459 wraps JSON.parse with rich error context including a bounded tail of stdout — no more cryptic Unexpected token … on locked-down windows images.

Also applies to: 255-256, 300-301, 380-390, 452-459

lib/runtime-rotation-proxy.ts (1)

129-146: lgtm — dead ACCOUNT_ID re-set is gone.

the previously flagged no-op guard at lib/runtime-rotation-proxy.ts:129-146 has been removed; accountId is resolved once via resolveAccountId (see lib/runtime-rotation-proxy.ts:683) and set once here. clean.

Comment thread lib/codex-manager/help.ts
Comment thread lib/logger.ts
Comment thread lib/runtime-rotation-proxy.ts Outdated
Comment thread lib/runtime/app-bind.ts Outdated
Comment thread lib/runtime/app-bind.ts Outdated
Comment thread scripts/codex-app-router.js Outdated
Comment thread scripts/codex-app-router.js
Comment thread test/app-bind.test.ts
Comment thread test/codex-manager-rotation-command.test.ts
Comment thread vitest.config.ts
Comment thread lib/runtime/app-bind.ts
Comment thread scripts/codex.js
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
scripts/codex.js (1)

1183-1290: ⚠️ Potential issue | 🔴 Critical

windows hazard: copy-fallback path round-trips arbitrary ~/.codex files through utf-8 → binary corruption on sync-back

linkFileIntoShadowHome (scripts/codex.js:1268-1282) tries symlinkSync then linkSync, falling back silently to copyFileSync via mirrorFileIntoShadowHome (scripts/codex.js:1284-1290). on windows without developer mode, file symlinks throw EPERM; hardlinks fail across volumes. so on a typical windows install, every non-state file in ~/.codex ends up as an independent copy in the shadow home.

cleanup then walks the shadow with collectShadowHomeSyncFileNames (scripts/codex.js:1292-1312), captures both shadow and original via captureShadowHomeState which reads readFileSync(filePath, "utf8") (scripts/codex.js:1193), and on mismatch writes the utf-8 string back to the original via syncShadowHomeStateFile (scripts/codex.js:1399). for any binary file in ~/.codex (sqlite chat history, packed caches, lock files with non-text bytes, plugin artifacts, etc.) this is a lossy utf-8 round-trip — replacement chars on read, mojibake on write, file silently destroyed.

state files (auth.json, accounts.json, .codex-global-state.json) are json so the round-trip happens to be safe, but the discovery loop at scripts/codex.js:1331-1374 deliberately includes arbitrary non-state files (symlink-or-copy via mirrorFileIntoShadowHome) and rememberSyncFile(name) adds them to the sync-back set at line 1359 regardless. that's the bug surface.

fix via Buffer (no encoding) instead of utf-8:

  • update captureShadowHomeState to read without encoding (returns Buffer)
  • update shadowHomeStateMatches to use Buffer.equals() for byte comparison
  • syncShadowHomeStateFile already uses copyFileSync so it's encoding-agnostic

add a vitest fixture in test/shadow-home-mirror.test.ts that:

  • forces linkFileIntoShadowHome to return false (stub the link calls)
  • seeds ~/.codex/foo.bin with non-utf-8 bytes (Buffer.from([0xff, 0xfe, 0x00, 0x80]))
  • runs a roundtrip through createShadowHomeMirror + cleanup
  • asserts the original bytes survive byte-for-byte intact

current test at test/codex-bin-wrapper.test.ts uses only text files (state_5.sqlite written as "state\n" with utf-8), so it never exercises the copy-fallback corruption path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/codex.js` around lines 1183 - 1290, captureShadowHomeState currently
reads files with readFileSync(..., "utf8") which corrupts binary files when
mirror fallback copies are synced back; change captureShadowHomeState to read
raw Buffers (omit encoding) and update shadowHomeStateMatches to compare using
Buffer.equals(left.content, right.content) (ensuring you coerce/handle
null/unreadable cases); syncShadowHomeStateFile can remain as-is since it copies
bytes; add a vitest in test/shadow-home-mirror.test.ts that stubs
linkFileIntoShadowHome to return false, seeds a binary file
(Buffer.from([0xff,0xfe,0x00,0x80])) in the real ~/.codex fixture, runs
createShadowHomeMirror + cleanup, and asserts the original bytes survive
unchanged.
♻️ Duplicate comments (8)
lib/runtime-rotation-proxy.ts (6)

148-154: ⚠️ Potential issue | 🟡 Minor

isAuthorizedClient still uses === — swap to timingSafeEqual.

lib/runtime-rotation-proxy.ts:152-153 compares both the bearer token and x-api-key against clientApiKey with ===. proxy is bound to 127.0.0.1 so practical attack surface is small, but any local process on loopback can probe and the fix is one node:crypto import. defense-in-depth is cheap on an auth gate that protects token rotation state.

🛡️ drop-in replacement
+import { timingSafeEqual } from "node:crypto";
+
+function safeEqual(a: string, b: string): boolean {
+	const aBuf = Buffer.from(a);
+	const bBuf = Buffer.from(b);
+	if (aBuf.length !== bBuf.length) return false;
+	return timingSafeEqual(aBuf, bBuf);
+}
+
 function isAuthorizedClient(headers: Headers, clientApiKey: string | null): boolean {
 	if (!clientApiKey) return true;
 	const authorization = headers.get("authorization") ?? "";
 	const bearerMatch = authorization.match(/^Bearer\s+(.+)$/i);
-	if (bearerMatch?.[1]?.trim() === clientApiKey) return true;
-	return headers.get("x-api-key") === clientApiKey;
+	const bearer = bearerMatch?.[1]?.trim();
+	if (bearer && safeEqual(bearer, clientApiKey)) return true;
+	const apiKey = headers.get("x-api-key");
+	return typeof apiKey === "string" && safeEqual(apiKey, clientApiKey);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/runtime-rotation-proxy.ts` around lines 148 - 154, Replace the direct
string equality checks in isAuthorizedClient with a timing-safe comparison:
import timingSafeEqual from node:crypto, turn both operands into Buffer
instances (e.g., Buffer.from(token, 'utf8')) and only call timingSafeEqual when
the two buffers have identical lengths, falling back to false if lengths differ;
apply this to the bearer token comparison (bearerMatch?.[1]) and the x-api-key
header comparison while preserving the existing early-return behavior when
clientApiKey is null/undefined.

724-745: ⚠️ Potential issue | 🟠 Major

fetch + stream still have no timeouts; one stuck upstream burns the whole pool.

lib/runtime-rotation-proxy.ts:727-731 calls fetchImpl with no signal, and forwardStreamingResponse at lib/runtime-rotation-proxy.ts:583-590 reads in a bare while (true) loop with no stall watchdog. a hung codex responses endpoint will hold the account, the socket, and the client indefinitely — and because the per-request loop walks the full pool (lib/runtime-rotation-proxy.ts:664), a single stuck request can serially burn every account's quota. lib/config.ts already exposes getFetchTimeoutMs(pluginConfig) and getStreamStallTimeoutMs(pluginConfig) for exactly this purpose.

wire both:

  1. wrap fetchImpl with an AbortController fed by getFetchTimeoutMs. the existing catch at lib/runtime-rotation-proxy.ts:732-745 already handles abort by refunding + cooling down + rotating.
  2. add a stall timer in forwardStreamingResponse that resets per chunk and fires onStreamError() after getStreamStallTimeoutMs elapses with no data.
  3. add a vitest in test/runtime-rotation-proxy.test.ts that stubs fetchImpl to never resolve and asserts pool-exhausted fires within the timeout window.

As per coding guidelines for lib/**: "focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/runtime-rotation-proxy.ts` around lines 724 - 745, The fetch and
streaming paths in runtime-rotation-proxy lack timeouts: wrap the fetchImpl call
in the request loop (the block that assigns upstream in the rotation loop) with
an AbortController wired to getFetchTimeoutMs(pluginConfig) so fetchImpl
receives a signal and aborts on timeout (so the existing catch path that refunds
and cools down still applies), add a per-chunk stall watchdog in
forwardStreamingResponse that resets whenever a chunk is received and calls
onStreamError()/aborts the stream after getStreamStallTimeoutMs(pluginConfig)
elapses with no data, and add a vitest in test/runtime-rotation-proxy.test.ts
that stubs fetchImpl to never resolve and asserts the pool exhaustion/rotation
behavior triggers within the configured timeout; reference the symbols
fetchImpl, forwardStreamingResponse, getFetchTimeoutMs, getStreamStallTimeoutMs,
onStreamError, and accountManager to locate and modify the code.

220-226: ⚠️ Potential issue | 🟠 Major

readRequestBody still has no size cap — local DoS via loopback.

lib/runtime-rotation-proxy.ts:220-226 drains the entire request into memory with no ceiling. when clientApiKey is unset (the default at lib/runtime-rotation-proxy.ts:609-613), any local process that can reach loopback can post an unbounded body and exhaust heap before the body even hits the upstream selector. add a 64 MiB ceiling and respond 413 when exceeded.

🛡️ proposed cap
+const MAX_REQUEST_BODY_BYTES = 64 * 1024 * 1024;
+
 async function readRequestBody(req: IncomingMessage): Promise<Buffer> {
 	const chunks: Buffer[] = [];
+	let total = 0;
 	for await (const chunk of req) {
-		chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
+		const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
+		total += buf.length;
+		if (total > MAX_REQUEST_BODY_BYTES) {
+			throw Object.assign(new Error("request body too large"), {
+				statusCode: 413,
+			});
+		}
+		chunks.push(buf);
 	}
 	return Buffer.concat(chunks);
 }

then map the thrown statusCode in the outer catch at lib/runtime-rotation-proxy.ts:853-865 to a 413 response, and add a vitest in test/runtime-rotation-proxy.test.ts that posts an oversized body and asserts the 413 payload.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/runtime-rotation-proxy.ts` around lines 220 - 226, The readRequestBody
function currently reads the whole request into memory with no cap; modify
readRequestBody to enforce a 64 MiB (67_108_864 bytes) maximum while
accumulating chunks and throw an error (e.g., attach statusCode = 413) when the
limit is exceeded, so it stops consuming the request; update the outer error
handling logic in the runtime-rotation-proxy request flow (the catch that
converts thrown errors into responses) to translate that thrown statusCode into
an HTTP 413 response; add a vitest in test/runtime-rotation-proxy.test.ts that
POSTS a body larger than 64 MiB and asserts the proxy returns 413.

768-797: ⚠️ Potential issue | 🟡 Minor

401 still doesn't refund the consumed token, but 5xx does — bookkeeping divergence persists.

lib/runtime-rotation-proxy.ts:768-781 records failure + auth-failure cooldown but skips accountManager.refundToken(...). the 5xx branch immediately below at lib/runtime-rotation-proxy.ts:783-797 does refund. both branches represent "we did not serve the client", so token bookkeeping should match. either mirror the 5xx refund here or leave a one-line comment ("quota considered consumed by upstream when it returned 401") so a future contributor doesn't accidentally normalize one of the two.

same reminder applies to the 429 branch at lib/runtime-rotation-proxy.ts:747-766 — likely intentional that quota was genuinely used, but worth stating once.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/runtime-rotation-proxy.ts` around lines 768 - 797, The 401
(HTTP_STATUS.UNAUTHORIZED) branch in the rotation logic (within the block that
calls readErrorBody, accountManager.recordFailure,
accountManager.markAccountCoolingDown, accountManager.saveToDiskDebounced and
updates exhaustionReason/status.retries/status.rotations) does not call
accountManager.refundToken like the 5xx branch; either add a call to
accountManager.refundToken(refreshed.account, context.family, context.model) in
that 401 branch to keep token bookkeeping consistent, or if the intent is that
quota is consumed on 401, add a one-line comment above the 401 branch explaining
that choice (and likewise add a clarifying comment in the 429 branch) so future
contributors won’t normalize the behavior accidentally.

664-666: ⚠️ Potential issue | 🟡 Minor

per-request rotation budget still equals full pool — single prompt can drain every account.

lib/runtime-rotation-proxy.ts:664 still uses accountManager.getAccountCount() as the ceiling, so on a flapping upstream a single user prompt walks every account in sequence (each one paying network/fetch latency, cooldown, and refund overhead). combined with the missing fetch timeout above, this is the worst-case path. cap per-request rotation at e.g. min(getAccountCount(), 5) and emit pool_exhausted with reason: "budget" once that cap is hit, while keeping getAccountCount() as the absolute safety bound.

As per coding guidelines for **/*.{js,ts,mjs,cjs}: "implement bounded outbound request budget per prompt to prevent indefinite pool walking."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/runtime-rotation-proxy.ts` around lines 664 - 666, The per-request
rotation loop in runtime-rotation-proxy currently uses
accountManager.getAccountCount() as the dynamic ceiling, allowing a single
prompt to iterate the whole pool; change the loop ceiling to a bounded budget
(e.g., const rotationBudget = Math.min(accountManager.getAccountCount(), 5)) and
use attemptedIndexes.size < rotationBudget in place of Math.max(1,
accountManager.getAccountCount()); after hitting the budget emit the existing
pool_exhausted event with { reason: "budget" } and break out (while still
keeping accountManager.getAccountCount() as an absolute safety bound elsewhere),
and ensure this logic wraps where chooseAccount(...) and attemptedIndexes are
used to prevent full-pool walking per prompt.

39-52: ⚠️ Potential issue | 🟠 Major

lastAccountEmail is still on the public status surface — PII leak path remains open.

lib/runtime-rotation-proxy.ts:49 keeps the raw email on the public RuntimeRotationProxyStatus, and lib/runtime-rotation-proxy.ts:181 + lib/runtime-rotation-proxy.ts:187 write it both to the in-memory snapshot and to the persisted runtime-observability snapshot. anything that picks up getStatus() (the rotation app helper status file at runtime-rotation-app-helper.json, the app-bind status file referenced in the PR summary, or any debug bundle scooping observability state) lands raw account emails on disk. index + label + accountId already uniquely identify a rotated account for operator use.

test/runtime-rotation-proxy.test.ts:208-213 currently pins this exact field in place — pair this fix with that test update.

🛡️ drop email from the persisted status surface
 export interface RuntimeRotationProxyStatus {
 	…
 	lastAccountIndex: number | null;
 	lastAccountLabel: string | null;
-	lastAccountEmail: string | null;
 	lastAccountId: string | null;
 	lastAccountUpdatedAt: number | null;
 }
…
 	status.lastAccountLabel = identity.label;
-	status.lastAccountEmail = identity.email;
 	status.lastAccountId = identity.accountId;
…
 	mutateRuntimeObservabilitySnapshot((snapshot) => {
 		snapshot.lastAccountIndex = identity.index;
 		snapshot.lastAccountLabel = identity.label;
-		snapshot.lastAccountEmail = identity.email;
 		snapshot.lastAccountId = identity.accountId;
 		snapshot.lastAccountUpdatedAt = identity.updatedAt;
 	});

As per coding guidelines for lib/**: "check for logging that leaks tokens or emails."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/runtime-rotation-proxy.ts` around lines 39 - 52, The public status type
and snapshot writing still include the PII field lastAccountEmail, so remove
lastAccountEmail from the RuntimeRotationProxyStatus interface and stop writing
it in the snapshot and persisted status paths (referencing
RuntimeRotationProxyStatus, getStatus(), and where snapshots are built in
runtime-rotation-proxy methods that set lastAccountEmail); instead persist only
lastAccountIndex, lastAccountLabel, lastAccountId and lastAccountUpdatedAt to
uniquely identify accounts. Also update the test runtime-rotation-proxy.test.ts
(the assertions around lines referencing lastAccountEmail) to no longer expect
or assert the email field. Ensure no other logging or status serialization
writes the raw email.
test/codex-bin-wrapper.test.ts (1)

196-209: ⚠️ Potential issue | 🟡 Minor

fixture status still drifts from RuntimeRotationProxyStatus — missing startedAt and streamsStarted.

the previous review flagged that getStatus: () => ({}) did not satisfy the contract at lib/runtime-rotation-proxy.ts:39-52. the new buildStatus() covers most fields but still omits startedAt and streamsStarted, which the real status object exposes (see lib/runtime-rotation-proxy.ts:626-639 and the streaming bump at lib/runtime-rotation-proxy.ts:566). if scripts/codex.js or the app-helper ever start reading either of those (e.g. for uptime or stream telemetry), the wrapper tests will silently pass against a stub that doesn't reflect production shape.

🛡️ proposed fix
 			"function buildStatus() {",
 			"  return {",
+			"    startedAt: readOptionalNumberEnv('CODEX_MULTI_AUTH_TEST_PROXY_STARTED_AT') ?? 0,",
 			"    totalRequests: readOptionalNumberEnv('CODEX_MULTI_AUTH_TEST_PROXY_REQUESTS') ?? 0,",
 			"    upstreamRequests: 0,",
 			"    retries: 0,",
 			"    rotations: readOptionalNumberEnv('CODEX_MULTI_AUTH_TEST_PROXY_ROTATIONS') ?? 0,",
+			"    streamsStarted: readOptionalNumberEnv('CODEX_MULTI_AUTH_TEST_PROXY_STREAMS_STARTED') ?? 0,",
 			"    lastAccountIndex: readOptionalNumberEnv('CODEX_MULTI_AUTH_TEST_PROXY_LAST_ACCOUNT_INDEX'),",

As per coding guidelines for test/**: "demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/codex-bin-wrapper.test.ts` around lines 196 - 209, The test fixture
buildStatus() is missing the startedAt and streamsStarted fields required to
match RuntimeRotationProxyStatus; update buildStatus (in
test/codex-bin-wrapper.test.ts) to include startedAt (use a numeric timestamp or
null, e.g. readOptionalNumberEnv(...) ?? null) and streamsStarted (default to 0)
so the stub shape matches the real object returned by RuntimeRotationProxy (see
buildStatus, getStatus and runtime-rotation-proxy usages like streamsStarted and
startedAt).
test/runtime-rotation-proxy.test.ts (1)

138-424: ⚠️ Potential issue | 🟠 Major

still missing the regression cases the past review flagged.

re-checking the suite against the past review's three open items and test/** guidelines ("demand regression cases that reproduce concurrency bugs, token refresh races..."):

  1. x-api-key client auth path untested. lib/runtime-rotation-proxy.ts:153 accepts x-api-key: <clientApiKey> as an alternate auth header. the only auth test at test/runtime-rotation-proxy.test.ts:139-174 exercises Bearer only. add an accepts x-api-key client tokens case and confirm Bearer + x-api-key are independently honored.

  2. no concurrent inbound requests. lib/runtime-rotation-proxy.ts:868-870 dispatches handleRequest per connection and mutates shared status (lastError, lastAccountIndex) at every await boundary. fire N postResponses via Promise.all and assert counters reconcile (no negative upstreamRequests, no duplicate active-account writes via withAccountStorageTransactionMock).

  3. token refresh race untested. every fixture account at test/runtime-rotation-proxy.test.ts:46 has expiresAt: now + 3_600_000, so ensureFreshAccessToken (lib/runtime-rotation-proxy.ts:323-373) never fires. add a case with expiresAt < now, two simultaneous postResponses, and assert queuedRefresh dedupes (single commitRefreshedAuth call) and both responses succeed.

  4. pool-exhaustion retry_after_ms is asserted loosely. test/runtime-rotation-proxy.test.ts:394 only checks > 0; the contract at lib/runtime-rotation-proxy.ts:547 returns accountManager.getMinWaitTimeForFamily(family, model). assert exact equality so a future change to writePoolExhausted doesn't go unnoticed.

As per coding guidelines for test/**: "demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/runtime-rotation-proxy.test.ts` around lines 138 - 424, Add three
regression tests and tighten the pool-exhaustion assertion: (1) add an "accepts
x-api-key client tokens" test that posts to postResponses using header
"x-api-key: runtime-secret" and verify it is accepted independently of Bearer
auth (reference startRuntimeRotationProxy / clientApiKey and the existing Bearer
test at runtime-rotation-proxy tests); (2) add a concurrency test that fires N
simultaneous postResponses via Promise.all against startProxy and assert
proxy.getStatus() counters (upstreamRequests,
lastAccountIndex/lastAccountEmail), no negative upstreamRequests, and that
withAccountStorageTransactionMock was not invoked multiple duplicate times
(reference handleRequest and withAccountStorageTransactionMock); (3) add a
token-refresh race test by creating accounts with expiresAt < now, issue two
simultaneous postResponses, assert both succeed and that commitRefreshedAuth (or
the exported commit refresh hook used in ensureFreshAccessToken) was called
exactly once (reference ensureFreshAccessToken / queuedRefresh); and (4) change
the pool-exhaustion test to assert payload.error.retry_after_ms equals
accountManager.getMinWaitTimeForFamily("codex","gpt-5-codex") instead of just >
0 so the exact contract is enforced (reference writePoolExhausted and
getMinWaitTimeForFamily).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/runtime-rotation-proxy.ts`:
- Around line 198-202: The code currently calls accountManager.saveToDisk()
synchronously after marking switches for MODEL_FAMILIES, causing high write
amplification; change that call to accountManager.saveToDiskDebounced() to
coalesce writes (keep the loop and markSwitched calls as-is). Also evaluate
whether syncCodexCliActiveSelectionForIndex(account.index) truly needs immediate
persistence; if not, remove or defer it (e.g., call it from the debounced save
completion or omit it) so the request path avoids the extra immediate disk
write.
- Around line 193-207: persistRuntimeActiveAccount currently iterates
MODEL_FAMILIES and calls accountManager.markSwitched(account, "rotation",
family) for every family; change it to only call markSwitched for the family
actually served (use a new parameter family: ModelFamily in
persistRuntimeActiveAccount and call accountManager.markSwitched(account,
"rotation", family) only for that family) and update the site that invokes
persistRuntimeActiveAccount (the call that has access to context.family) to pass
context.family so only the served family’s currentAccountIndexByFamily is
stamped.

In `@scripts/codex.js`:
- Around line 425-482: The pendingAccountReadIds set in
createAppServerAccountReadProtocolProxy can grow unbounded because ids are only
removed when a response with a result arrives; modify the bookkeeping to (1)
soft-cap the set (e.g., if (pendingAccountReadIds.size > 4096) {
pendingAccountReadIds.clear(); console.error(...) }) or implement a TTL map
(Map<key, expiresAt>) and prune on insert to avoid leaks, and (2) update
rewriteAppServerAccountReadResponseLine to delete the id for both JSON-RPC
"result" and "error" responses (or check Object.hasOwn(message, "error")
alongside "result") and reorder early returns so the removal from
pendingAccountReadIds happens before returning; use the existing jsonRpcIdKey,
pendingAccountReadIds, createAppServerAccountReadProtocolProxy, and
rewriteAppServerAccountReadResponseLine symbols to locate and change the logic.

In `@test/codex-manager-cli.test.ts`:
- Around line 6883-6948: The test claims "before rendering" but never asserts
call order; update the test after calling runCodexMultiAuthCli to assert that
setCodexCliActiveSelectionMock ran before promptLoginModeMock by comparing their
mock.invocationCallOrder values (e.g., get the first invocationCallOrder entry
for setCodexCliActiveSelectionMock and for promptLoginModeMock and
expect(setOrder).toBeLessThan(promptOrder)); keep existing expectations but add
this ordering assertion to ensure the sync happens prior to rendering/prompting.
- Around line 946-954: Add a deterministic regression test case mirroring the
existing "intentional-reset" mock but using Windows-style backslash paths so the
intentional-reset branch output remains correct on Windows; locate the test near
inspectStorageHealthMock.mockResolvedValueOnce in test/codex-manager-cli.test.ts
and add another mockResolvedValueOnce with state: "intentional-reset" and
path/resetMarkerPath/walPath values containing backslashes (e.g.
"C:\\mock\\openai-codex-accounts.json" and the corresponding
".intentional-reset" and ".wal" variants), then assert the CLI status output for
the intentional reset case matches the expected Windows path formatting. Ensure
the test uses vitest utilities and stays deterministic (no real secrets or
skipped assertions).

In `@test/runtime-rotation-proxy.test.ts`:
- Around line 208-213: The test currently asserts a concrete PII value from
proxy.getStatus() (lastAccountEmail) which pins PII leakage behavior; update the
assertion to enforce a redacted contract instead: remove the lastAccountEmail
key from the expected object or replace that line with a negative assertion like
expect(proxy.getStatus()).not.toHaveProperty("lastAccountEmail") (or assert a
hashed/redacted form) so the test matches the intended behavior
implemented/expected in lib/runtime-rotation-proxy.ts (see handling around
runtime-rotation-proxy.ts:49) rather than asserting the raw email.

---

Outside diff comments:
In `@scripts/codex.js`:
- Around line 1183-1290: captureShadowHomeState currently reads files with
readFileSync(..., "utf8") which corrupts binary files when mirror fallback
copies are synced back; change captureShadowHomeState to read raw Buffers (omit
encoding) and update shadowHomeStateMatches to compare using
Buffer.equals(left.content, right.content) (ensuring you coerce/handle
null/unreadable cases); syncShadowHomeStateFile can remain as-is since it copies
bytes; add a vitest in test/shadow-home-mirror.test.ts that stubs
linkFileIntoShadowHome to return false, seeds a binary file
(Buffer.from([0xff,0xfe,0x00,0x80])) in the real ~/.codex fixture, runs
createShadowHomeMirror + cleanup, and asserts the original bytes survive
unchanged.

---

Duplicate comments:
In `@lib/runtime-rotation-proxy.ts`:
- Around line 148-154: Replace the direct string equality checks in
isAuthorizedClient with a timing-safe comparison: import timingSafeEqual from
node:crypto, turn both operands into Buffer instances (e.g., Buffer.from(token,
'utf8')) and only call timingSafeEqual when the two buffers have identical
lengths, falling back to false if lengths differ; apply this to the bearer token
comparison (bearerMatch?.[1]) and the x-api-key header comparison while
preserving the existing early-return behavior when clientApiKey is
null/undefined.
- Around line 724-745: The fetch and streaming paths in runtime-rotation-proxy
lack timeouts: wrap the fetchImpl call in the request loop (the block that
assigns upstream in the rotation loop) with an AbortController wired to
getFetchTimeoutMs(pluginConfig) so fetchImpl receives a signal and aborts on
timeout (so the existing catch path that refunds and cools down still applies),
add a per-chunk stall watchdog in forwardStreamingResponse that resets whenever
a chunk is received and calls onStreamError()/aborts the stream after
getStreamStallTimeoutMs(pluginConfig) elapses with no data, and add a vitest in
test/runtime-rotation-proxy.test.ts that stubs fetchImpl to never resolve and
asserts the pool exhaustion/rotation behavior triggers within the configured
timeout; reference the symbols fetchImpl, forwardStreamingResponse,
getFetchTimeoutMs, getStreamStallTimeoutMs, onStreamError, and accountManager to
locate and modify the code.
- Around line 220-226: The readRequestBody function currently reads the whole
request into memory with no cap; modify readRequestBody to enforce a 64 MiB
(67_108_864 bytes) maximum while accumulating chunks and throw an error (e.g.,
attach statusCode = 413) when the limit is exceeded, so it stops consuming the
request; update the outer error handling logic in the runtime-rotation-proxy
request flow (the catch that converts thrown errors into responses) to translate
that thrown statusCode into an HTTP 413 response; add a vitest in
test/runtime-rotation-proxy.test.ts that POSTS a body larger than 64 MiB and
asserts the proxy returns 413.
- Around line 768-797: The 401 (HTTP_STATUS.UNAUTHORIZED) branch in the rotation
logic (within the block that calls readErrorBody, accountManager.recordFailure,
accountManager.markAccountCoolingDown, accountManager.saveToDiskDebounced and
updates exhaustionReason/status.retries/status.rotations) does not call
accountManager.refundToken like the 5xx branch; either add a call to
accountManager.refundToken(refreshed.account, context.family, context.model) in
that 401 branch to keep token bookkeeping consistent, or if the intent is that
quota is consumed on 401, add a one-line comment above the 401 branch explaining
that choice (and likewise add a clarifying comment in the 429 branch) so future
contributors won’t normalize the behavior accidentally.
- Around line 664-666: The per-request rotation loop in runtime-rotation-proxy
currently uses accountManager.getAccountCount() as the dynamic ceiling, allowing
a single prompt to iterate the whole pool; change the loop ceiling to a bounded
budget (e.g., const rotationBudget = Math.min(accountManager.getAccountCount(),
5)) and use attemptedIndexes.size < rotationBudget in place of Math.max(1,
accountManager.getAccountCount()); after hitting the budget emit the existing
pool_exhausted event with { reason: "budget" } and break out (while still
keeping accountManager.getAccountCount() as an absolute safety bound elsewhere),
and ensure this logic wraps where chooseAccount(...) and attemptedIndexes are
used to prevent full-pool walking per prompt.
- Around line 39-52: The public status type and snapshot writing still include
the PII field lastAccountEmail, so remove lastAccountEmail from the
RuntimeRotationProxyStatus interface and stop writing it in the snapshot and
persisted status paths (referencing RuntimeRotationProxyStatus, getStatus(), and
where snapshots are built in runtime-rotation-proxy methods that set
lastAccountEmail); instead persist only lastAccountIndex, lastAccountLabel,
lastAccountId and lastAccountUpdatedAt to uniquely identify accounts. Also
update the test runtime-rotation-proxy.test.ts (the assertions around lines
referencing lastAccountEmail) to no longer expect or assert the email field.
Ensure no other logging or status serialization writes the raw email.

In `@test/codex-bin-wrapper.test.ts`:
- Around line 196-209: The test fixture buildStatus() is missing the startedAt
and streamsStarted fields required to match RuntimeRotationProxyStatus; update
buildStatus (in test/codex-bin-wrapper.test.ts) to include startedAt (use a
numeric timestamp or null, e.g. readOptionalNumberEnv(...) ?? null) and
streamsStarted (default to 0) so the stub shape matches the real object returned
by RuntimeRotationProxy (see buildStatus, getStatus and runtime-rotation-proxy
usages like streamsStarted and startedAt).

In `@test/runtime-rotation-proxy.test.ts`:
- Around line 138-424: Add three regression tests and tighten the
pool-exhaustion assertion: (1) add an "accepts x-api-key client tokens" test
that posts to postResponses using header "x-api-key: runtime-secret" and verify
it is accepted independently of Bearer auth (reference startRuntimeRotationProxy
/ clientApiKey and the existing Bearer test at runtime-rotation-proxy tests);
(2) add a concurrency test that fires N simultaneous postResponses via
Promise.all against startProxy and assert proxy.getStatus() counters
(upstreamRequests, lastAccountIndex/lastAccountEmail), no negative
upstreamRequests, and that withAccountStorageTransactionMock was not invoked
multiple duplicate times (reference handleRequest and
withAccountStorageTransactionMock); (3) add a token-refresh race test by
creating accounts with expiresAt < now, issue two simultaneous postResponses,
assert both succeed and that commitRefreshedAuth (or the exported commit refresh
hook used in ensureFreshAccessToken) was called exactly once (reference
ensureFreshAccessToken / queuedRefresh); and (4) change the pool-exhaustion test
to assert payload.error.retry_after_ms equals
accountManager.getMinWaitTimeForFamily("codex","gpt-5-codex") instead of just >
0 so the exact contract is enforced (reference writePoolExhausted and
getMinWaitTimeForFamily).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4bd97771-63a8-474a-b417-d70f8e9ae301

📥 Commits

Reviewing files that changed from the base of the PR and between 3ec7e6e and 98bd520.

📒 Files selected for processing (6)
  • lib/codex-manager.ts
  • lib/runtime-rotation-proxy.ts
  • scripts/codex.js
  • test/codex-bin-wrapper.test.ts
  • test/codex-manager-cli.test.ts
  • test/runtime-rotation-proxy.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,ts,mjs,cjs}

📄 CodeRabbit inference engine (README.md)

Store multi-auth settings in ~/.codex/multi-auth/settings.json or override with CODEX_MULTI_AUTH_DIR environment variable

Store OAuth accounts in ~/.codex/multi-auth/openai-codex-accounts.json with per-project accounts under ~/.codex/multi-auth/projects/<project-key>/openai-codex-accounts.json

Respect environment variable overrides for runtime configuration: CODEX_MULTI_AUTH_DIR, CODEX_MULTI_AUTH_CONFIG_PATH, CODEX_MODE, CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY, CODEX_MULTI_AUTH_APP_ROTATION_IDLE_MS, CODEX_MULTI_AUTH_APP_BIND_INSTALL, CODEX_MULTI_AUTH_APP_LAUNCHER_INSTALL, CODEX_TUI_V2, CODEX_TUI_COLOR_PROFILE, CODEX_TUI_GLYPHS, CODEX_AUTH_BACKGROUND_RESPONSES, CODEX_AUTH_FETCH_TIMEOUT_MS, CODEX_AUTH_STREAM_STALL_TIMEOUT_MS

Implement account health-aware selection and automatic failover for the OAuth account pool

Disable whole-pool replay by default when every account is rate-limited; implement bounded outbound request budget per prompt to prevent indefinite pool walking

Trigger cooldown instead of aggressive rotation on repeated cross-account 5xx bursts

Stagger proactive refresh to reduce background refresh bursts

Implement flagged account verification and restore flow for previously flagged accounts

Support session affinity and live account sync controls for account switching during runtime

Expose runtime counters, budget/cooldown state, and multi-auth probe visibility in codex auth status and codex auth report commands

Ensure Codex-oriented request/prompt compatibility with strict runtime handling for forwarded Codex CLI/app sessions

Enable backgroundResponses in settings or set CODEX_AUTH_BACKGROUND_RESPONSES=1 only for callers that intentionally send background: true in requests

Preserve destination-only accounts and fail safely on backup filename collisions in experimental settings

Files:

  • lib/codex-manager.ts
  • test/codex-manager-cli.test.ts
  • test/codex-bin-wrapper.test.ts
  • test/runtime-rotation-proxy.test.ts
  • scripts/codex.js
  • lib/runtime-rotation-proxy.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/codex-manager.ts
  • lib/runtime-rotation-proxy.ts
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/codex-manager-cli.test.ts
  • test/codex-bin-wrapper.test.ts
  • test/runtime-rotation-proxy.test.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:22.007Z
Learning: Validate configuration after changes using `codex auth status`, `codex auth check`, and `codex auth forecast --live` commands
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:30.232Z
Learning: Plugin runtime config source selection follows this order: (1) Unified settings `pluginConfig` from `settings.json` when present and valid, (2) Fallback file config from `CODEX_MULTI_AUTH_CONFIG_PATH` when unified settings are absent/invalid, (3) Hardcoded defaults; environment variables override individual runtime settings after source selection
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:30.232Z
Learning: Keep recommended defaults enabled: `menuAutoFetchLimits`, `menuSortEnabled`, `liveAccountSync`, `sessionAffinity`, `proactiveRefreshGuardian`, and `preemptiveQuotaEnabled` for most environments
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:30.232Z
Learning: Use stable environment overrides (`CODEX_MULTI_AUTH_DIR`, `CODEX_MULTI_AUTH_CONFIG_PATH`, `CODEX_MODE`, etc.) for day-to-day workflows and reserve advanced/internal overrides for debugging and maintainer workflows only
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:30.232Z
Learning: Validate effective configuration using `codex auth status`, `codex auth list`, `codex auth check`, and `codex auth forecast --live` commands
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:49.773Z
Learning: When enabling `backgroundResponses` compatibility switch, ensure that stateless pipelines are not affected by forced `store=true`, preserve input item IDs, and test one known `background: true` request end-to-end before rolling out across shared automation
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:49.773Z
Learning: Cross-process refresh coordination must use lease/state files and should not be manually edited while the CLI is running
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:49.773Z
Learning: Backup and WAL artifacts may exist briefly during writes and recovery as part of normal safety behavior and should not be treated as errors
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:59.674Z
Learning: Store canonical local data under `~/.codex/multi-auth`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:59.674Z
Learning: Support project-scoped account pools for repo-specific account state separation
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:59.674Z
Learning: Implement linked-worktree identity sharing to allow the same repository to share account state across worktrees
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:59.674Z
Learning: Persist quota cache between runs for faster forecast and dashboard visibility
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:59.674Z
Learning: Implement browser-first OAuth flow with manual fallback using `--manual`, `--no-browser`, and `CODEX_AUTH_NO_BROWSER=1` environment variable
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:59.674Z
Learning: Support manual/non-TTY login that accepts full callback URL on stdin for automation and host-managed shells
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:59.674Z
Learning: Implement safe repair workflow in `codex auth fix` to detect and repair known local storage inconsistencies
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:59.674Z
Learning: Provide diagnostics and optional repair capabilities via `codex auth doctor` command
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:59.674Z
Learning: Implement backup and WAL (Write-Ahead Logging) recovery in storage runtime for safer persistence during interrupted or partial writes
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:10:59.674Z
Learning: Support optional plugin runtime that uses the same account pool for request transformation, token refresh, retry/cooldown handling, session affinity, and quota-aware account selection
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Primary operations use `codex auth ...` command. Compatibility aliases are supported: `codex multi auth ...`, `codex multi-auth ...`, `codex multiauth ...`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Use `codex auth login` to open interactive auth dashboard; support `--manual` or `--no-browser` flags to skip browser launch and use manual callback flow
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Use `codex auth list` to list saved accounts and active account, and `codex auth switch <index>` to set active account by index
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Use `codex auth verify` as the single entry point for installation self-tests, supporting `--paths`, `--flagged`, and `--all` modes; use `--json` for machine-readable output
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Use `codex auth why-selected [--now|--last] [--json]` to explain which account the rotation selector would pick with per-candidate scoring
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Use `codex auth rotation` subcommands (enable, disable, status, bind-app, unbind-app) to manage the opt-in runtime Responses proxy for live Codex account rotation
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Enable runtime rotation via `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=1` environment variable for the current process without changing settings
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: When rotation is enabled, create a temporary shadow `CODEX_HOME/config.toml` with custom provider named `codex-multi-auth-runtime-proxy` and start a `127.0.0.1` proxy on a random port
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: For non-TTY sessions (including `CODEX_TUI=1`, `CODEX_DESKTOP=1`, `TERM_PROGRAM=codex`, `ELECTRON_RUN_AS_NODE=1`), auth flows should degrade to deterministic text behavior with defaults for workspace selection
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: In non-TTY/manual shells, accept the full OAuth redirect URL on stdin, for example: `echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: `codex --version` should report the official `openai/codex` CLI version; `codex-multi-auth --version` and `codex-multi-auth -v` should report the wrapper package version
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Use structured JSON output format with `--json` flag on commands like `verify-flagged`, `verify`, `why-selected`, `best`, `forecast`, `report`, `fix`, `doctor`, `config explain`, and `debug bundle`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Use `--explain` flag with `forecast` and `report` commands to include reasoning details in output (text or JSON)
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Use `--live` flag with `best`, `forecast`, `report`, and `fix` commands to use live probe before decisions/output
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Use `--dry-run` flag with `verify-flagged`, `verify`, `fix`, and `doctor` commands to preview changes without writing to storage
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Use `--model <model>` flag with `best`, `forecast`, `report`, and `fix` commands to specify the model for live probe paths
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Recommended workflow pack for health and planning: `codex auth check`, `codex auth forecast --live --explain --model gpt-5-codex`, `codex auth report --live --json`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:14.839Z
Learning: Recommended workflow pack for repair and recovery: `codex auth fix --dry-run`, `codex auth fix --live --model gpt-5-codex`, `codex auth doctor --fix`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:24.393Z
Learning: When `CODEX_MULTI_AUTH_DIR` is set, the settings file root location moves accordingly
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:24.393Z
Learning: Use environment variables `CODEX_MULTI_AUTH_DIR`, `CODEX_MULTI_AUTH_CONFIG_PATH`, `CODEX_MODE`, `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY`, `CODEX_MULTI_AUTH_APP_BIND_INSTALL`, `CODEX_TUI_V2`, `CODEX_TUI_COLOR_PROFILE`, `CODEX_TUI_GLYPHS`, `CODEX_AUTH_FETCH_TIMEOUT_MS`, and `CODEX_AUTH_STREAM_STALL_TIMEOUT_MS` for stable operator overrides
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:24.393Z
Learning: Reject named backup filenames that contain separators, traversal patterns (`..`), `.rotate.`, `.tmp`, and `.wal` suffixes
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:24.393Z
Learning: Always show preview before applying sync changes, and preserve destination active selection and destination-only accounts during merge preview/apply
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:24.393Z
Learning: After configuration changes, validate with `codex auth status`, `codex auth check`, and `codex auth forecast --live` commands
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:29.981Z
Learning: Maintain deterministic fallback behavior for older official Codex runtimes and non-entitled or quota-limited accounts when handling gpt-5.5 models
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:29.981Z
Learning: Implement `codexRuntimeRotationProxy` as opt-in feature with environment variable `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=1` and CLI commands `codex auth rotation enable|disable|status`
Learnt from: CR
Repo: ndycode/codex-multi-auth

Timestamp: 2026-04-25T03:11:29.981Z
Learning: Ensure wrapper retry path handles both ChatGPT unsupported-model errors and upstream no-access variant (`does not exist or you do not have access`) responses
🔇 Additional comments (5)
test/codex-manager-cli.test.ts (1)

9-9: good deterministic storage-health test scaffolding

the new inspectStorageHealth mock wiring/reset/default state makes command tests deterministic and aligned with the storagehealthsummary contract (test/codex-manager-cli.test.ts:9, test/codex-manager-cli.test.ts:183, test/codex-manager-cli.test.ts:661, test/codex-manager-cli.test.ts:828, lib/storage/health.ts:1).

Also applies to: 183-183, 661-661, 828-836

scripts/codex.js (3)

1377-1406: bundle-level coherence on shadow-home sync-back is still per-file — concurrent sessions can publish mixed auth state

the cleanup closure now adds an "only write if currentOriginalState still matches my snapshot" check (scripts/codex.js:1392-1395), which is a nice optimistic guard against trampling another session's later write. but the loop is still per-file: session A can win on auth.json while session B wins on accounts.json, and the resulting on-disk bundle pairs an auth.json from session A with an accounts.json from session B. that's exactly the failure mode flagged in the previous review — bundle atomicity, not file atomicity.

the right shape is still: take a single bundle-level lease on originalCodexHome (e.g., a .codex-multi-auth.sync.lock advisory file with pid+ts), capture all SHADOW_HOME_STATE_FILES snapshots together, decide the per-file write set, perform the renames as a group, then release. fall back to per-file best-effort only when the lease cannot be acquired within a small budget. this matches the cross-process refresh learning in the project memory.

at minimum, please land a vitest regression that simulates two createShadowHomeMirror cleanups racing against the same originalCodexHome (interleaved captureShadowHomeState + syncShadowHomeStateFile calls) and asserts the final on-disk bundle is internally consistent (e.g., active account in auth.json exists in accounts.json).

based on retrieved learnings: "Cross-process refresh coordination must use lease/state files and should not be manually edited while the CLI is running."


1455-1469: still warning on every codex invocation when CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY is malformed

parseRuntimeRotationProxyEnv (scripts/codex.js:1455-1469) is reachable from isRuntimeRotationProxyEnabled (scripts/codex.js:1471-1492) which runs on every forwarded command. one typo in a shell rc → a console.error line on every codex … call until the user finds and fixes it. previous review asked for a module-scoped alreadyWarnedInvalidRuntimeRotationProxy guard; the code as-shown still re-warns unconditionally.

+let alreadyWarnedInvalidRuntimeRotationProxy = false;
+
 function parseRuntimeRotationProxyEnv(value) {
 	if (value === undefined) return undefined;
 	const normalized = value.trim().toLowerCase();
 	if (normalized.length === 0) return undefined;
 	if (normalized === "1" || normalized === "true" || normalized === "yes") {
 		return true;
 	}
 	if (normalized === "0" || normalized === "false" || normalized === "no") {
 		return false;
 	}
-	console.error(
-		"codex-multi-auth: ignoring invalid CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY value. Expected 0/1, true/false, or yes/no.",
-	);
+	if (!alreadyWarnedInvalidRuntimeRotationProxy) {
+		alreadyWarnedInvalidRuntimeRotationProxy = true;
+		console.error(
+			"codex-multi-auth: ignoring invalid CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY value. Expected 0/1, true/false, or yes/no.",
+		);
+	}
 	return undefined;
 }

1498-1544: TOML table-detection regex still misses [[array_of_tables]] headers

both removeRuntimeRotationProviderBlock (scripts/codex.js:1508) and rewriteTopLevelModelProvider (scripts/codex.js:1526) use /^\s*\[[^\]]+\]\s*$/, which does not match [[profiles.foo]]-style headers. behavioural fallout:

  • in removeRuntimeRotationProviderBlock: if our provider block is followed by a [[...]] section instead of a single-bracket table, we keep "skipping" past the array-of-tables header and silently drop user content.
  • in rewriteTopLevelModelProvider: if the user's config.toml opens with a [[...]] section, we never find a "table" boundary, never inject model_provider = … at the top, and instead append it at end-of-file, where it gets nested under whatever [[…]] was last — codex ignores it and the proxy never activates.

both regexes need to accept one or two brackets:

-		if (skipping && /^\s*\[[^\]]+\]\s*$/.test(line)) {
+		if (skipping && /^\s*\[{1,2}[^\]]+\]{1,2}\s*$/.test(line)) {
 			skipping = false;
 		}
-		const isTable = /^\s*\[[^\]]+\]\s*$/.test(line);
+		const isTable = /^\s*\[{1,2}[^\]]+\]{1,2}\s*$/.test(line);

still no test coverage for rewriteTopLevelModelProvider, removeRuntimeRotationProviderBlock, or rewriteConfigTomlForRuntimeRotationProxy. please add a fixture in test/codex-bin-wrapper.test.ts (or a dedicated test/runtime-rotation-config-rewrite.test.ts) with a config.toml that opens with [[profiles.foo]]\nmodel = "x"\n and asserts rewriteConfigTomlForRuntimeRotationProxy(...) injects model_provider at the top, above the array-of-tables header.

lib/codex-manager.ts (1)

1207-1254: activeAccountMatchesCodexCliState defaults to "drifted" on missing identifiers and syncCodexCliActiveSelectionIfDrifted force-refreshes from disk on every menu iteration

two bugs in the same helper pair at lib/codex-manager.ts:1208-1254, both live in the menu hot path:

  1. activeAccountMatchesCodexCliState returns false whenever neither accountId pair nor email pair can be compared — including cases where the local account has no accountId but the cli state does. that flips into a "drifted" condition and writes undefined ids back into cli state via setCodexCliActiveSelection, clobbering valid state. fail-safe direction is "no drift" when we cannot compare. change return false to return true at line 1224.

  2. syncCodexCliActiveSelectionIfDrifted calls loadCodexCliState({ forceRefresh: true }) at line 1233 on every menu render (line 2789 is inside the promptLoginMode loop). this bypasses in-process cache and hits disk every iteration, stacking on top of loadFlaggedAccounts() + loadDashboardDisplaySettings() already running per pass. gate the call to only run once per 5 seconds or similar.

no vitest coverage exists for either helper — land regression tests covering (a) account with only email vs cli state with only accountId → expect no write, and (b) drift detected → single write per drift cycle, not per render. check test/codex-manager*.test.ts or wherever runAuthLogin is exercised.

Comment thread lib/runtime-rotation-proxy.ts
Comment thread lib/runtime-rotation-proxy.ts Outdated
Comment thread scripts/codex.js
Comment thread test/codex-manager-cli.test.ts
Comment thread test/codex-manager-cli.test.ts
Comment thread test/runtime-rotation-proxy.test.ts
Comment thread lib/runtime/app-bind.ts
Comment thread lib/runtime/app-bind.ts
Comment thread scripts/codex.js
Comment thread scripts/codex.js
Comment thread scripts/codex.js
Comment thread lib/runtime/app-bind.ts
Comment thread scripts/codex.js Outdated
Comment thread lib/runtime-rotation-proxy.ts Outdated
Comment thread scripts/codex.js
Comment thread scripts/codex-app-router.js
Comment thread scripts/codex.js
Comment thread scripts/codex.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant