Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Public overview of how `codex-multi-auth` fits around the official Codex CLI: ac
| Binary | Script | Role |
| --- | --- | --- |
| `codex-multi-auth` | `scripts/codex-multi-auth.js` | Account manager only. Bare subcommands (`status`, `login`, …) normalize to the local auth manager. |
| `codex-multi-auth-codex` | `scripts/codex.js` | Wrapper: `auth ...` stays local; every other command forwards to official Codex with optional runtime rotation and a shadow `CODEX_HOME`. |
| `codex-multi-auth-codex` | `scripts/codex.js` | Wrapper: `auth ...` stays local; every other command forwards to official Codex with optional runtime rotation, on a shadow `CODEX_HOME` for request commands and the canonical one for interactive and resident surfaces. |
| `mcodex` | `scripts/mcodex.js` | Convenience over `codex.js`: default forward, `--monitor` (live `list` via `watch`), `--tmux` / `-t` (optional `--live-accounts`). |
| `codex-multi-auth-app-launcher` | `scripts/codex-app-launcher.js` | Desktop launcher helper for supported user-level shortcuts / managed macOS wrapper apps. |

Expand Down Expand Up @@ -60,7 +60,8 @@ The standalone manager normalizes bare account-manager commands, so both `codex-
- Handles multi-auth `auth` subcommands locally.
- Forwards non-auth commands to official Codex.
- For request-bearing sessions with runtime rotation enabled, creates a temporary shadow `CODEX_HOME`, writes a local provider (`codex-multi-auth-runtime-proxy`), and starts a loopback proxy for that process.
- For interactive TUI sessions, keeps the canonical `CODEX_HOME` and passes the same provider as `-c` overrides instead, so session history and SQLite state are not copied into a shadow and reindexed on every launch.
- For interactive TUI sessions, `resume`/`fork`, and `app-server`, keeps the canonical `CODEX_HOME` and passes the same provider as `-c` overrides instead, so session history and SQLite state are not copied into a shadow and reindexed on every launch.
- `app-server` is on that list for a stronger reason than the others: it is a resident server whose clients drive whole threads through it, and it cannot start at all in a shadow home, because Codex refuses when `<CODEX_HOME>/app-server-control` exists and is not a directory. Its helper also stops with the server rather than idling on, so a supervised restart does not strand one proxy per cycle.
- Keeps forwarded sessions on file-backed auth state unless the caller opts out.
- Supports ephemeral force-pin: `codex-multi-auth-codex --account <index|email|id>` (or `CODEX_MULTI_AUTH_FORCE_ACCOUNT`) for a single invocation only — never mutates the persisted `switch` pin.

Expand Down Expand Up @@ -201,7 +202,8 @@ Terminal user or Codex app
v
codex-multi-auth-codex wrapper / app bind / mcodex
|
| shadow CODEX_HOME + provider: codex-multi-auth-runtime-proxy
| provider: codex-multi-auth-runtime-proxy
| (shadow CODEX_HOME for request commands; canonical for TUI/resume/fork/app-server)
v
localhost Responses proxy (client token)
|
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ Keep these enabled for most environments:

## Runtime Rotation Proxy

`codexRuntimeRotationProxy` is enabled by default. When enabled through defaults, settings, `codex-multi-auth rotation enable`, or `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=1`, the `codex-multi-auth-codex` wrapper starts a localhost-only Responses proxy for forwarded official Codex sessions, including CLI request commands, `codex app-server`, and `codex app` launches through the wrapper. The wrapper writes a temporary shadow `CODEX_HOME/config.toml` that selects a custom provider named `codex-multi-auth-runtime-proxy`, launches the official Codex surface against that provider, and removes the shadow home after the owning process exits. Interactive TUI sessions instead stay on the canonical `CODEX_HOME` and receive the same provider through `-c` overrides, which avoids reindexing session history on every launch and leaves the real `config.toml` untouched. Set `codexRuntimeRotationProxy=false`, run `codex-multi-auth rotation disable`, or set `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0` to bypass the proxy.
`codexRuntimeRotationProxy` is enabled by default. When enabled through defaults, settings, `codex-multi-auth rotation enable`, or `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=1`, the `codex-multi-auth-codex` wrapper starts a localhost-only Responses proxy for forwarded official Codex sessions, including CLI request commands, `codex app-server`, and `codex app` launches through the wrapper. The wrapper writes a temporary shadow `CODEX_HOME/config.toml` that selects a custom provider named `codex-multi-auth-runtime-proxy`, launches the official Codex surface against that provider, and removes the shadow home after the owning process exits. Interactive TUI sessions and `codex app-server` instead stay on the canonical `CODEX_HOME` and receive the same provider through `-c` overrides, which avoids reindexing session history on every launch and leaves the real `config.toml` untouched. For `app-server` that placement is required rather than an optimization: a resident server hands every attached client its thread index, and Codex refuses to start at all when `<CODEX_HOME>/app-server-control` exists and is not a directory, which is what the shadow mirror makes of it. None of this applies to the help and schema-generation forms — `--help`/`-h` after any of these commands, `app-server --help` and `app-server -h` included, and the `app-server help` / `generate-ts` / `generate-json-schema` subcommands — which make no model request and so start no proxy and inject no provider `-c` overrides; the transport-independent `cli_auth_credentials_store` reconcile still applies. Set `codexRuntimeRotationProxy=false`, run `codex-multi-auth rotation disable`, or set `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0` to bypass the proxy.

A single forwarded run can be pinned to one account with `codex-multi-auth-codex --account <selector>` (or `CODEX_MULTI_AUTH_FORCE_ACCOUNT`). The pin is applied per-invocation by that run's own proxy instance, so it never touches the persisted `switch` pin and cannot leak across concurrent sessions. Because the proxy is required for the pin to take effect, `--account` fails hard when the proxy is disabled rather than silently using a rotated account. See [Force an account for one invocation](reference/commands.md#force-an-account-for-one-invocation).

Expand Down
17 changes: 13 additions & 4 deletions docs/development/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@ Official Codex CLI

Runtime rotation enabled -> one of three transports
|
|- interactive TUI (no forwarded subcommand)
|- interactive TUI (no forwarded subcommand), resume, fork, app-server
| canonical CODEX_HOME + ephemeral -c provider overrides
| (no shadow copy, no provider/transport rewrite of config.toml,
| detach on exit; the auth-store reconcile above still applies)
| (no shadow copy, no provider/transport rewrite of config.toml;
| the auth-store reconcile above still applies)
| detach on exit, except app-server: a resident server owns its
| proxy for its whole lifetime, so the helper stops with it
|
|- codex app
| app runtime helper process + shadow CODEX_HOME
Expand All @@ -67,6 +69,10 @@ Runtime rotation enabled -> one of three transports
| shadow CODEX_HOME/config.toml
| |- model_provider = "codex-multi-auth-runtime-proxy"
| |- provider base_url = localhost proxy
|
|- (help and schema generation take no transport at all:
| --help/-h on any of these, and app-server help / generate-ts /
| generate-json-schema, forward without starting a proxy)
v
lib/runtime-rotation-proxy.ts
|- validates local client token
Expand Down Expand Up @@ -162,13 +168,14 @@ Policy evaluation (`lib/policy/runtime-policy.ts`) can block paused/drained acco

1. The wrapper checks `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY` and `codexRuntimeRotationProxy`.
2. If disabled, the command forwards to the official Codex CLI unchanged except for normal wrapper compatibility settings.
3. If enabled, `createRuntimeRotationProxyContextIfEnabled` (`scripts/codex.js`) picks one of three transports from the forwarded argv:
3. If enabled, `createRuntimeRotationProxyContextIfEnabled` (`scripts/codex.js`) routes the forwarded argv onto one of three transports. More branches than transports: the interactive commands and `app-server` share the canonical-home helper, differing only in options.

| Branch | Predicate | Transport |
| --- | --- | --- |
| Interactive TUI | `isCodexInteractiveTuiCommand` — no forwarded subcommand at all | App runtime helper with `useCanonicalHome: true` and `detachOnExit: true`. Runs against the **canonical** `CODEX_HOME`; the provider is passed as ephemeral `-c model_providers.*` overrides. No shadow copy and no state sync-back. Nothing **provider- or transport-related** is written into `config.toml` on this path — the only top-level key the wrapper still reconciles there is `cli_auth_credentials_store`, which is transport-independent (see step 4 note). |
| Interactive `resume` / `fork` | `isCodexInteractiveResumeCommand` — forwarded command is `resume` or `fork` | Same transport and options as the interactive TUI above. These open a TUI against an existing thread, so they must see the canonical thread index. |
| `codex app` | `isCodexAppCommand` — forwarded command is `app` | App runtime helper process with a shadow `CODEX_HOME`. |
| `app-server` | `isCodexAppServerCommand` — forwarded command is `app-server` | Canonical-home app helper, but `detachOnExit: false`: a resident server owns its proxy for its whole lifetime. `account/read` rewriting stays on. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

update the transport count and system diagram.

This branch adds a fourth runtime transport, but line 165 still says “one of three transports,” and the system diagram omits app-server. Readers can follow the old shadow-home path for this command. Add the canonical-home app-server branch to the diagram and change the count to four. scripts/codex.js:4574-4660 is the routing contract.

As per path instructions, development documentation must prefer the current architecture and verify feature matrices against implemented features.

🤖 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/development/ARCHITECTURE.md` at line 172, Update the architecture
documentation’s transport count from three to four and revise the system diagram
to include the canonical-home app-server branch identified by
isCodexAppServerCommand, matching the routing contract in scripts/codex.js.
Ensure the diagram shows app-server following its resident proxy ownership path
rather than the legacy shadow-home path.

Source: Path instructions

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping three, and flagging this as the one open disagreement on the PR so a human can settle it.

The count was already mechanism-based before this branch: the same sentence said "one of three transports" over four branch rows — TUI, resume/fork, app, everything else. So rows and transports were never 1:1 here, and reading a new row as a new transport contradicts the file's own established usage.

app-server reuses the canonical-home app helper the interactive commands already use. detachOnExit is lifecycle and proxyAppServerAccountRead is a wrapper-side protocol rewrite; neither changes home placement nor how traffic reaches the proxy. The three mechanisms are unchanged: canonical-home helper, shadow-home helper for codex app, inline shadow home for everything else.

What was genuinely wrong is that the convention was implicit, so the sentence now states it ("more branches than transports"), and the diagram — which was stale in a way that predates this PR, missing resume/fork since #648 — now lists every branch under the transport it takes, including app-server and the no-transport help path.

@ndycode if you count rows rather than mechanisms in this document, say so and I'll change it to four in one commit.

| Everything else | request-bearing forwarded command | Shadow `CODEX_HOME` created inline by the wrapper process. |

4. The wrapper starts `lib/runtime-rotation-proxy.ts` on `127.0.0.1` with a per-process client API key. Shadow-home branches copy relevant official Codex state and rewrite `config.toml` to select `codex-multi-auth-runtime-proxy`; the canonical-home branch instead injects the same provider through `-c` arguments and passes the client key via `OPENAI_API_KEY`.
Expand All @@ -183,6 +190,8 @@ Why the interactive branch is different: copying the Codex home into a shadow ma

`resume` and `fork` belong to that same branch for a stronger reason. The shadow mirror deliberately omits the runtime SQLite state (`isRuntimeRotationShadowHomeOmittedEntry`), so a shadow home only ever holds a partial thread index rebuilt from the linked `sessions` directory. Resuming a thread that the shadow index does not contain left the TUI on a blank screen forever, while the same command worked under the official CLI and with the proxy disabled. Routing both commands to the canonical home is what makes the thread visible (#647).

`app-server` joins them for both reasons at once, and one of its own. It is a resident server rather than a one-shot forwarded command: clients attach for the life of the process and drive whole threads through it, so a shadow home would hand every attached client the mirror's frozen snapshot of the thread index. It also cannot start there at all — Codex refuses when `<CODEX_HOME>/app-server-control` exists and is not a directory, and the shadow mirror symlinks that directory like any other, so `codex app-server` exits with `socket directory path exists and is not a directory` on any home that has already run one. Because the server is resident, its branch sets `detachOnExit: false`: the helper stops when the server does, instead of idling on for its full timeout and stranding one proxy per supervised restart. `proxyAppServerAccountRead` stays on across the move, so stdio clients still see the `codex-multi-auth` identity from `account/read`, `getAuthStatus`, and `account/rateLimits/read`.

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Helper shutdown is bounded rather than best-effort. `stopRuntimeRotationAppHelper` sends `SIGTERM`, waits out the graceful window, escalates to `SIGKILL` if the helper is still running, and then unconditionally destroys the helper's stdio streams and unrefs the child. That last step is the load-bearing one: the helper is spawned with piped stdio, so a helper that outlives the window — or any process that inherited those pipes — keeps the wrapper's event loop referenced and the shell prompt never returns. On Windows the signals are emulated as unconditional termination, so the stream teardown is the only part that reliably frees the wrapper there.

Two interactive sessions can therefore run concurrently against the same home — the same as running the official CLI twice — and **no lock is taken over session state**: neither session copies or syncs it, so there is nothing to clobber. Regression coverage lives in `test/codex-bin-wrapper.test.ts`.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ Behavior:
- `reset-runtime` clears volatile rotation state and, when app-bind helpers are available, restarts the packaged app bind. Specifically it (1) unbinds and rebinds the packaged Codex app so the router picks up the reset state, (2) resets the process-global rotation trackers and circuit breakers, and (3) clears the persisted runtime-observability fields used by status/report — pool-exhaustion reason, per-account skip reasons, and policy-blocked entries — stamping a reset timestamp and reason. If the app-bind helpers are unavailable it still performs (2) and (3), and reports that new wrapper sessions will pick up the reset state. A failed bind restart exits non-zero.
- `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0` disables the proxy for the current process without changing settings.

When enabled, the wrapper creates a temporary shadow `CODEX_HOME/config.toml` with a custom provider named `codex-multi-auth-runtime-proxy`, starts a `127.0.0.1` proxy on a random port, and forwards official Codex Responses traffic through that provider. This applies to CLI request commands plus `codex app-server` and `codex app` when they are launched through the wrapper. Existing behavior is unchanged while the setting and env override are off.
When enabled, the wrapper creates a temporary shadow `CODEX_HOME/config.toml` with a custom provider named `codex-multi-auth-runtime-proxy`, starts a `127.0.0.1` proxy on a random port, and forwards official Codex Responses traffic through that provider. This applies to CLI request commands plus `codex app` when they are launched through the wrapper. `codex app-server` uses the same proxy but stays on the canonical `CODEX_HOME` and receives the provider through `-c` overrides, like the interactive TUI: a resident server must see the real thread index, and Codex refuses to start when `<CODEX_HOME>/app-server-control` is a symlink, which is what a shadow home makes of it. Printing help or generating schemas never makes a model request, so it takes no transport at all: `--help`/`-h` after any of these commands — `app-server --help` and `app-server -h` included — and the `app-server help` / `generate-ts` / `generate-json-schema` subcommands, all forward without starting a proxy. Existing behavior is unchanged while the setting and env override are off.

If every managed account is temporarily unavailable, the proxy returns `codex_runtime_rotation_pool_exhausted` with a retry hint pointing back to `codex-multi-auth rotation status`.

Expand Down
1 change: 1 addition & 0 deletions docs/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ The 2.0.1 line makes runtime rotation the default for request-bearing wrapper-la
- Installed wrappers may perform a best-effort daily npm version check during normal forwarded startup. If a newer package is detected, update manually with `npm install -g codex-multi-auth@latest`.
- Official Codex app binaries are not patched.
- Pause/drain account policies and budget/profile checks are enforced on the rotation path via `evaluateRuntimePolicy`.
- `codex-multi-auth-codex app-server` now runs against the canonical `CODEX_HOME` instead of a temporary shadow home, so it sees the real thread index and no longer fails to start on a home that already has an `app-server-control` directory. Nothing migrates and no state moves; the observable differences are that no shadow home appears under `runtime-shadow-homes/` for this command, and that the proxy now runs in the same detached rotation helper the interactive TUI uses — visible as a `--codex-multi-auth-runtime-app-helper` process while the server runs — and stops when the server exits. Help and schema-generation forms are unaffected — they take no transport at all.

Validate after enabling:

Expand Down
31 changes: 30 additions & 1 deletion scripts/codex.js
Original file line number Diff line number Diff line change
Expand Up @@ -4286,7 +4286,17 @@ async function createRuntimeRotationAppHelperContext(

const cleanup = async ({ exitCode } = {}) => {
const livedMs = Date.now() - startedAt;
if (exitCode === 0 && (options.detachOnExit === true || livedMs < detachGraceMs)) {
// The grace window exists for callers that exit immediately and expect the
// helper to outlive them — `codex app` hands off to the desktop app and
// returns at once. A caller that opts out explicitly means it, and a
// resident server that happens to exit quickly (a client attaches, runs one
// query, disconnects) must not strand a helper for the full idle timeout
// just for being short-lived. Only `detachOnExit === true` detaches past
// the window; only an unset `detachOnExit` still detaches inside it.
const detach =
options.detachOnExit === true ||
(options.detachOnExit !== false && livedMs < detachGraceMs);
if (exitCode === 0 && detach) {
helper.stdout?.destroy();
helper.stderr?.destroy();
helper.unref();
Expand Down Expand Up @@ -4321,6 +4331,7 @@ async function createRuntimeRotationAppHelperContext(
...baseContext.env,
...helperEnv,
},
proxyAppServerAccountRead: options.proxyAppServerAccountRead === true,
cleanup: async (details) => {
try {
await cleanup(details);
Expand Down Expand Up @@ -4351,6 +4362,16 @@ async function createRuntimeRotationProxyContextIfEnabled(
if (isCodexAppCommand(rawArgs)) {
return createRuntimeRotationAppHelperContext(baseContext, configTomlModule);
}
if (isCodexAppServerCommand(rawArgs)) {
return createRuntimeRotationAppHelperContext(baseContext, configTomlModule, {
// A resident server owns its proxy for its whole lifetime, so the helper
// stops with it instead of idling on after exit — a supervised server
// that restarts would otherwise strand one helper per restart.
detachOnExit: false,
useCanonicalHome: true,
proxyAppServerAccountRead: true,
});
}
if (
isCodexInteractiveTuiCommand(rawArgs) ||
isCodexInteractiveResumeCommand(rawArgs)
Expand Down Expand Up @@ -4550,6 +4571,14 @@ function isCodexAppCommand(rawArgs) {
return findForwardedCommand(rawArgs)?.command === "app";
}

// `app-server` is a resident server rather than a one-shot forwarded command:
// clients attach for the life of the process and drive whole threads through it,
// so it takes the canonical-home transport alongside the interactive entry points
// below. The shadow home cannot host one. Codex refuses to start when
// `<CODEX_HOME>/app-server-control` is a symlink, which is exactly what the shadow
// mirror makes of it, and the mirror snapshots the runtime SQLite state rather than
// linking it, so the server would serve a frozen, throwaway thread index — the same
// divergence that hung resume (#647).
function isCodexAppServerCommand(rawArgs) {
return findForwardedCommand(rawArgs)?.command === "app-server";
}
Expand Down
Loading