feat(codex): force a specific account per invocation with --account (#623) - #624
Conversation
…623) Add `codex-multi-auth-codex --account <index|email|id>` (and the equivalent CODEX_MULTI_AUTH_FORCE_ACCOUNT env var) to pin a single account for one forwarded Codex run, for users who keep separate account pools and want a specific invocation to use a specific account. Mechanism: the launcher resolves the selector to a 0-based index against the same scoped accounts pool the proxy loads, strips the launcher-only flag from the args forwarded to real Codex, and publishes CODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEX. The runtime rotation proxy consumes it as an ephemeral pin (`pinnedIndex = state.forcedAccountIndex ?? storageMeta.pinnedAccountIndex`), reusing the existing deterministic-pin path — so it never mutates the persisted `switch` pin and cannot leak across concurrent sessions (each invocation owns its own proxy instance). Fail-hard by design: an unavailable target yields the existing codex_pinned_account_unavailable error instead of spilling onto another account, and the wrapper exits non-zero (without launching Codex) when the selector does not resolve or the runtime rotation proxy is disabled. Tests: proxy-level (deterministic selection, fail-hard, normalizeForcedAccountIndex) and launcher-level (missing value, rotation disabled, out-of-range, strip + index publish). Docs updated (commands reference, configuration, CHANGELOG). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…623) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
📝 WalkthroughWalkthroughThis PR adds an ephemeral, per-invocation account-forcing capability to codex-multi-auth-codex via a new ChangesForce Account Per Invocation
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant CodexJS as scripts/codex.js
participant AccountsPool
participant RuntimeRotationProxy
User->>CodexJS: codex-multi-auth-codex --account <selector>
CodexJS->>CodexJS: strip --account, extract selector
CodexJS->>AccountsPool: load openai-codex-accounts.json
CodexJS->>CodexJS: resolve selector to 0-based index
alt runtime rotation proxy disabled
CodexJS-->>User: hard error, exit non-zero
else proxy enabled
CodexJS->>RuntimeRotationProxy: set CODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEX
RuntimeRotationProxy->>RuntimeRotationProxy: pinnedIndex = forcedAccountIndex ?? storageMeta.pinnedAccountIndex
CodexJS->>CodexJS: forward stripped args to real Codex
end
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
| const parsed = | ||
| typeof value === "number" ? value : Number.parseInt(value.trim(), 10); | ||
| if (!Number.isInteger(parsed) || parsed < 0) { |
There was a problem hiding this comment.
normalizeForcedAccountIndex uses Number.parseInt for string input, which silently truncates floats: normalizeForcedAccountIndex("1.5") returns 1 instead of null. the number-type path correctly rejects 1.5 via Number.isInteger, but the string path doesn't. CODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEX is always published as String(integer) by the launcher, so this only bites someone who sets the internal env var by hand — but the asymmetry is a test gap and could mask a stale/corrupt env value being silently accepted.
| const parsed = | |
| typeof value === "number" ? value : Number.parseInt(value.trim(), 10); | |
| if (!Number.isInteger(parsed) || parsed < 0) { | |
| const trimmed = typeof value === "number" ? null : value.trim(); | |
| const parsed = | |
| trimmed !== null | |
| ? Number.parseInt(trimmed, 10) | |
| : value; | |
| if ( | |
| trimmed !== null && | |
| Number.isInteger(parsed) && | |
| String(parsed) !== trimmed | |
| ) { | |
| return null; | |
| } | |
| if (!Number.isInteger(parsed) || parsed < 0) { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/runtime-rotation-proxy.ts
Line: 621-623
Comment:
`normalizeForcedAccountIndex` uses `Number.parseInt` for string input, which silently truncates floats: `normalizeForcedAccountIndex("1.5")` returns `1` instead of `null`. the number-type path correctly rejects `1.5` via `Number.isInteger`, but the string path doesn't. `CODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEX` is always published as `String(integer)` by the launcher, so this only bites someone who sets the internal env var by hand — but the asymmetry is a test gap and could mask a stale/corrupt env value being silently accepted.
```suggestion
const trimmed = typeof value === "number" ? null : value.trim();
const parsed =
trimmed !== null
? Number.parseInt(trimmed, 10)
: value;
if (
trimmed !== null &&
Number.isInteger(parsed) &&
String(parsed) !== trimmed
) {
return null;
}
if (!Number.isInteger(parsed) || parsed < 0) {
```
How can I resolve this? If you propose a fix, please make it concise.| if (arg === "--account") { | ||
| sawFlag = true; | ||
| const next = args[index + 1]; | ||
| if (typeof next !== "string") { | ||
| return { | ||
| selector: null, | ||
| strippedArgs: args, | ||
| error: | ||
| "codex-multi-auth: --account requires a value (account index, email, or account id).", | ||
| }; | ||
| } | ||
| selector = next; | ||
| index += 1; |
There was a problem hiding this comment.
--account followed by a flag-looking value is silently consumed
typeof next !== "string" only guards against end-of-args (undefined). if a user mistypes codex-multi-auth-codex --account --model gpt-5-codex, the string "--model" is swallowed as the account selector and "gpt-5-codex" lands in strippedArgs as a bare positional — the forwarded codex invocation then sees gpt-5-codex as a positional argument rather than a flag value. the resulting error ("did not match any configured account") is confusing because the real mistake is a missing account value. adding a check like if (next.startsWith("--")) before consuming it would surface the right error message.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/codex.js
Line: 332-344
Comment:
**`--account` followed by a flag-looking value is silently consumed**
`typeof next !== "string"` only guards against end-of-args (`undefined`). if a user mistypes `codex-multi-auth-codex --account --model gpt-5-codex`, the string `"--model"` is swallowed as the account selector and `"gpt-5-codex"` lands in `strippedArgs` as a bare positional — the forwarded codex invocation then sees `gpt-5-codex` as a positional argument rather than a flag value. the resulting error ("did not match any configured account") is confusing because the real mistake is a missing account value. adding a check like `if (next.startsWith("--"))` before consuming it would surface the right error message.
How can I resolve this? If you propose a fix, please make it concise.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 `@docs/features.md`:
- Line 39: Update the “Per-invocation account pin” row in the documentation
table so it describes a single forwarded Codex invocation rather than a broader
wrapper session. Keep the wording aligned with the launcher/runtime contract
used by codex-multi-auth-codex and its --account flag, and make clear that the
pin is ephemeral for one run only and does not persist beyond that invocation.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 527bf60f-427a-4045-8ef6-403c5fcdb035
📒 Files selected for processing (11)
CHANGELOG.mdREADME.mddocs/configuration.mddocs/features.mddocs/reference/commands.mdlib/runtime-rotation-proxy.tslib/runtime/rotation-proxy-state.tslib/runtime/rotation-server-types.tsscripts/codex.jstest/codex-bin-wrapper.test.tstest/runtime-rotation-proxy.test.ts
| | --- | --- | --- | | ||
| | Local Responses proxy | Routes forwarded official Codex Responses/model traffic through a loopback provider named `codex-multi-auth-runtime-proxy` | `codex-multi-auth rotation status` | | ||
| | Per-request account rotation | Moves to another managed account on quota, auth refresh, network, or server failure before streaming response bytes | runtime proxy | | ||
| | Per-invocation account pin | Forces one account for a single wrapper session (ephemeral, fail-hard, never touches the persisted `switch` pin) | `codex-multi-auth-codex --account <index\|email\|id>` | |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Tighten the scope wording.
This reads like a broader wrapper session, but the feature is scoped to one forwarded Codex invocation. Align the table text with the launcher/runtime contract so users don’t expect the pin to survive beyond a single run.
As per coding guidelines: keep terminology consistent with runtime names in documentation. Based on learnings: the PR objective and launcher contract describe a single forwarded invocation.
🔧 Suggested wording
-| Per-invocation account pin | Forces one account for a single wrapper session (ephemeral, fail-hard, never touches the persisted `switch` pin) | `codex-multi-auth-codex --account <index\|email\|id>` |
+| Per-invocation account pin | Forces one account for a single forwarded Codex invocation (ephemeral, fail-hard, never touches the persisted `switch` pin) | `codex-multi-auth-codex --account <index\|email\|id>` |📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| | Per-invocation account pin | Forces one account for a single wrapper session (ephemeral, fail-hard, never touches the persisted `switch` pin) | `codex-multi-auth-codex --account <index\|email\|id>` | | |
| | Per-invocation account pin | Forces one account for a single forwarded Codex invocation (ephemeral, fail-hard, never touches the persisted `switch` pin) | `codex-multi-auth-codex --account <index\|email\|id>` | |
🤖 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 `@docs/features.md` at line 39, Update the “Per-invocation account pin” row in
the documentation table so it describes a single forwarded Codex invocation
rather than a broader wrapper session. Keep the wording aligned with the
launcher/runtime contract used by codex-multi-auth-codex and its --account flag,
and make clear that the pin is ephemeral for one run only and does not persist
beyond that invocation.
Source: Coding guidelines
Follow-up to the code review of #624: - Add proxy tests that exercise the real env-consumption branch (CODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEX) with no option, and option-over-env precedence (also proving a forced index of 0 is honored) — the exact mechanism that must survive the launcher -> detached app-helper boundary. - Add a launcher test that drives the detached app-helper path (bare interactive TUI) and asserts the resolved index reaches the helper process env, via a gated marker in the fixture proxy stub. - Add launcher tests for the email, account-id, --account=<v>, and CODEX_MULTI_AUTH_FORCE_ACCOUNT env-var selectors, plus flag-wins-over-env. - Scrub CODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEX from the background quota-refresh child's env so a management command can never inherit a per-run pin. - Simplify the proxy option/env guard to `??` (null and undefined both defer to the env) and document the tri-state. - Add the now-required forcedAccountIndex to the rotation-proxy-state test helper. - Document that an all-digit selector is always treated as a 1-based index (numeric account ids must be selected by index or email). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code review addressed (commit d488f4e)An independent review flagged 2 Medium (both test-coverage) + several Low/Nit. All actionable items are fixed:
Verification (Windows,
|
Release the per-invocation --account account-forcing feature (#623/#624). Bumps package version, promotes CHANGELOG [Unreleased] -> [2.4.0], adds docs/releases/v2.4.0.md, and updates the README/docs-portal current-stable links and the AGENTS.md version header. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #623.
Summary
codex-multi-auth-codex --account <index|email|id>(and the equivalentCODEX_MULTI_AUTH_FORCE_ACCOUNTenv var) to pin a single account for one forwarded Codex run — for users who keep separate account pools (e.g. personal vs. work) and want a specific invocation to use a specific account (e.g. when driving Codex from another tool).What Changed
scripts/codex.js): parses--account/--account=(flag wins overCODEX_MULTI_AUTH_FORCE_ACCOUNT), strips the launcher-only flag from the args forwarded to real Codex, resolves the selector to a 0-based index against the same scoped accounts pool the proxy loads, and publishesCODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEXfor the proxy. Selector forms: 1-based index, email, or account id.lib/runtime-rotation-proxy.ts,rotation-proxy-state.ts,rotation-server-types.ts): newforcedAccountIndexoption (falls back to the env var so it survives the launcher → detached app-helper process boundary), consumed aspinnedIndex = state.forcedAccountIndex ?? storageMeta.pinnedAccountIndex. This reuses the existing deterministic-pin path, so no other selection logic changes.switchpin, and cannot leak across concurrent sessions.codex_pinned_account_unavailableerror rather than spilling onto another account; the wrapper exits non-zero (without launching Codex) when the selector does not resolve or the runtime rotation proxy is disabled.--account <group>can layer on without reworking the single-pin path (documented, not built).Validation
npm run lintnpm run typechecknpm test— ran the affected suites in full:test/runtime-rotation-proxy.test.tsandtest/codex-bin-wrapper.test.ts(195 passing, incl. 8 new). One pre-existing, unrelated proxy test (evicts oldest local thread goal fallbacks…) hits the default 5s vitest timeout on this Windows box — reproduced identically on cleanmain; passes at--testTimeout=20000.npm test -- test/documentation.test.tsnpm run buildDocs and Governance Checklist
docs/getting-started.md(onboarding flow unchanged)docs/features.mdupdated (new capability row)docs/reference/*pages updated (commands.mdflag + dedicated section;configuration.mdenv vars + proxy note)docs/upgrade.md(no migration behavior change)SECURITY.md/CONTRIBUTING.mdreviewed — no account emails/tokens added to proxy client response headers or logs; selector-error output lists emails only on the user's own stderr, consistent with existinglist/status output.Risk and Rollback
--account/env are unset, behavior is byte-for-byte unchanged (the internal index env is also proactively cleared). Reuses the audited deterministic-pin path.Additional Notes
normalizeForcedAccountIndexunit) and launcher-level (missing value, rotation disabled, out-of-range, strip + index publish).## [Unreleased].🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
--account <index|email|id>or an environment variable.Bug Fixes
Documentation
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
adds
codex-multi-auth-codex --account <index|email|id>(andCODEX_MULTI_AUTH_FORCE_ACCOUNT) to pin a single account for one forwarded codex run without touching the persisted switch pin. the implementation is additive and fail-hard: an unknown selector or disabled proxy exits non-zero before launching codex, and the forced pin reuses the existing deterministic-pin path in the proxy with no rotation.scripts/codex.js): parses and strips--account/--account=, resolves the selector to a 0-based index against the scoped accounts pool, and publishesCODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEX; also removes the index from the background forecast child's env to prevent pin bleed.lib/runtime-rotation-proxy.ts,rotation-proxy-state.ts,rotation-server-types.ts):normalizeForcedAccountIndexcoerces the option or env string;state.forcedAccountIndex ?? storageMeta.pinnedAccountIndexplumbs the ephemeral pin into the existing deterministic-pin path per request.test/codex-bin-wrapper.test.ts,test/runtime-rotation-proxy.test.ts): 8 new tests covering error paths, all selector forms (numeric/email/id/env/equals), flag-wins-over-env precedence, fail-hard 503 with no upstream call, and the detached app-helper env boundary.Confidence Score: 5/5
safe to merge; the feature is purely additive and the existing forwarding path is byte-for-byte unchanged when --account and CODEX_MULTI_AUTH_FORCE_ACCOUNT are both unset.
the new code is well-scoped: flag parsing, selector resolution, and env publication are isolated to the launcher; the proxy change is a single ?? expression that reuses the audited deterministic-pin path. the background refresh child env cleanup correctly deletes the internal index var, though the user-facing selector is left (flagged as a suggestion). no storage changes, no concurrency issues on the proxy side since forcedAccountIndex lives in per-instance state.
scripts/codex.js — the background refresh child inherits CODEX_MULTI_AUTH_FORCE_ACCOUNT (user-facing selector) even though CODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEX (resolved index) is correctly deleted.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram actor User participant Launcher as scripts/codex.js participant Proxy as runtime-rotation-proxy.ts participant Account as AccountManager participant Codex as Real Codex CLI User->>Launcher: codex-multi-auth-codex --account 2 exec Launcher->>Launcher: "extractForcedAccountFlag()<br/>strips --account 2, selector=2" Launcher->>Launcher: isRuntimeRotationProxyEnabled() → true Launcher->>Launcher: "resolveForcedAccountIndex(2)<br/>reads accounts.json, returns index=1" Launcher->>Launcher: "set CODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEX=1" Launcher->>Proxy: "startRuntimeRotationProxy({forcedAccountIndex: 1})" Proxy->>Proxy: "normalizeForcedAccountIndex(option ?? env)<br/>stores in state.forcedAccountIndex=1" Launcher->>Codex: forward exec (stripped args, no --account) Codex->>Proxy: POST /v1/responses Proxy->>Proxy: "pinnedIndex = state.forcedAccountIndex(1) ?? storageMeta.pinnedAccountIndex" Proxy->>Account: pick account[1] (deterministic, no rotation) Account-->>Proxy: access token Proxy->>Proxy: inject auth headers for account[1] Proxy-->>Codex: 200 OK (streamed) Codex-->>User: output%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram actor User participant Launcher as scripts/codex.js participant Proxy as runtime-rotation-proxy.ts participant Account as AccountManager participant Codex as Real Codex CLI User->>Launcher: codex-multi-auth-codex --account 2 exec Launcher->>Launcher: "extractForcedAccountFlag()<br/>strips --account 2, selector=2" Launcher->>Launcher: isRuntimeRotationProxyEnabled() → true Launcher->>Launcher: "resolveForcedAccountIndex(2)<br/>reads accounts.json, returns index=1" Launcher->>Launcher: "set CODEX_MULTI_AUTH_FORCE_ACCOUNT_INDEX=1" Launcher->>Proxy: "startRuntimeRotationProxy({forcedAccountIndex: 1})" Proxy->>Proxy: "normalizeForcedAccountIndex(option ?? env)<br/>stores in state.forcedAccountIndex=1" Launcher->>Codex: forward exec (stripped args, no --account) Codex->>Proxy: POST /v1/responses Proxy->>Proxy: "pinnedIndex = state.forcedAccountIndex(1) ?? storageMeta.pinnedAccountIndex" Proxy->>Account: pick account[1] (deterministic, no rotation) Account-->>Proxy: access token Proxy->>Proxy: inject auth headers for account[1] Proxy-->>Codex: 200 OK (streamed) Codex-->>User: outputComments Outside Diff (1)
test/codex-bin-wrapper.test.ts, line 469-551 (link)the four new launcher tests only exercise the numeric
--accountflag form. three paths that could silently break are not covered:CODEX_MULTI_AUTH_FORCE_ACCOUNT=<selector>env var fallback (the flag-vs-env precedence logic inresolveForcedAccountSelectoris only reachable via this path)--account work@example.com) and account-id selector (--account acc_...) againstfindAccountIndexByIdOrEmail--account=<value>equals-sign form (parsed separately from the space form inextractForcedAccountFlag)writeAccountsFixturealready provides everything needed to write these; adding one test per path would close the gap.Prompt To Fix With AI
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (2): Last reviewed commit: "test/fix: address code review on --accou..." | Re-trigger Greptile