fix(codex): stop runtime helpers from leaking past their idle timeout (#663) - #664
Conversation
The idle reaper's owner check was a bare kill(pid, 0), which answers "does a process hold this integer", never "is this still my launcher". A recycled PID at one tick pushes the deadline forward 12 hours, and the deadline only ever moves forward, so one false positive is never corrected — helpers were observed 33 hours past their timeout, 183 concurrent, 5.6 GB RSS. Owner liveness is now PID plus the launcher's kernel start time (read under LC_ALL=C so locale cannot disable the check), re-verified at most once a minute; a failed re-read keeps the previous verdict instead of declaring a live owner dead, and where no start time is known the check degrades to bare liveness. An absolute lifetime ceiling (24h default, CODEX_MULTI_AUTH_APP_ROTATION_MAX_LIFETIME_MS) bounds the leak if activity accounting is ever wrong again. Status telemetry is per helper PID instead of N writers last-writer- winning one file at 1 Hz, published on change plus heartbeat; readers prefer the newest live helper and still read the legacy path, and app-bind unbind walks every per-PID candidate through the same ownership-verified stop it applied to the shared file. Helpers remove their owner file on exit; launchers sweep metadata whose helper PID is dead — or provably recycled, by comparing the PID's kernel start time against the file's own timestamps — before spawning. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FW2tPyLdcRXrnGsYVVJeEj
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 Walkthroughseverity: major. this fixes pid reuse, unbounded helper lifetime, status-file contention, and stale metadata risks. the main architectural decisions are pid-plus-kernel-start-time validation, per-pid status files with legacy fallback, bounded helper lifetime, and verified multi-helper cleanup. regression tests cover posix and windows paths, cleanup, status selection, migration, and concurrent helpers.
review risks:
Walkthroughruntime rotation helpers now use PID-specific status files, launcher start-time identity checks, idle and maximum lifetime limits, stale metadata cleanup, and live-helper-aware status selection. tests cover lifecycle limits, concurrent helper isolation, status discovery, and unbind cleanup. Changesruntime helper lifecycle
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Launcher
participant RuntimeAppHelper
participant OwnerProcess
participant StatusFiles
participant RotationStatus
Launcher->>RuntimeAppHelper: pass owner PID and start time
RuntimeAppHelper->>OwnerProcess: verify process identity
RuntimeAppHelper->>StatusFiles: publish PID-specific status
RotationStatus->>StatusFiles: discover legacy and PID-specific files
RotationStatus-->>RotationStatus: select newest live helper
the regression coverage does not include a windows-specific process-start lookup case. concurrent helper isolation is covered in 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
lib/codex-manager/commands/rotation.ts (1)
639-641: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winthe terminal-state list does not know about the new
max-lifetimestate.
scripts/codex.js:4360publishesstate: "max-lifetime"on the ceiling exit, andscripts/codex.js:4370publishes"error".lib/codex-manager/commands/rotation.ts:639still tests only"stopped"and"idle-timeout". a helper that hit the ceiling therefore falls through to the running branch, andisProcessAliveis the only remaining gate — the exact gate this PR just proved unreliable under pid reuse. the reader also selects the freshest terminal stamp when nothing is live, so amax-lifetimerecord is a normal thing to hit here.invert the check: treat only
"running"as running.🐛 treat any non-running state as not running
const alive = isProcessAlive(status.pid); - if (!alive || status.state === "stopped" || status.state === "idle-timeout") { + if (!alive || status.state !== "running") { return "Codex app helper: not running"; }
lib/runtime/runtime-current-account.ts:205already uses the!== "running"form, so this also removes the divergence between the two readers. please add a rotation-status regression case for amax-lifetimerecord.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/codex-manager/commands/rotation.ts` around lines 639 - 641, Update the rotation status check surrounding the visible terminal-state condition to treat only status.state === "running" as running; return "Codex app helper: not running" for max-lifetime, error, stopped, idle-timeout, and any other non-running state, regardless of isProcessAlive. Add a regression case covering a max-lifetime rotation record.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/development/ARCHITECTURE.md`:
- Line 278: Add a storage-table row for
runtime-rotation-app-helper-owner.<pid>.json, describing it as the
per-live-helper owner metadata file and noting the unsuffixed legacy path if
applicable. Keep the existing runtime-rotation-app-helper.<pid>.json entry
unchanged.
In `@docs/reference/storage-paths.md`:
- Line 162: Update the runtime-rotation helper status-file row in the storage
paths documentation to state that terminal per-<pid> files persist until the
next helper launch removes files for dead PIDs, while the owner’s file is
removed on clean helper exit.
In `@lib/codex-manager/commands/rotation.ts`:
- Around line 577-600: Update the rotation status flow so helper statuses are
read and parsed only once. Have printRotationStatus build the filtered
live-helper array from that single result, derive both the selected status and
liveHelperCount from it, and pass both values explicitly to
formatAppRuntimeHelperStatus instead of relying on countLiveAppRuntimeHelpers or
its default.
In `@lib/runtime/app-bind.ts`:
- Around line 1509-1529: Extract per-PID helper status path discovery into a
shared listRuntimeHelperStatusPaths helper in the runtime constants module,
using APP_RUNTIME_HELPER_STATUS_FILE to build the escaped pattern, filter
supplied directory entries, and append the legacy status path. Update the
app-bind discovery block and the corresponding rotation command logic to call
this helper, removing their duplicated pattern construction and path assembly
while preserving directory-read error handling.
- Around line 1518-1525: The readdir failure handling in the unbind flow must
retry and report errors instead of silently treating helper discovery as empty.
Update the helperStatusNames loading block to invoke readdir through
withFileOperationRetry, and on final failure emit a warning via options.log
before preserving the legacy-only fallback.
In `@scripts/codex.js`:
- Around line 4034-4090: The sweep currently performs an unbounded process-start
lookup for each live-PID candidate, blocking helper startup. Update
sweepStaleRuntimeRotationAppHelperMetadata to cache readProcessStartTimeMs
results by PID for the duration of one sweep and enforce a bounded number of
identity probes, treating candidates beyond the limit as not dead; add a
regression test with several hundred stale per-PID files that verifies the probe
count remains within the bound.
- Around line 3899-3923: Update the helper-side liveness recheck invoked through
createRuntimeRotationAppHelperOwnerLivenessCheck so readProcessStartTimeMs does
not block the live rotation proxy event loop for up to 2 seconds. Prefer an
asynchronous, single-flight ps probe that lets each tick reuse the last verdict
while a probe is pending; ensure a wedged ps cannot spawn overlapping child
processes. Leave the launcher-side synchronous sweep path unchanged.
- Around line 4017-4025: Update removeRuntimeRotationAppHelperOwnerFile and the
other runtime-rotation metadata deletion paths around their existing rmSync
calls to use withSynchronousFileOperationRetry, preserving best-effort cleanup
while retrying transient EBUSY, EPERM, and ENOTEMPTY failures. Add regression
coverage in the existing codex-bin-wrapper tests for transient Windows-lock
cleanup across all three deletion paths.
In `@test/app-bind.test.ts`:
- Around line 1133-1167: Expand the unbind regression coverage around
unbindCodexAppRuntimeRotation to include two dead per-PID status files plus the
legacy status file and assert all are removed in one call. Add an owner file
beside a per-PID record and assert unbind removes it, then add an identityToken
record without a matching owner and verify it remains while options.log receives
the ownership warning. Preserve the existing single-helper coverage.
In `@test/codex-bin-wrapper.test.ts`:
- Around line 3369-3457: Add regression coverage in the helper lifecycle test
around the existing statusFiles polling to verify deduplicated status
publishing: use a sufficiently long idle timeout, identify the running helper’s
status path, sample its statSync(...).mtimeMs, wait across multiple heartbeat
ticks without traffic, and assert the timestamp is unchanged. Keep the existing
sweep, owner cleanup, and terminal idle-timeout assertions intact.
- Around line 3238-3274: Gate the “idles out when the owner PID is alive but its
identity does not match” test to non-Windows platforms. Add a Windows-only
companion test asserting the helper remains alive when owner identity is
unavailable, and ensure this regression suite runs in the Windows workflow;
leave the existing test around line 3279 unguarded.
---
Outside diff comments:
In `@lib/codex-manager/commands/rotation.ts`:
- Around line 639-641: Update the rotation status check surrounding the visible
terminal-state condition to treat only status.state === "running" as running;
return "Codex app helper: not running" for max-lifetime, error, stopped,
idle-timeout, and any other non-running state, regardless of isProcessAlive. Add
a regression case covering a max-lifetime rotation record.
🪄 Autofix
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 Plus
Run ID: 5e87c0ad-3719-403e-b6e3-21e777665cea
📒 Files selected for processing (14)
AGENTS.mddocs/configuration.mddocs/development/ARCHITECTURE.mddocs/development/CONFIG_FIELDS.mddocs/privacy.mddocs/reference/storage-paths.mdlib/codex-manager/commands/rotation.tslib/runtime/app-bind.tslib/runtime/runtime-current-account.tsscripts/codex.jstest/app-bind.test.tstest/codex-bin-wrapper.test.tstest/codex-manager-rotation-command.test.tstest/runtime-current-account.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (23)
**/*
📄 CodeRabbit inference engine (AGENTS.md)
Do not edit generated
dist/output or local temporary/cache directories; modify source and regenerate build output instead.
Files:
AGENTS.mddocs/development/CONFIG_FIELDS.mdtest/codex-manager-rotation-command.test.tstest/app-bind.test.tsdocs/reference/storage-paths.mdtest/runtime-current-account.test.tsdocs/configuration.mddocs/development/ARCHITECTURE.mdlib/codex-manager/commands/rotation.tslib/runtime/runtime-current-account.tsdocs/privacy.mdlib/runtime/app-bind.tstest/codex-bin-wrapper.test.tsscripts/codex.js
docs/**/*.md
📄 CodeRabbit inference engine (docs/STYLE_GUIDE.md)
docs/**/*.md: User-facing documentation should follow the page template: Title and one-line lead, Quick path commands, Core operational workflow, Troubleshooting or failure handling, and Related links
Use short sections and scan-friendly tables in documentation where they improve clarity
Prefer direct, actionable language in documentation
Use runnable command examples in documentation
Explain expected outcomes after critical commands in documentation
Keep terminology consistent with runtime names in documentation
Avoid speculative language when behavior is deterministic in documentation
Put the user problem in the first paragraph before implementation detail
Use descriptive page titles such ascodex-multi-auth Featuresinstead of generic titles on public docs
Do not repeat keyword lists in every section; search terms should appear only where they help a developer understand the page
Canonical command family iscodex-multi-auth ...
Canonical runtime root is~/.codex/multi-auth
Runtime rotation must be described as default-on unless the release policy changes
Legacy command/path references belong only in migration contexts in documentation
Compatibility aliases (codex multi auth,codex multi-auth,codex multiauth) belong only in command reference, troubleshooting, or migration contexts
Keep command flags aligned with runtime usage text in documentation
Avoid non-runnable command snippets in documentation
Avoid conflicting path guidance across documentation
Avoid legacy-first onboarding language in documentationOrganize repository documentation according to the defined layers: product entry, user operations, reference, and development.
docs/**/*.md: Do not describecodex-multi-authas replacing@openai/codexor publishing the globalcodexbinary; preserve the official CLI's ownership ofcodex.
Usecodex-multi-authfor account management, and reservecodex-multi-auth-codexormcodexfor intentionally forwarding official Codex commands th...
Files:
docs/development/CONFIG_FIELDS.mddocs/reference/storage-paths.mddocs/configuration.mddocs/development/ARCHITECTURE.mddocs/privacy.md
docs/development/CONFIG_FIELDS.md
📄 CodeRabbit inference engine (docs/development/RUNBOOK_ADD_CONFIG_FIELD.md)
Update
docs/development/CONFIG_FIELDS.mdwith field inventory details when adding new configuration fieldsMaintain full field inventory in
docs/development/CONFIG_FIELDS.md
Files:
docs/development/CONFIG_FIELDS.md
docs/development/**/*.md
📄 CodeRabbit inference engine (docs/DOCUMENTATION.md)
Keep internal architecture, configuration flow, repository ownership, testing, parity, metadata, and audit guidance in development documentation.
Prefer current architecture and reference documentation over historical plans and audit snapshots when describing the present system.
Files:
docs/development/CONFIG_FIELDS.mddocs/development/ARCHITECTURE.md
docs/development/**/*
📄 CodeRabbit inference engine (docs/development/CONFIG_FLOW.md)
docs/development/**/*: Resolve the runtime root directory in this order:CODEX_MULTI_AUTH_DIR; explicit non-defaultCODEX_HOME/multi-auth; existing account-storage roots underCODEX_HOMEor~/.codex; canonical~/.codex/multi-auth; and legacy paths only when storage signals exist.
ReaddashboardDisplaySettingsandpluginConfigfromsettings.json, while preserving legacy compatibility loading and migration.
ResolvepluginConfigvalues using this precedence: existingCODEX_MULTI_AUTH_CONFIG_PATHfile, valid unifiedsettings.jsonconfiguration, legacy compatibility configuration, thenDEFAULT_PLUGIN_CONFIG; apply environment-variable overrides afterward.
Ignore a configured but nonexistentCODEX_MULTI_AUTH_CONFIG_PATHduring loading, but create it on the first save while the variable remains set.
Resolve dashboard display values from persisteddashboardDisplaySettings, followed by normalization and fallback defaults.
Resolve account storage by selecting the root directory, using the global accounts file by default, using a project-namespaced path when project-scoped mode is active, and attempting applicable legacy project-file migration.
Normalize standalonecodex-multi-authbare subcommands toauth ...before dispatch; normalize wrapper aliases; run auth-manager commands locally; forward out-of-scope wrapper commands to the official Codex CLI.
For forwarded request-bearing commands, honor runtime rotation: resolveCODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY, thenpluginConfig.codexRuntimeRotationProxy, which defaults to enabled.
When rotation is enabled for a requesting command, use a per-process-token loopback Responses proxy, a temporary shadowCODEX_HOME, and a rewrittenconfig.toml; synchronize refreshed official Codex state on exit and remove the shadow home.
The runtime proxy must select or refresh managed accounts and rotate on rate-limit, authentication, network, or server failures before streaming begins.
The plugin host m...
Files:
docs/development/CONFIG_FIELDS.mddocs/development/ARCHITECTURE.md
docs/development/**/*.{md,mdx}
📄 CodeRabbit inference engine (docs/development/TESTING.md)
When documentation changes, verify every command snippet is runnable, path references match runtime modules, cross-links are valid, and the feature matrix matches implemented features.
Files:
docs/development/CONFIG_FIELDS.mddocs/development/ARCHITECTURE.md
docs/**/*.{md,mdx}
📄 CodeRabbit inference engine (docs/troubleshooting.md)
Document that
codex-multi-auth-codexis the optional forwarding wrapper, whilecodex-multi-authis the canonical account-manager command family; the package does not publish a globalcodexbinary.Document the canonical command names, runtime paths, configuration precedence, storage migration behavior, and upgrade procedures consistently across the referenced documentation.
Files:
docs/development/CONFIG_FIELDS.mddocs/reference/storage-paths.mddocs/configuration.mddocs/development/ARCHITECTURE.mddocs/privacy.md
docs/**/*
📄 CodeRabbit inference engine (docs/privacy.md)
docs/**/*: Keep account and session state local under the configured runtime root; honorCODEX_MULTI_AUTH_DIRandCODEX_MULTI_AUTH_CONFIG_PATHoverrides.
Do not add custom analytics, a project-owned remote database, or network destinations beyond the required OpenAI OAuth/backend and GitHub raw/releases endpoints.
Runtime rotation and the optional local bridge must use loopback-only listeners; the bridge must expose only/health,/v1/models, and/v1/responsesand require a local bearer token by default.
Store local bridge client tokens as SHA-256 hashes with prefixes and labels; never persist plaintext tokens, which may be shown only during create or rotate operations.
Treat raw request and response body logs enabled byCODEX_PLUGIN_LOG_BODIES=1as sensitive data; avoid exposing them by default and support appropriate rotation or deletion.
Usage ledger entries must contain only local request metadata summaries; hash email identities and never store prompts, authorization headers, or raw sensitive account identifiers.
Cleanup functionality must remove all canonical local data, including override-root locations whenCODEX_MULTI_AUTH_DIRorCODEX_MULTI_AUTH_CONFIG_PATHis configured, on supported platforms.
Files:
docs/development/CONFIG_FIELDS.mddocs/reference/storage-paths.mddocs/configuration.mddocs/development/ARCHITECTURE.mddocs/privacy.md
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/development/CONFIG_FIELDS.mddocs/reference/storage-paths.mddocs/configuration.mddocs/development/ARCHITECTURE.mddocs/privacy.md
test/**/*.test.ts
📄 CodeRabbit inference engine (test/AGENTS.md)
test/**/*.test.ts: Write Vitest test suites with globals enabled (describe, it, expect)
Maintain 80%+ coverage threshold across statements, branches, functions, and lines
Use removeWithRetry() for Windows filesystem cleanup instead of bare fs.rm to handle EBUSY, EPERM, and ENOTEMPTY errors
Do not rely on dist/ in tests; use source files instead
Do not skip tests without justification
Relax lint rules for test files as configured in eslint.config.js
Files:
test/codex-manager-rotation-command.test.tstest/app-bind.test.tstest/runtime-current-account.test.tstest/codex-bin-wrapper.test.ts
**/*.{js,ts,mjs,cjs}
📄 CodeRabbit inference engine (README.md)
**/*.{js,ts,mjs,cjs}: Do not publish or replace a globalcodexbinary; official OpenAI installation paths must retain ownership of thecodexcommand.
Keep OAuth credentials local and restrict runtime rotation and local bridges to loopback interfaces.
Require hashed local client tokens to protect the optional loopback bridge.
Responsesbackground: truecompatibility must remain opt-in; requests using it must use statefulstore=truerouting rather than statelessstore=falserouting.
Never run npm install or update commands automatically; only display a manual upgrade notice when appropriate.
Experimental synchronization and backup flows must be non-destructive by default: preview before applying sync, preserve destination-only accounts, and fail safely on backup filename collisions.
Keep account storage project-scoped under the configured multi-auth root when operating in repo-specific workflows.
Files:
test/codex-manager-rotation-command.test.tstest/app-bind.test.tstest/runtime-current-account.test.tslib/codex-manager/commands/rotation.tslib/runtime/runtime-current-account.tslib/runtime/app-bind.tstest/codex-bin-wrapper.test.tsscripts/codex.js
**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,js}: Use ESM-only modules; the package is configured with"type": "module".
Do not useas any,@ts-ignore, or@ts-expect-error.
Runtime rotation must remain enabled by default and stay aligned with explicit release and migration documentation.
Files:
test/codex-manager-rotation-command.test.tstest/app-bind.test.tstest/runtime-current-account.test.tslib/codex-manager/commands/rotation.tslib/runtime/runtime-current-account.tslib/runtime/app-bind.tstest/codex-bin-wrapper.test.tsscripts/codex.js
test/**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Windows-sensitive tests and scripts must not use bare recursive deletion without retry handling.
Files:
test/codex-manager-rotation-command.test.tstest/app-bind.test.tstest/runtime-current-account.test.tstest/codex-bin-wrapper.test.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-rotation-command.test.tstest/app-bind.test.tstest/runtime-current-account.test.tstest/codex-bin-wrapper.test.ts
docs/reference/**/*.md
📄 CodeRabbit inference engine (docs/STYLE_GUIDE.md)
New flags/settings/paths must be reflected in
docs/reference/*
docs/reference/**/*.md: Keep command, API, error-contract, settings, and storage-path details in the canonical reference documentation.
Document compatibility aliases (codex multi auth,codex multi-auth, andcodex multiauth) only in command-reference, troubleshooting, or migration sections.
Files:
docs/reference/storage-paths.md
docs/{index.md,getting-started.md,faq.md,architecture.md,features.md,configuration.md,troubleshooting.md,privacy.md,upgrade.md}
📄 CodeRabbit inference engine (docs/DOCUMENTATION.md)
Keep the listed public documentation pages as the canonical sources for operator onboarding, FAQ, architecture, features, configuration, troubleshooting, privacy, and upgrades.
Files:
docs/configuration.mddocs/privacy.md
lib/**/*.ts
📄 CodeRabbit inference engine (lib/AGENTS.md)
lib/**/*.ts: Route all public exports throughlib/index.tsor documented package subpaths.
Keep module dependencies acyclic and preserve the layeringtypes/constants → storage → accounts → runtime → manager/CLI; lower layers must not import higher layers.
Preserve runtime rotation pass-through semantics except for intentionally changed auth or provider headers.
Deduplicate emails usingnormalizeEmailKey(), which trims and lowercases the email.
Use classes for state requiring multiple independent instances or dependency injection, includingAccountManager,CircuitBreaker,SessionAffinityStore, and theCodexErrorhierarchy. Reserve module-level state for genuinely process-global concerns and provide a test reset helper for such state.
Never import fromdist/in source tests or library code.
Never suppress type errors.
Never patch official Codex application binaries for desktop routing.
Never use bare recursive cleanup in Windows-sensitive paths without retry handling.
Files:
lib/codex-manager/commands/rotation.tslib/runtime/runtime-current-account.tslib/runtime/app-bind.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/commands/rotation.tslib/runtime/runtime-current-account.tslib/runtime/app-bind.ts
lib/{runtime-rotation-proxy.ts,runtime/**/*.ts}
📄 CodeRabbit inference engine (lib/AGENTS.md)
Runtime rotation must fail open to normal official Codex forwarding when startup helpers are unavailable.
Files:
lib/runtime/runtime-current-account.tslib/runtime/app-bind.ts
lib/runtime/**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Do not patch official Codex app binaries; use the reversible app-bind or launcher-helper mechanisms instead.
Files:
lib/runtime/runtime-current-account.tslib/runtime/app-bind.ts
test/**/codex-bin-wrapper.test.ts
📄 CodeRabbit inference engine (test/AGENTS.md)
Test bin wrapper lazy-load and missing dist handling with concurrent invocations in codex-bin-wrapper.test.ts
Files:
test/codex-bin-wrapper.test.ts
scripts/codex.js
📄 CodeRabbit inference engine (AGENTS.md)
Keep
codex-multi-auth-codexauth commands local, but forward non-auth commands to the official Codex CLI; do not reimplement general Codex commands.
Files:
scripts/codex.js
scripts/**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
Windows-sensitive cleanup and write operations must retry transient
EBUSY,EPERM, andENOTEMPTYfailures where applicable.
Files:
scripts/codex.js
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:05:55.408Z
Learning: Runtime configuration source precedence must be: existing `CODEX_MULTI_AUTH_CONFIG_PATH`, valid unified-settings `pluginConfig`, legacy compatibility files, then `DEFAULT_PLUGIN_CONFIG`; environment variables override individual runtime settings afterward.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:05:55.408Z
Learning: A set-but-missing `CODEX_MULTI_AUTH_CONFIG_PATH` must be ignored during loading but remain the save target when the environment variable is set.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:05:55.408Z
Learning: When `CODEX_HOME` is non-default, multi-auth must resolve strictly to `$CODEX_HOME/multi-auth` and must not scan other roots for existing account pools.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:05:55.408Z
Learning: The per-invocation account pin from `--account` or `CODEX_MULTI_AUTH_FORCE_ACCOUNT` must be ephemeral, take precedence over the environment variable, never modify the persisted switch pin, and fail hard when the runtime rotation proxy is disabled.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:05:55.408Z
Learning: The runtime rotation proxy must preserve request bodies and streaming responses, replace outbound authorization with the selected managed account, remove hop-by-hop/private metadata headers and stale decoded `content-encoding`, and return a structured pool-exhaustion error when no account is available.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:05:55.408Z
Learning: OAuth token revocation responses must be returned directly instead of rotating to another account; the affected account receives the configured token-invalidation cooldown.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:05:55.408Z
Learning: Package install scripts must remain side-effect-free: postinstall may print a short notice but must not perform setup, npm installation, or update commands.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:05:55.408Z
Learning: First-run desktop self-heal must run only for durable global installs, with setup state recorded at `~/.codex/multi-auth/first-run-setup.json`; `npx` and project-local installs must not consume the marker.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:05:55.408Z
Learning: The wrapper may perform a best-effort daily npm version check, but it must only print a manual upgrade notice and must never execute npm install or update commands automatically.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:05:55.408Z
Learning: Persisted or rewritten Codex configuration must use `cli_auth_credentials_store = "file"` unless explicitly opted out through `CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE=0`.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:06:24.959Z
Learning: Persist canonical settings in `~/.codex/multi-auth/settings.json` with top-level `version`, `dashboardDisplaySettings`, and `pluginConfig` fields.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:06:24.959Z
Learning: Keep runtime rotation controls separated by layer: persisted settings, per-process overrides, wrapper app-helper environment, and packaged app-bind environment.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:06:24.959Z
Learning: Treat backup/WAL artifacts created during storage writes and recovery as normal temporary safety behavior.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:06:31.830Z
Learning: Data handling must comply with OpenAI's Terms of Use and Privacy Policy.
📚 Learning: 2026-06-04T06:14:18.093Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/scheduling-strategy-config.test.ts:1-1
Timestamp: 2026-06-04T06:14:18.093Z
Learning: In ndycode/codex-multi-auth, do not flag explicit imports from "vitest" (e.g., describe, it, expect, beforeEach/afterEach, etc.) in test files as issues—even if the Vitest config sets `globals: true`. The repo’s established convention is to keep these imports for consistency with neighboring tests; removing them would make files outliers.
Applied to files:
test/codex-manager-rotation-command.test.tstest/app-bind.test.tstest/runtime-current-account.test.tstest/codex-bin-wrapper.test.ts
📚 Learning: 2026-06-04T06:14:24.975Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/runtime-rotation-proxy.test.ts:2478-2491
Timestamp: 2026-06-04T06:14:24.975Z
Learning: In ndycode/codex-multi-auth test files (e.g. `test/*.test.ts`), when creating V3 storage fixtures for accounts, it’s an intentional convention to use `as never` for deliberately minimal stored-account objects that only include `refreshToken`, `addedAt`, and `lastUsed`. Do not treat `as never` here as a type-safety problem: optional/other fields are expected to be populated by the runtime during execution, and the cast is used solely to keep the fixture minimal and consistent across existing tests.
Applied to files:
test/codex-manager-rotation-command.test.tstest/app-bind.test.tstest/runtime-current-account.test.tstest/codex-bin-wrapper.test.ts
🪛 ast-grep (0.45.1)
test/runtime-current-account.test.ts
[warning] 547-557: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFile(
join(tempDir, runtime-rotation-app-helper.${process.pid}.json),
JSON.stringify({
kind: "codex-app-runtime-rotation-helper",
state: "running",
pid: process.pid,
lastAccountId: "acc_live",
updatedAt: now - 30_000,
}),
"utf8",
)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
[warning] 574-584: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFile(
join(tempDir, "runtime-rotation-app-helper.99999998.json"),
JSON.stringify({
kind: "codex-app-runtime-rotation-helper",
state: "idle-timeout",
pid: 99999998,
lastAccountId: "acc_older",
updatedAt: now - 60_000,
}),
"utf8",
)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
[warning] 585-595: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFile(
join(tempDir, "runtime-rotation-app-helper.99999999.json"),
JSON.stringify({
kind: "codex-app-runtime-rotation-helper",
state: "stopped",
pid: 99999999,
lastAccountId: "acc_newer",
updatedAt: now - 10_000,
}),
"utf8",
)
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
lib/codex-manager/commands/rotation.ts
[warning] 557-560: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
^${basePattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\d+\\.json$,
"i",
)
Note: [CWE-1333] Inefficient Regular Expression Complexity
(regexp-from-variable)
lib/runtime/runtime-current-account.ts
[warning] 160-163: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
^${basePattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\d+\\.json$,
"i",
)
Note: [CWE-1333] Inefficient Regular Expression Complexity
(regexp-from-variable)
lib/runtime/app-bind.ts
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] 1513-1516: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
^${helperStatusPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\d+\\.json$,
"i",
)
Note: [CWE-1333] Inefficient Regular Expression Complexity
(regexp-from-variable)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
test/codex-bin-wrapper.test.ts
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type SpawnSyncReturns, spawn, spawnSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type SpawnSyncReturns, spawn, spawnSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type SpawnSyncReturns, spawn, spawnSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type SpawnSyncReturns, spawn, spawnSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type SpawnSyncReturns, spawn, spawnSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type SpawnSyncReturns, spawn, spawnSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
scripts/codex.js
[warning] 4043-4046: Detects non-literal values in regular expressions
Context: new RegExp(
^${baseName.replace(/\.json$/i, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.(\\d+)\\.json$,
"i",
)
Note: [CWE-1333] Inefficient Regular Expression Complexity (ReDoS via non-literal RegExp).
(detect-non-literal-regexp)
🪛 OpenGrep (1.26.0)
scripts/codex.js
[ERROR] 4079-4079: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
[ERROR] 4079-4079: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
🔇 Additional comments (30)
scripts/codex.js (10)
3-3: LGTM!
88-100: LGTM!
3792-3806: LGTM!
3934-3969: LGTM!
4034-4082: LGTM!Also applies to: 4091-4108
4188-4232: LGTM!
4255-4259: LGTM!
4332-4335: LGTM!
4348-4360: LGTM!
4450-4474: LGTM!lib/runtime/runtime-current-account.ts (3)
1-1: LGTM!Also applies to: 127-129
156-176: LGTM!
178-198: LGTM!lib/codex-manager/commands/rotation.ts (2)
1-1: LGTM!Also applies to: 518-520, 550-575
650-655: LGTM!test/codex-bin-wrapper.test.ts (5)
692-704: LGTM!Also applies to: 3084-3090
2848-2870: LGTM!Also applies to: 3598-3602
3160-3236: LGTM!
3311-3343: LGTM!Also applies to: 3354-3363
3344-3353: 🎯 Functional Correctnessremove the parse-error concern.
test/codex-bin-wrapper.test.ts:3346andtest/codex-bin-wrapper.test.ts:3349each contain one valid type assertion.> Likely an incorrect or invalid review comment.test/codex-manager-rotation-command.test.ts (1)
449-504: LGTM!test/runtime-current-account.test.ts (1)
545-598: LGTM!docs/reference/storage-paths.md (1)
42-42: LGTM!docs/configuration.md (1)
76-76: LGTM!docs/development/CONFIG_FIELDS.md (1)
270-272: LGTM!docs/development/ARCHITECTURE.md (1)
200-200: LGTM!lib/runtime/app-bind.ts (2)
4-4: LGTM!Also applies to: 1648-1648
1530-1599: the per-helper decision logic reads correctly; identity gating holds.traced each branch: the legacy exact name cannot match the
\.\d+\.jsonpattern, so no candidate is processed twice. a setidentityTokenwith a missing or mismatched owner file preserves the status instead of signalling a PID this process cannot prove it owns. the alive-then-stop sequence atlib/runtime/app-bind.ts:1555andlib/runtime/app-bind.ts:1565is TOCTOU-safe becausestopRuntimeRotationAppHelperProcessre-verifies start time, arg, andscriptPathatlib/runtime/app-bind.ts:1288-1310before it signals.one gap:
test/app-bind.test.tscovers only the dead per-PID helper. see the comment on that file for the missing cases.AGENTS.md (1)
139-139: LGTM!docs/privacy.md (1)
33-33: LGTM!Also applies to: 91-91, 118-118
Shared per-PID status discovery moves next to its filename constant (listRuntimeHelperStatusPaths) and all three readers use it; rotation status derives selection and live count from one scan; only "running" counts as running so max-lifetime/error stamps read as terminal. The helper's identity probe is async and single-flight so a wedged ps stalls a background probe, never the proxy event loop. Metadata deletions retry transient Windows locks; the launch-path sweep memoizes identity probes per PID and caps them per sweep. app-bind unbind logs when it cannot enumerate per-PID files and retries the readdir. Tests: multi-helper and ownership-preservation unbind cases, publish-rate regression, sweep retry regression, max-lifetime status case, POSIX gate plus a Windows companion for the identity-unavailable degradation. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FW2tPyLdcRXrnGsYVVJeEj
|
All twelve findings triaged — eleven accepted and fixed, one declined with reasoning. Pushed as ✅ accepted
❌ declined: seeding hundreds of stale files to test sweep boundednessThe 579-file case that motivates the concern never probes |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@test/codex-bin-wrapper.test.ts`:
- Around line 3436-3467: Update the retry test around
withSynchronousFileOperationRetry to explicitly document its four-attempt
contract and that two injected EBUSY failures require the third deletion
attempt. Add a deterministic Windows regression scenario covering two stale
metadata files while accounting for the process-wide failure counter, ensuring
cleanup succeeds consistently regardless of file ordering.
🪄 Autofix
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 Plus
Run ID: b9ce91e6-d362-481c-b6ab-957f0b4371ee
📒 Files selected for processing (10)
docs/development/ARCHITECTURE.mddocs/reference/storage-paths.mdlib/codex-manager/commands/rotation.tslib/runtime-constants.tslib/runtime/app-bind.tslib/runtime/runtime-current-account.tsscripts/codex.jstest/app-bind.test.tstest/codex-bin-wrapper.test.tstest/codex-manager-rotation-command.test.ts
📜 Review details
🧰 Additional context used
📓 Path-based instructions (22)
docs/**/*.md
📄 CodeRabbit inference engine (docs/STYLE_GUIDE.md)
docs/**/*.md: User-facing documentation should follow the page template: Title and one-line lead, Quick path commands, Core operational workflow, Troubleshooting or failure handling, and Related links
Use short sections and scan-friendly tables in documentation where they improve clarity
Prefer direct, actionable language in documentation
Use runnable command examples in documentation
Explain expected outcomes after critical commands in documentation
Keep terminology consistent with runtime names in documentation
Avoid speculative language when behavior is deterministic in documentation
Put the user problem in the first paragraph before implementation detail
Use descriptive page titles such ascodex-multi-auth Featuresinstead of generic titles on public docs
Do not repeat keyword lists in every section; search terms should appear only where they help a developer understand the page
Canonical command family iscodex-multi-auth ...
Canonical runtime root is~/.codex/multi-auth
Runtime rotation must be described as default-on unless the release policy changes
Legacy command/path references belong only in migration contexts in documentation
Compatibility aliases (codex multi auth,codex multi-auth,codex multiauth) belong only in command reference, troubleshooting, or migration contexts
Keep command flags aligned with runtime usage text in documentation
Avoid non-runnable command snippets in documentation
Avoid conflicting path guidance across documentation
Avoid legacy-first onboarding language in documentationOrganize repository documentation according to the defined layers: product entry, user operations, reference, and development.
docs/**/*.md: Do not describecodex-multi-authas replacing@openai/codexor publishing the globalcodexbinary; preserve the official CLI's ownership ofcodex.
Usecodex-multi-authfor account management, and reservecodex-multi-auth-codexormcodexfor intentionally forwarding official Codex commands th...
Files:
docs/development/ARCHITECTURE.mddocs/reference/storage-paths.md
docs/development/**/*.md
📄 CodeRabbit inference engine (docs/DOCUMENTATION.md)
Keep internal architecture, configuration flow, repository ownership, testing, parity, metadata, and audit guidance in development documentation.
Prefer current architecture and reference documentation over historical plans and audit snapshots when describing the present system.
Files:
docs/development/ARCHITECTURE.md
docs/development/**/*
📄 CodeRabbit inference engine (docs/development/CONFIG_FLOW.md)
docs/development/**/*: Resolve the runtime root directory in this order:CODEX_MULTI_AUTH_DIR; explicit non-defaultCODEX_HOME/multi-auth; existing account-storage roots underCODEX_HOMEor~/.codex; canonical~/.codex/multi-auth; and legacy paths only when storage signals exist.
ReaddashboardDisplaySettingsandpluginConfigfromsettings.json, while preserving legacy compatibility loading and migration.
ResolvepluginConfigvalues using this precedence: existingCODEX_MULTI_AUTH_CONFIG_PATHfile, valid unifiedsettings.jsonconfiguration, legacy compatibility configuration, thenDEFAULT_PLUGIN_CONFIG; apply environment-variable overrides afterward.
Ignore a configured but nonexistentCODEX_MULTI_AUTH_CONFIG_PATHduring loading, but create it on the first save while the variable remains set.
Resolve dashboard display values from persisteddashboardDisplaySettings, followed by normalization and fallback defaults.
Resolve account storage by selecting the root directory, using the global accounts file by default, using a project-namespaced path when project-scoped mode is active, and attempting applicable legacy project-file migration.
Normalize standalonecodex-multi-authbare subcommands toauth ...before dispatch; normalize wrapper aliases; run auth-manager commands locally; forward out-of-scope wrapper commands to the official Codex CLI.
For forwarded request-bearing commands, honor runtime rotation: resolveCODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY, thenpluginConfig.codexRuntimeRotationProxy, which defaults to enabled.
When rotation is enabled for a requesting command, use a per-process-token loopback Responses proxy, a temporary shadowCODEX_HOME, and a rewrittenconfig.toml; synchronize refreshed official Codex state on exit and remove the shadow home.
The runtime proxy must select or refresh managed accounts and rotate on rate-limit, authentication, network, or server failures before streaming begins.
The plugin host m...
Files:
docs/development/ARCHITECTURE.md
docs/development/**/*.{md,mdx}
📄 CodeRabbit inference engine (docs/development/TESTING.md)
When documentation changes, verify every command snippet is runnable, path references match runtime modules, cross-links are valid, and the feature matrix matches implemented features.
Files:
docs/development/ARCHITECTURE.md
docs/**/*.{md,mdx}
📄 CodeRabbit inference engine (docs/troubleshooting.md)
Document that
codex-multi-auth-codexis the optional forwarding wrapper, whilecodex-multi-authis the canonical account-manager command family; the package does not publish a globalcodexbinary.Document the canonical command names, runtime paths, configuration precedence, storage migration behavior, and upgrade procedures consistently across the referenced documentation.
Files:
docs/development/ARCHITECTURE.mddocs/reference/storage-paths.md
docs/**/*
📄 CodeRabbit inference engine (docs/configuration.md)
docs/**/*: Keep the recommended defaults enabled for menu auto-fetch limits, menu sorting, live account synchronization, session affinity, proactive refresh guarding, and preemptive quota handling.
Validate effective configuration usingcodex-multi-auth status,list,check, andforecast --livewhen reviewing or troubleshooting configuration changes.
Files:
docs/development/ARCHITECTURE.mddocs/reference/storage-paths.md
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/development/ARCHITECTURE.mddocs/reference/storage-paths.md
test/**/*.test.ts
📄 CodeRabbit inference engine (test/AGENTS.md)
test/**/*.test.ts: Write Vitest test suites with globals enabled (describe, it, expect)
Maintain 80%+ coverage threshold across statements, branches, functions, and lines
Use removeWithRetry() for Windows filesystem cleanup instead of bare fs.rm to handle EBUSY, EPERM, and ENOTEMPTY errors
Do not rely on dist/ in tests; use source files instead
Do not skip tests without justification
Relax lint rules for test files as configured in eslint.config.js
Files:
test/codex-manager-rotation-command.test.tstest/app-bind.test.tstest/codex-bin-wrapper.test.ts
**/*.{js,ts,mjs,cjs}
📄 CodeRabbit inference engine (README.md)
**/*.{js,ts,mjs,cjs}: Do not publish or replace a globalcodexbinary; official OpenAI installation paths must retain ownership of thecodexcommand.
Keep OAuth credentials local and restrict runtime rotation and local bridges to loopback interfaces.
Require hashed local client tokens to protect the optional loopback bridge.
Responsesbackground: truecompatibility must remain opt-in; requests using it must use statefulstore=truerouting rather than statelessstore=falserouting.
Never run npm install or update commands automatically; only display a manual upgrade notice when appropriate.
Experimental synchronization and backup flows must be non-destructive by default: preview before applying sync, preserve destination-only accounts, and fail safely on backup filename collisions.
Keep account storage project-scoped under the configured multi-auth root when operating in repo-specific workflows.
Files:
test/codex-manager-rotation-command.test.tslib/runtime-constants.tslib/runtime/app-bind.tstest/app-bind.test.tslib/codex-manager/commands/rotation.tsscripts/codex.jslib/runtime/runtime-current-account.tstest/codex-bin-wrapper.test.ts
**/*.{ts,js}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,js}: Use ESM-only modules; the package is configured with"type": "module"and targets Node.js >= 18.17.
Do not useas any,@ts-ignore, or@ts-expect-error.
Files:
test/codex-manager-rotation-command.test.tslib/runtime-constants.tslib/runtime/app-bind.tstest/app-bind.test.tslib/codex-manager/commands/rotation.tsscripts/codex.jslib/runtime/runtime-current-account.tstest/codex-bin-wrapper.test.ts
test/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Windows-sensitive tests and cleanup helpers must exercise retry handling for transient filesystem locks and failures.
Files:
test/codex-manager-rotation-command.test.tstest/app-bind.test.tstest/codex-bin-wrapper.test.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-rotation-command.test.tstest/app-bind.test.tstest/codex-bin-wrapper.test.ts
lib/**/*.ts
📄 CodeRabbit inference engine (lib/AGENTS.md)
lib/**/*.ts: Route all public exports throughlib/index.tsor documented package subpaths.
Keep module dependencies acyclic and preserve the layeringtypes/constants → storage → accounts → runtime → manager/CLI; lower layers must not import higher layers.
Preserve runtime rotation pass-through semantics except for intentionally changed auth or provider headers.
Deduplicate emails usingnormalizeEmailKey(), which trims and lowercases the email.
Use classes for state requiring multiple independent instances or dependency injection, includingAccountManager,CircuitBreaker,SessionAffinityStore, and theCodexErrorhierarchy. Reserve module-level state for genuinely process-global concerns and provide a test reset helper for such state.
Never import fromdist/in source tests or library code.
Never suppress type errors.
Never patch official Codex application binaries for desktop routing.
Never use bare recursive cleanup in Windows-sensitive paths without retry handling.
Files:
lib/runtime-constants.tslib/runtime/app-bind.tslib/codex-manager/commands/rotation.tslib/runtime/runtime-current-account.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/runtime-constants.tslib/runtime/app-bind.tslib/codex-manager/commands/rotation.tslib/runtime/runtime-current-account.ts
lib/{runtime-rotation-proxy.ts,runtime/**/*.ts}
📄 CodeRabbit inference engine (lib/AGENTS.md)
Runtime rotation must fail open to normal official Codex forwarding when startup helpers are unavailable.
Files:
lib/runtime/app-bind.tslib/runtime/runtime-current-account.ts
lib/runtime/**/*.ts
📄 CodeRabbit inference engine (AGENTS.md)
Keep runtime rotation default-on through
codexRuntimeRotationProxy, while preserving the documented opt-out behavior.
Files:
lib/runtime/app-bind.tslib/runtime/runtime-current-account.ts
lib/runtime/app-bind.ts
📄 CodeRabbit inference engine (AGENTS.md)
Do not patch official Codex app binaries; use the reversible app-bind or launcher-helper mechanism instead.
Files:
lib/runtime/app-bind.ts
docs/reference/**/*.md
📄 CodeRabbit inference engine (docs/STYLE_GUIDE.md)
New flags/settings/paths must be reflected in
docs/reference/*
docs/reference/**/*.md: Keep command, API, error-contract, settings, and storage-path details in the canonical reference documentation.
Document compatibility aliases (codex multi auth,codex multi-auth, andcodex multiauth) only in command-reference, troubleshooting, or migration sections.
Files:
docs/reference/storage-paths.md
scripts/codex*.js
📄 CodeRabbit inference engine (AGENTS.md)
Do not bypass the official Codex CLI by reimplementing general Codex commands in the wrapper; local handling is limited to account/auth commands and other commands must be forwarded.
Files:
scripts/codex.js
scripts/**/*.js
📄 CodeRabbit inference engine (AGENTS.md)
Windows-sensitive cleanup and writes must retry transient
EBUSY,EPERM, andENOTEMPTYfailures where applicable; avoid bare recursive deletion.
Files:
scripts/codex.js
scripts/codex.js
📄 CodeRabbit inference engine (AGENTS.md)
Shadow
CODEX_HOMEhandling must preserve official Codex state, synchronize state back safely, and clean up locks and temporary provider configuration.
Files:
scripts/codex.js
test/**/codex-bin-wrapper.test.ts
📄 CodeRabbit inference engine (test/AGENTS.md)
Test bin wrapper lazy-load and missing dist handling with concurrent invocations in codex-bin-wrapper.test.ts
Files:
test/codex-bin-wrapper.test.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:06.380Z
Learning: Package installation scripts must remain side-effect-free; first-run durable CLI setup performs best-effort repair without blocking the requested command.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:06.380Z
Learning: Budget guards are intentionally soft under concurrency because evaluations use a pre-request ledger snapshot; concurrent requests may briefly overshoot.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Store account and session state locally under the configured runtime root; honor `CODEX_MULTI_AUTH_DIR` and `CODEX_MULTI_AUTH_CONFIG_PATH` overrides.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Do not add custom analytics, a project-owned remote database, or network calls outside required OAuth, backend/update, and listed GitHub endpoints.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Keep runtime rotation and the optional local bridge loopback-only. The bridge must expose only `/health`, `/v1/models`, and `/v1/responses`, and require a local bearer token by default.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Store local bridge client tokens as SHA-256 hashes rather than plaintext; show plaintext tokens only during creation or rotation.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Do not log prompts, authorization headers, raw sensitive account identifiers, or other sensitive payloads in usage and observability metadata.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Treat raw request and response body logs enabled by `CODEX_PLUGIN_LOG_BODIES=1` as sensitive data, and rotate or delete them as needed.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Ensure cleanup removes all multi-auth-owned data, including accounts, caches, leases, usage, backups, projects, app-bind state, logs, prompt caches, helper files, and configured override-root paths.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Use hashed email/account identity values where usage or account-policy metadata requires identity keys; do not persist raw sensitive account identifiers.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Keep local request metadata summaries free of prompts, authorization headers, and raw sensitive account IDs.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Ensure account pools, policies, routing profiles, budget guards, backups, and runtime metadata remain local to the configured runtime root.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:23.215Z
Learning: Usage of the project must comply with OpenAI’s Terms of Use and Privacy Policy.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: When `CODEX_HOME` is set to a non-default directory, resolve multi-auth storage strictly under `$CODEX_HOME/multi-auth` and do not scan the default `~/.codex/multi-auth` directory.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Treat `~/.codex/multi-auth` as project-owned storage, while `~/.codex/accounts.json`, `~/.codex/auth.json`, and `~/.codex/config.toml` remain official Codex CLI-owned files.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Never read from or write to the OS keychain or the `security` CLI; use the official Codex file-backed auth layout instead.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Reconcile the persisted top-level `cli_auth_credentials_store` value in `config.toml` to `"file"` during first-run setup, wrapper startup, and `doctor --fix`, while leaving profile-level assignments unchanged.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Preserve existing `config.toml` line endings and accept either TOML string form when recognizing `cli_auth_credentials_store = 'file'` or `"file"`; insert a missing top-level key before the first table.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Make first-run setup one-shot and concurrency-safe using an exclusive marker create; failures must be debug-logged without blocking the user command, and pre-v2 or unreadable markers must migrate by replaying only the auth-store step.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Use atomic writes and appropriate Windows `EPERM`/`EBUSY` retry handling for configuration and storage updates; if wrapper-startup reconciliation ultimately fails, swallow the error and continue because the per-invocation override protects that run.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Do not take locks for concurrent interactive TUI sessions; they operate directly on the canonical `CODEX_HOME` and must preserve stock concurrent-session behavior.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Exclude cache-like artifacts and `.reset-intent` markers from recovery candidates; suppress flagged-account backup recovery while the flagged reset marker remains present.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Named backup export names may contain only letters, numbers, `_`, and `-`; append `.json` when omitted, reject path separators, `..`, `.rotate.`, `.tmp`, and `.wal`, and do not overwrite existing files except through an explicit lower-level force path.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Keep the local bridge loopback-only, expose only `/health`, `/v1/models`, and `/v1/responses`, and persist token hashes rather than plaintext tokens; show plaintext tokens only during explicit create or rotate commands.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Enforce account pause and drain policies at selection time through `evaluateRuntimePolicy`, excluding blocked accounts from hybrid rotation.
Learnt from: CR
Repo: ndycode/codex-multi-auth
Timestamp: 2026-08-11T18:28:33.293Z
Learning: Run `npm run build` and the targeted unified-settings, storage-recovery-paths, and storage-flagged tests when validating backup or restore changes.
📚 Learning: 2026-06-04T06:14:18.093Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/scheduling-strategy-config.test.ts:1-1
Timestamp: 2026-06-04T06:14:18.093Z
Learning: In ndycode/codex-multi-auth, do not flag explicit imports from "vitest" (e.g., describe, it, expect, beforeEach/afterEach, etc.) in test files as issues—even if the Vitest config sets `globals: true`. The repo’s established convention is to keep these imports for consistency with neighboring tests; removing them would make files outliers.
Applied to files:
test/codex-manager-rotation-command.test.tstest/app-bind.test.tstest/codex-bin-wrapper.test.ts
📚 Learning: 2026-06-04T06:14:24.975Z
Learnt from: ndycode
Repo: ndycode/codex-multi-auth PR: 510
File: test/runtime-rotation-proxy.test.ts:2478-2491
Timestamp: 2026-06-04T06:14:24.975Z
Learning: In ndycode/codex-multi-auth test files (e.g. `test/*.test.ts`), when creating V3 storage fixtures for accounts, it’s an intentional convention to use `as never` for deliberately minimal stored-account objects that only include `refreshToken`, `addedAt`, and `lastUsed`. Do not treat `as never` here as a type-safety problem: optional/other fields are expected to be populated by the runtime during execution, and the cast is used solely to keep the fixture minimal and consistent across existing tests.
Applied to files:
test/codex-manager-rotation-command.test.tstest/app-bind.test.tstest/codex-bin-wrapper.test.ts
🪛 ast-grep (0.45.1)
lib/runtime-constants.ts
[warning] 26-29: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(
^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\d+\\.json$,
"i",
)
Note: [CWE-1333] Inefficient Regular Expression Complexity
(regexp-from-variable)
lib/runtime/app-bind.ts
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
test/codex-bin-wrapper.test.ts
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type SpawnSyncReturns, spawn, spawnSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type SpawnSyncReturns, spawn, spawnSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { type SpawnSyncReturns, spawn, spawnSync } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(detect-child-process-typescript)
🔇 Additional comments (11)
scripts/codex.js (1)
3899-3954: LGTM!Also applies to: 3974-4013, 4060-4095, 4104-4191, 4318-4345
docs/development/ARCHITECTURE.md (1)
279-279: LGTM!lib/runtime-constants.ts (1)
1-1: LGTM!Also applies to: 12-37
lib/runtime/runtime-current-account.ts (1)
5-5: LGTM!Also applies to: 156-166
lib/codex-manager/commands/rotation.ts (1)
33-33: LGTM!Also applies to: 549-591, 623-651, 705-714, 737-739
lib/runtime/app-bind.ts (2)
13-13: LGTM!Also applies to: 1510-1533
1652-1652: 🩺 Stability & Availabilityremove this concern;
unlinkIfExistsalready retries windows lock errors.lib/runtime/app-bind.ts:302wrapsunlink(path)withwithFileOperationRetry, andtest/app-bind-io-retry.test.ts:182covers transient cleanup failures.> Likely an incorrect or invalid review comment.test/app-bind.test.ts (1)
1168-1222: LGTM!Also applies to: 1224-1267
docs/reference/storage-paths.md (1)
162-162: LGTM!test/codex-bin-wrapper.test.ts (1)
3245-3247: LGTM!Also applies to: 3278-3310, 3401-3431
test/codex-manager-rotation-command.test.ts (1)
506-530: LGTM!
…er-independent Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01FW2tPyLdcRXrnGsYVVJeEj
|
Accepted — pushed as |
Two of these were documentation this PR had itself made wrong: moving the metadata sweep after `spawn()` invalidated a claim in ARCHITECTURE.md and a comment in the wrapper test that both still said "before". Behaviour: - `ps` does not exist on Windows, so `readProcessStartTimeMs` and its async twin could only ever fail there — once per launcher launch and up to `probeBudget` times per sweep, each one a process spawn that learns nothing. Both short-circuit on win32 now. Windows runs owner liveness on bare `kill(pid, 0)` and the 24h ceiling is what bounds a leak there; the "degraded check" row says so instead of implying it is rare. - The owner-identity recheck was pinned at 60s. `lastIdentityVerdict` starts optimistic, so the first tick reports the owner alive while the probe is in flight — deliberate against a 12h timeout, but the lifecycle tests compress the window to 250ms, where a 60s recheck is longer than the whole thing under test and the flip came down to probe timing. The interval now scales off the resolved idle/detached window. Production is unchanged: both defaults are hours. - `mapWithConcurrency` retired a runner on an `undefined` item rather than skipping it. Unreachable today — `items` is `string[]` — but the failure mode it guards is "helpers left running while the user is told the app was unbound", so only running past the end ends a runner. - The orphan owner pass preserved a live-PID owner file without a word, while every other preserve in that function warns. Telling "a helper is starting right now" from "the PID was recycled" needs the recorded-start-time comparison the launcher sweep does and unbind has no equivalent of; that stays a scope decision, but not a silent one. Fixtures: - Windows allocates PIDs from a pool rather than a monotonic counter, so `withDeadPid`'s "a just-exited PID is not reused" did not hold there — and its callers assert dead-PID cleanup on every platform. Deadness is re-asserted immediately before the PID is handed over, turning a rare Windows-only flake in a cleanup test into an immediate fixture error. - The parent end of the stdin pipe is destroyed on reap; `exit` fires before stdio teardown and some fixtures hold 16 at once. - The hand-rolled spawn/SIGKILL/poll copy in the wrapper test uses `withDeadPid`, which waits on `exit` instead of polling. - The EPERM owner-liveness test is win32-skipped: it sources the owner start time from `ps`, so on Windows the env var was empty, the identity branch never engaged, and it exercised bare liveness under a name claiming otherwise. - Nested `withDeadPid` scopes flattened via `withDeadPids`. Coverage: - `UNBIND_HELPER_CONCURRENCY` is exported and observed. With three records any pool width behaved identically, so an edit to `Infinity` would have shipped green; a fixture now runs 2x the bound in live helper records through unbind and measures peak in-flight at the `verifyProcessIdentity` seam. - test/app-helper-selection.test.ts covers the four selector predicates directly — non-positive/fractional PIDs, every terminal state, the staleness boundary either side by 1ms, null `updatedAt`, `startedAt` inside and outside the clock tolerance, recency in both input orders. - The staleness window is pinned to the wrapper's heartbeat. The wrapper cannot import from `lib/`, so nothing linked the two numbers; the test reads the constant out of `scripts/codex.js` and asserts ten heartbeats still fit inside the window. - A permanently locked metadata file is asserted survivable rather than assumed: the launch still exits 0 and the file waits for the next sweep. Not taken: a cache for the synchronous helper-status scan. It is pre-existing (#664 introduced the per-PID scan) and unchanged here; the menu loop blocks on user input between iterations, and the accumulation that would make it hurt is what this PR bounds. A time-based cache would show stale account state in the UI it is meant to speed up. Still uncovered: the mtime guard's negative path — a file replaced between classification and deletion. Forcing a write into that window needs another production test hook, which is too high a price for a microseconds-wide race. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_015b4Lew3oHNEYmTZtg7zEWz
The detached reap added in ndycode#665 could kill a live `codex app` session. `codex app` relies on the detach grace rather than an explicit `detachOnExit`, so its launcher exits inside the grace window and the helper's owner is dead from the first tick. From then on the only thing standing between the desktop app and a dead proxy was `countOpenConnections() === 0` — and the proxy never sets `server.keepAliveTimeout`, so Node closes idle client sockets after its 5s default. A user who stops typing for the length of the detached window has zero sockets and no new requests, so the helper exits `owner-gone`, and the next message gets ECONNREFUSED against a dead localhost port with nothing left to restart it. Pre-ndycode#665 that session survived for the full 12h idle timeout. Gate the reap on the helper having *never* served a request. Every leaked helper in the ndycode#663 report had `totalRequests: 0`, so the leak is entirely a never-served phenomenon and the narrower gate closes it in full; a helper that served anything was genuinely handed off and falls back to the idle timeout and the 24h lifetime ceiling, which is where it sat before the detached window existed. Two more lifecycle fixes in the same tick: - The owner verdict is now three-valued. "No owner PID was recorded" and "the owner is confirmed dead" are different facts, and collapsing them into one `false` started the detached clock on the first tick for any helper launched without an owner PID — invoked directly, which is the documented reproduction in ndycode#663, or spawned by a pre-upgrade launcher — and reaped it silently 15 minutes later. `unknown` fires neither branch, which is what the pre-ndycode#664 `ownerPid && isAlive(ownerPid)` guard did. - The status heartbeat now accounts for the detached window. `publishToken` zeroes `idleExpiresAt`, so the published deadline only catches up on a heartbeat; pinned to the idle window alone, `rotation status` kept advertising a 12h deadline for a helper seconds from exiting, and under a short DETACHED_IDLE_MS override it never caught up at all. Also in this commit, both from the same review pass: - The metadata sweep runs after the helper spawn instead of before it. It is synchronous and unbounded — readdir, a readFileSync per live candidate, rmSync with a blocking backoff, bounded `ps` probes — and the state it cleans up is exactly the state that makes it slow, so it sat in front of `codex app` and TUI startup. Nothing about spawning depends on it. The launch timeout is armed after it either way. - Sweep deletions are guarded by an mtime re-check. Classifying a file as stale and deleting it are two moments, and a PID freed between them can be handed to a helper starting right now, which republishes that exact path before the delete lands. - The published wrapper's fault injectors need an explicit CODEX_MULTI_AUTH_TEST_FAULT_INJECTION=1 opt-in and a strict digits-only parse. `Number.parseInt` reads "2abc" as 2 and "1e3" as 1, so a value that was never meant to be a count could arm an injector in a user's install and silently defeat the first N metadata deletions of every sweep (ndycode#668). Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_015b4Lew3oHNEYmTZtg7zEWz
ndycode#664 moved helper status files to `runtime-rotation-app-helper.<pid>.json` and updated `docs/reference/storage-paths.md`, but the storage table in the README kept the pre-per-PID shared name. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_015b4Lew3oHNEYmTZtg7zEWz
Summary
Stops runtime rotation app helpers from leaking past their idle timeout, and
stops N helpers from trampling one shared status file. Closes #663.
Observed before the fix: 183 concurrent helpers, 5.58 GB RSS, 41 past the
12-hour idle timeout (oldest 33 h), refilling at 10–28/hour under ordinary
use; 701 orphaned owner files; one status file rewritten ~183×/s. All
defective sites date to
1bc40eb.What changed
scripts/codex.jskill(pid, 0)answers "does aprocess hold this integer", never "is this still my launcher" — and because
the idle deadline only ever moves forward, one recycled-PID false positive
per 12 h window makes a helper immortal. The launcher now states its
identity at spawn (PID + kernel start time via
CODEX_MULTI_AUTH_APP_ROTATION_OWNER_START_TIME_MS, captured withps -o lstart=underLC_ALL=C—lstartis locale-formatted and alocalized string can fail
Date.parse, which would silently disable thefix); the helper requires both halves to match and both always come from
the launcher's own capture, never an inherited environment value. It
re-verifies at most once a minute (a process spawn per 1 Hz tick would cost
more than it saves, and the
pscall carries a 2 s timeout so a wedgedread cannot hang the proxy's event loop); a failed re-read keeps the
previous verdict — under the process-table pressure this fix exists for,
forkitself can fail, and declaring a live owner dead would kill theproxy out from under an active session. Where no start time is known at
all (no
ps, or a pre-upgrade launcher) the check degrades to bareliveness. The EPERM-tolerance test now runs with a real matching start
time, so it covers the production configuration — EPERM through the
identity branch — and doubles as the false-positive guard: a matching
identity must keep a live owner's helper alive.
CODEX_MULTI_AUTH_APP_ROTATION_MAX_LIFETIME_MS,default 24 h,
0disables) sits beside the idle check, deliberatelyunconditional on activity: it exists for exactly the case where activity
accounting is wrong again, converting any future unbounded leak into a
bounded one.
runtime-rotation-app-helper.<pid>.json(mirroring the owner files),publishes on change plus a heartbeat of
min(60 s, idleTimeout)instead ofevery tick, and stamps a terminal state on exit. The
readyline'sstatusPathpoints at the per-PID file.exit (no post-mortem value); each launcher sweeps per-PID status/owner
files whose helper PID is dead — and the legacy shared status file once the
PID recorded inside it is dead — before spawning. A live
kill(pid, 0)isnot taken as proof of life here either: when the file records when its
helper started, a current PID-holder whose kernel start time is meaningfully
later cannot be that helper, so a recycled PID cannot shield a stale file
from every future sweep. Terminal status stamps therefore survive until the
next launch, long enough to be read without accumulating forever. First
launch on the affected machine swept 694 stale owner files.
lib/runtime/runtime-current-account.ts,lib/codex-manager/commands/rotation.tslive running helper, fall back to the freshest terminal stamp, and
still read the legacy un-suffixed path so a pre-upgrade helper stays
visible during the transition.
rotation statusappends(+N more running)when several live helpers exist — with one pinnedapp-server per account that is the normal state, and the old line implied
the arbitrary last writer was the only helper. Both keep their own
hardened local reader by the codebase's existing convention.
lib/runtime/app-bind.tscodex-multi-auth uninstall— now walks everyper-PID status file plus the legacy path through the same per-helper logic
the single shared file used to get: ownership verification (status/owner
identity-token agreement plus process-identity checks) gates every stop, so
unbind reaps each helper it can prove is one of ours and preserves — with a
warning — anything it cannot. Without this, moving status to per-PID files
would have made unbind silently stop nothing while reporting success.
Tests
Four new cases in
test/codex-bin-wrapper.test.ts, each verified to failwith its defect hand-reverted:
with a mismatched start time idles out on schedule; fails against the bare
kill(pid, 0)check.helper past the ceiling.
each reporting its own PID; the shared legacy path is never written.
launch — including a stale legacy status file.
The identity mechanism is mutation-verified: stubbing the start-time read to
null(the fully-disabled state) flips the mismatch test to failing, so thesuite pins that the read succeeds and is compared, not merely that the code
path exists. New unit coverage for the reader migration: per-PID preference
over a fresher-but-dead record, terminal-stamp fallback
(
test/runtime-current-account.test.ts), the(+N more running)status linewith a dead PID counted for nothing
(
test/codex-manager-rotation-command.test.ts), and per-PID unbind cleanup(
test/app-bind.test.ts).Two existing tests updated only in where they read status (per-PID glob
instead of the legacy path); the EPERM test gains the production start-time
env as described above; every pre-existing unit test in both reader suites
passes unchanged, which is the legacy-fallback working. No detach semantics
changed and the shutdown tests pass untouched.
Validation
npm run lint/npm run typecheck/npm run buildnpx vitest run test/codex-bin-wrapper.test.ts test/runtime-current-account.test.ts test/codex-manager-rotation-command.test.ts test/documentation.test.ts—only the pre-existing macOS flakes fail (the shim file-op retry timeout and
the Windows-path resolver trio, which fail intermittently on clean
mainunder full-file load as well)
owner files; a real helper carries the identity env and publishes its
per-PID status;
rotation statusreports the newest live helper with(+1 more running)across one pre-upgrade (legacy-file) and one new(per-PID) helper simultaneously; SIGTERM to a helper writes its terminal
stamp and removes its owner file.
Risk and rollback
Risk: low-medium. The reaper change strictly widens the conditions under
which a helper exits (identity mismatch, lifetime ceiling); it never keeps a
helper alive longer than today. The status migration keeps the legacy path
readable; the only consumers of the shared file in-tree are the two migrated
readers and
app-bind.ts(see above). On platforms withoutps, every newcheck degrades to today's behavior exactly.
Rollback: revert the commit. Per-PID status/owner files from the interim
are swept by any subsequent launcher (old code ignores them; they are small
and inert).
Two costs stated plainly: the launcher adds one synchronous
psspawn(~5 ms measured) to each CLI invocation that starts a helper, and
psisresolved via
PATHlike every other external binary the wrapper shells outto.
Known follow-ups (deliberately out of scope)
wrapper's child exits, not when the wrapper itself receives SIGTERM — so a
supervised restart that signals the wrapper strands its helper for the
idle window (now bounded at 12 h idle / 24 h ceiling rather than
immortal). Wrapper-level signal handlers are the real fix and deserve
their own change.
extra pipe FD from launcher to helper EOFs on launcher death, whatever the
cause — no PID, no reuse, no
ps. EOF must mean "start the idlecountdown", not "exit now" (the
codex apphandoff depends on helpersoutliving their launcher). The start-time check in this PR is the smaller,
mergeable version; happy to follow up with the handle if you want it.
past N) would be a second backstop; 183 concurrent helpers should never
have been reachable.
state === "running"+kill(pid, 0)whenpicking the live helper to display. A SIGKILLed helper whose PID is later
recycled can be reported as current until the next launcher sweep removes
its file (the sweep itself is identity-checked, so the file does not
survive past that). Threading start-time identity through the TypeScript
readers is mechanical but widens the diff; happy to follow up.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FW2tPyLdcRXrnGsYVVJeEj
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
the pr bounds runtime helper lifetime, adds launcher identity checks, and migrates helper telemetry from one shared file to per-pid records.
Confidence Score: 5/5
the pr appears safe to merge.
no blocking failure remains.
Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[launcher] --> B[sweep stale metadata] B --> C[spawn helper] C --> D[write per-pid owner metadata] C --> E[write per-pid status heartbeat] E --> F[status readers] D --> G[unbind ownership verification] E --> G G --> H[stop verified helpers] C --> I{idle or lifetime limit} I --> J[terminal status] J --> K[remove owner metadata]Reviews (3): Last reviewed commit: "test(codex): pin the sweep-retry test to..." | Re-trigger Greptile
Context used: