From 0ef663820359651df9dc0de43ab33e2750f17c7c Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Mon, 10 Aug 2026 11:56:12 -0400 Subject: [PATCH 1/5] fix(codex): route app-server through the canonical home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `codex-multi-auth-codex app-server` fell through to the shadow-home transport, and a resident server cannot live there. It usually cannot even start. Codex refuses when `/app-server-control` exists and is not a directory, and the shadow mirror symlinks that directory like any other, so on any home that has already run an app-server the child exits immediately with `socket directory path exists and is not a directory`. A server that does start is worse off than one that fails: the mirror snapshots the runtime SQLite state rather than linking it, so every attached client drives threads against a frozen, throwaway index — the same divergence that hung `resume` on a blank screen (#647), except an app-server holds it for the whole life of the process and hands it to every client that attaches. Classify `app-server` with the predicate that already exists for it and route it to the canonical-home app helper, alongside the interactive entry points. Two details differ from that branch: - `detachOnExit: false`. A resident server owns its proxy for its whole lifetime, so the helper stops when the server does. Leaving it to idle out would strand one proxy helper per supervised restart. - `proxyAppServerAccountRead` is threaded through the helper context and set for this branch, so moving the transport does not silently drop the `account/read` / `getAuthStatus` / `account/rateLimits/read` rewriting that stdio clients already rely on. Only the shadow branch used to set it. Verified end to end against codex-cli 0.147.0 with two managed accounts: the pinned account governs the server's model calls, proved by reading each turn's rollout `rate_limits` back against the accounts' distinct quota windows, and rollouts land in the canonical `sessions` directory. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0134wwvqFQA87kf1K2cLeEk6 --- docs/development/ARCHITECTURE.md | 3 + scripts/codex.js | 19 ++++++ test/codex-bin-wrapper.test.ts | 107 +++++++++++++++++++++++++++---- 3 files changed, 118 insertions(+), 11 deletions(-) diff --git a/docs/development/ARCHITECTURE.md b/docs/development/ARCHITECTURE.md index 2207bb5b..882c1da1 100644 --- a/docs/development/ARCHITECTURE.md +++ b/docs/development/ARCHITECTURE.md @@ -169,6 +169,7 @@ Policy evaluation (`lib/policy/runtime-policy.ts`) can block paused/drained acco | 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. | | 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`. @@ -183,6 +184,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 `/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`. + 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`. diff --git a/scripts/codex.js b/scripts/codex.js index 19d3e1e6..a1d976ab 100755 --- a/scripts/codex.js +++ b/scripts/codex.js @@ -4321,6 +4321,7 @@ async function createRuntimeRotationAppHelperContext( ...baseContext.env, ...helperEnv, }, + proxyAppServerAccountRead: options.proxyAppServerAccountRead === true, cleanup: async (details) => { try { await cleanup(details); @@ -4351,6 +4352,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) @@ -4550,6 +4561,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 +// `/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"; } diff --git a/test/codex-bin-wrapper.test.ts b/test/codex-bin-wrapper.test.ts index 8c5e0bb6..d5a7bc47 100644 --- a/test/codex-bin-wrapper.test.ts +++ b/test/codex-bin-wrapper.test.ts @@ -2146,7 +2146,7 @@ describe("codex bin wrapper", () => { } }); - it("starts the opt-in runtime rotation proxy for app-server without capturing protocol stdio", () => { + it("starts the opt-in runtime rotation proxy for app-server without capturing protocol stdio", async () => { const fixtureRoot = createWrapperFixture(); createRuntimeRotationProxyFixtureModule(fixtureRoot); const fakeBin = createCustomFakeCodexBin(fixtureRoot, [ @@ -2173,27 +2173,39 @@ describe("codex bin wrapper", () => { CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, CODEX_HOME: originalHome, CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY: "1", + CODEX_MULTI_AUTH_APP_ROTATION_IDLE_MS: "200", CODEX_MULTI_AUTH_TEST_PROXY_MARKER: markerPath, OPENAI_API_KEY: undefined, }); const output = combinedOutput(result); expect(result.status).toBe(0); + // The server runs on the canonical home, so the provider is configured + // entirely through `-c` overrides rather than a rewritten shadow config. + expect(output).toContain("FORWARDED:app-server --listen stdio://"); expect(output).toContain( - `FORWARDED:app-server --listen stdio:// -c cli_auth_credentials_store="file" -c model_provider="${RUNTIME_ROTATION_PROXY_PROVIDER_ID}"`, + `-c model_provider="${RUNTIME_ROTATION_PROXY_PROVIDER_ID}"`, ); - const apiKeyMatch = output.match(/^OPENAI_API_KEY:([0-9a-f]{64})$/m); - expect(apiKeyMatch?.[1]).toBeTruthy(); - expect(output).toContain("requires_openai_auth = false"); - expect(output).toContain('name = "codex-multi-auth"'); expect(output).toContain( - `experimental_bearer_token = "${apiKeyMatch?.[1]}"`, + `-c model_providers.${RUNTIME_ROTATION_PROXY_PROVIDER_ID}.base_url="http://127.0.0.1:4567"`, ); - expect(output).toContain('wire_api = "responses"'); - expect(output).not.toContain("env_key"); - expect(readFileSync(markerPath, "utf8")).toBe( - "start:http://127.0.0.1:4567\nclose\n", + expect(output).toContain( + `-c model_providers.${RUNTIME_ROTATION_PROXY_PROVIDER_ID}.requires_openai_auth=false`, + ); + expect(output).toContain( + `-c model_providers.${RUNTIME_ROTATION_PROXY_PROVIDER_ID}.env_key="OPENAI_API_KEY"`, + ); + expect(output).toContain(`CODEX_HOME:${originalHome}`); + const apiKeyMatch = output.match(/^OPENAI_API_KEY:([0-9a-f]{64})$/m); + expect(apiKeyMatch?.[1]).toBeTruthy(); + // account/read still reports the multi-auth identity: moving the transport + // must not silently drop the rewrite stdio clients depend on. + expect(output).toContain("APP_SERVER_LABEL:1"); + // The canonical home's own config is never rewritten on disk. + expect(readFileSync(join(originalHome, "config.toml"), "utf8")).toBe( + 'model_provider = "openai"\n', ); + await waitForFileText(markerPath, "start:http://127.0.0.1:4567\nclose\n"); }); it("rewrites app-server account/read responses to the codex-multi-auth display name", () => { @@ -3154,6 +3166,79 @@ describe("codex bin wrapper", () => { }); } + // `app-server` is a resident server: clients attach for the life of the process + // and drive whole threads through it, so it needs the canonical home for the + // same reason resume did. On the shadow transport it cannot even start — Codex + // rejects a symlinked `/app-server-control`, which is what the + // shadow mirror makes of it — and a server that did start would serve the + // mirror's frozen snapshot of the thread index instead of the real one. + it("uses the canonical Codex home for `app-server` runtime routing", async () => { + const fixtureRoot = createWrapperFixture(); + createRuntimeRotationProxyFixtureModule(fixtureRoot); + const fakeBin = createCustomFakeCodexBin(fixtureRoot, [ + "#!/usr/bin/env node", + 'const fs = require("node:fs");', + 'const path = require("node:path");', + "const args = process.argv.slice(2);", + "console.log(`APP_SERVER_HOME_IS_ORIGINAL:${process.env.CODEX_HOME === process.env.ORIGINAL_CODEX_HOME}`);", + // A symlinked control directory is what makes Codex refuse to start on + // the shadow transport; on the canonical home it is the real directory. + 'const controlPath = path.join(process.env.CODEX_HOME ?? "", "app-server-control");', + "console.log(`APP_SERVER_CONTROL_IS_SYMLINK:${fs.existsSync(controlPath) && fs.lstatSync(controlPath).isSymbolicLink()}`);", + // The thread index only exists in the canonical home; the shadow mirror + // snapshots it, so a shadow-hosted server serves a frozen copy. + 'const statePath = path.join(process.env.CODEX_HOME ?? "", "state_5.sqlite");', + 'console.log(`APP_SERVER_THREAD_INDEX:${fs.existsSync(statePath) ? fs.readFileSync(statePath, "utf8").trim() : "missing"}`);', + 'console.log(`APP_SERVER_COMMAND:${args[0]}`);', + 'console.log(`APP_SERVER_LISTEN:${args[args.indexOf("--listen") + 1]}`);', + 'console.log(`APP_SERVER_HAS_BASE_URL_OVERRIDE:${args.some((arg) => arg.includes("model_providers.codex-multi-auth-runtime-proxy.base_url="))}`);', + "process.exit(0);", + ]); + const originalHome = join(fixtureRoot, "codex-home"); + const markerPath = join(fixtureRoot, "proxy-marker.txt"); + const listenUrl = "unix:///tmp/codex-multi-auth-app-server-test.sock"; + mkdirSync(originalHome, { recursive: true }); + mkdirSync(join(originalHome, "app-server-control"), { recursive: true }); + writeFileSync( + join(originalHome, "config.toml"), + 'model_provider = "openai"\n', + "utf8", + ); + writeFileSync( + join(originalHome, "state_5.sqlite"), + "canonical-thread-index\n", + "utf8", + ); + + const result = runWrapper(fixtureRoot, ["app-server", "--listen", listenUrl], { + CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, + CODEX_HOME: originalHome, + ORIGINAL_CODEX_HOME: originalHome, + CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY: "1", + CODEX_MULTI_AUTH_APP_ROTATION_IDLE_MS: "200", + CODEX_MULTI_AUTH_TEST_PROXY_MARKER: markerPath, + OPENAI_API_KEY: undefined, + }); + + const output = combinedOutput(result); + if (result.status !== 0) { + throw new Error(output); + } + expect(output).toContain("APP_SERVER_HOME_IS_ORIGINAL:true"); + expect(output).toContain("APP_SERVER_CONTROL_IS_SYMLINK:false"); + expect(output).toContain("APP_SERVER_THREAD_INDEX:canonical-thread-index"); + expect(output).toContain("APP_SERVER_COMMAND:app-server"); + expect(output).toContain(`APP_SERVER_LISTEN:${listenUrl}`); + // Rotation is still active: the proxy overrides ride along as `-c` args, so + // the pinned account still bills the server's model calls. + expect(output).toContain("APP_SERVER_HAS_BASE_URL_OVERRIDE:true"); + // The canonical home is never rewritten on disk. + expect(readFileSync(join(originalHome, "config.toml"), "utf8")).toBe( + 'model_provider = "openai"\n', + ); + await waitForFileText(markerPath, "start:http://127.0.0.1:4567\nclose\n"); + }); + // Printing help makes no model requests, so it must not pay for the rotation // transport at all. This matters most for resume/fork now that they are // interactive: that branch detaches its helper on a clean exit, so a helper From 5c8fee5713e3b3ad76eef8e0b4d07218cce1d882 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Mon, 10 Aug 2026 12:32:46 -0400 Subject: [PATCH 2/5] docs: correct the app-server transport in the user-facing docs `docs/reference/commands.md` and `docs/configuration.md` both stated that `codex app-server` runs against a shadow `CODEX_HOME`. It now stays on the canonical home like the interactive TUI, and for app-server that placement is required rather than an optimization, so both pages say why. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0134wwvqFQA87kf1K2cLeEk6 --- docs/configuration.md | 2 +- docs/reference/commands.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5c1b739f..c1c3d3b6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 `/app-server-control` exists and is not a directory, which is what the shadow mirror makes of it. 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 ` (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). diff --git a/docs/reference/commands.md b/docs/reference/commands.md index e820ae6f..80953c54 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -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 `/app-server-control` is a symlink, which is what a shadow home makes of it. 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`. From 79f492df591d826f154109002a4e5e91b0d7c805 Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Mon, 10 Aug 2026 12:54:38 -0400 Subject: [PATCH 3/5] fix(codex): stop the detach grace from stranding an app-server helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review catch. `detachOnExit: false` was not actually honored for a short-lived server: the grace clause detaches on any clean exit inside the window, so an app-server that a client attaches to, queries once, and disconnects from within five seconds still left its helper and proxy running for the full 12h idle timeout — with the owner gone and nothing refreshing the activity clock. That is precisely the stranding the branch claims to prevent. The window exists for callers that hand off and return immediately; `codex app` is the real one. So it now applies only when the caller has not decided for itself: `detachOnExit === true` detaches past the window, an unset `detachOnExit` still detaches inside it, and an explicit `false` never detaches. TUI passes true and `codex app` passes neither, so both keep their behavior exactly. Both app-server tests drop the shortened idle timeout they needed to see the proxy close, which turns each of them into a regression test for this: they fail against the old clause and pass against the new one. Also restores the `cli_auth_credentials_store="file"` assertion the earlier test rewrite dropped for app-server, and corrects docs/architecture.md, which still described the canonical home as interactive-only and labelled the sole runtime path as shadow. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0134wwvqFQA87kf1K2cLeEk6 --- docs/architecture.md | 8 +++++--- scripts/codex.js | 12 +++++++++++- test/codex-bin-wrapper.test.ts | 6 +++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 464f9895..b7a011d2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. | @@ -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 `/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 ` (or `CODEX_MULTI_AUTH_FORCE_ACCOUNT`) for a single invocation only — never mutates the persisted `switch` pin. @@ -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) | diff --git a/scripts/codex.js b/scripts/codex.js index a1d976ab..03654290 100755 --- a/scripts/codex.js +++ b/scripts/codex.js @@ -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(); diff --git a/test/codex-bin-wrapper.test.ts b/test/codex-bin-wrapper.test.ts index d5a7bc47..e025519b 100644 --- a/test/codex-bin-wrapper.test.ts +++ b/test/codex-bin-wrapper.test.ts @@ -2173,7 +2173,6 @@ describe("codex bin wrapper", () => { CODEX_MULTI_AUTH_REAL_CODEX_BIN: fakeBin, CODEX_HOME: originalHome, CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY: "1", - CODEX_MULTI_AUTH_APP_ROTATION_IDLE_MS: "200", CODEX_MULTI_AUTH_TEST_PROXY_MARKER: markerPath, OPENAI_API_KEY: undefined, }); @@ -2182,7 +2181,9 @@ describe("codex bin wrapper", () => { expect(result.status).toBe(0); // The server runs on the canonical home, so the provider is configured // entirely through `-c` overrides rather than a rewritten shadow config. - expect(output).toContain("FORWARDED:app-server --listen stdio://"); + expect(output).toContain( + 'FORWARDED:app-server --listen stdio:// -c cli_auth_credentials_store="file"', + ); expect(output).toContain( `-c model_provider="${RUNTIME_ROTATION_PROXY_PROVIDER_ID}"`, ); @@ -3215,7 +3216,6 @@ describe("codex bin wrapper", () => { CODEX_HOME: originalHome, ORIGINAL_CODEX_HOME: originalHome, CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY: "1", - CODEX_MULTI_AUTH_APP_ROTATION_IDLE_MS: "200", CODEX_MULTI_AUTH_TEST_PROXY_MARKER: markerPath, OPENAI_API_KEY: undefined, }); From 07d0162446593a2188f1d4edc10805e000f3f4cf Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Mon, 10 Aug 2026 13:21:15 -0400 Subject: [PATCH 4/5] fix(codex): assert helper shutdown by liveness, not by graceful close MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 2. The app-server tests waited for the proxy's `close` marker to prove the helper stopped rather than detached. That only works where the helper gets to run its SIGTERM handler: on Windows the terminate is unconditional, so the marker never arrives even though cleanup did exactly what it should, and the assertion would hang for five seconds before failing. Both tests now read the helper's pid from the marker and poll until the process is gone, which is true on every platform. They still fail against the previous detach-grace clause, so the regression they were added for is intact. Docs: the runtime-rotation transport diagram listed three branches and had been missing `resume`/`fork` since they moved to the canonical home; it now lists every branch under the transport it takes, including app-server, and records that help and schema generation take no transport at all. `docs/reference/commands.md` and `docs/configuration.md` say the same, so neither reads as though `app-server --help` starts a proxy — while still noting that the transport skip does not stop the transport-independent `cli_auth_credentials_store` reconcile, which `buildForwardArgs` appends on every path. The prose still says three transports, because that count is right: the interactive commands and `app-server` share the canonical-home helper and differ only in options. The document already counted three over four branch rows before this change, so the convention was mechanism-based already; the sentence now says so instead of leaving the reader to infer it from a table that has grown a row. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0134wwvqFQA87kf1K2cLeEk6 --- docs/configuration.md | 2 +- docs/development/ARCHITECTURE.md | 14 +++++++++---- docs/reference/commands.md | 2 +- test/codex-bin-wrapper.test.ts | 34 ++++++++++++++++++++++++++++++-- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c1c3d3b6..d5fa5002 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 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 `/app-server-control` exists and is not a directory, which is what the shadow mirror makes of it. 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 `/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`, and `app-server help` / `generate-ts` / `generate-json-schema` — 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 ` (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). diff --git a/docs/development/ARCHITECTURE.md b/docs/development/ARCHITECTURE.md index 882c1da1..af1f9d28 100644 --- a/docs/development/ARCHITECTURE.md +++ b/docs/development/ARCHITECTURE.md @@ -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 @@ -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 @@ -162,7 +168,7 @@ 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 | | --- | --- | --- | diff --git a/docs/reference/commands.md b/docs/reference/commands.md index 80953c54..c1cae0f2 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -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` 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 `/app-server-control` is a symlink, which is what a shadow home makes of it. 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 `/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` on a request command, and `app-server help` / `generate-ts` / `generate-json-schema`, 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`. diff --git a/test/codex-bin-wrapper.test.ts b/test/codex-bin-wrapper.test.ts index e025519b..f542d967 100644 --- a/test/codex-bin-wrapper.test.ts +++ b/test/codex-bin-wrapper.test.ts @@ -2174,6 +2174,7 @@ describe("codex bin wrapper", () => { CODEX_HOME: originalHome, CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY: "1", CODEX_MULTI_AUTH_TEST_PROXY_MARKER: markerPath, + CODEX_MULTI_AUTH_TEST_PROXY_MARKER_PID: "1", OPENAI_API_KEY: undefined, }); @@ -2206,7 +2207,21 @@ describe("codex bin wrapper", () => { expect(readFileSync(join(originalHome, "config.toml"), "utf8")).toBe( 'model_provider = "openai"\n', ); - await waitForFileText(markerPath, "start:http://127.0.0.1:4567\nclose\n"); + // The helper stopped with the server rather than being detached to idle + // out. Asserted by the helper's own liveness rather than by the proxy's + // `close` marker: on Windows the terminate is unconditional and never + // runs the child's shutdown handler, so the marker would never arrive + // even though cleanup did exactly what it should. + await waitForPath(markerPath); + const marker = readFileSync(markerPath, "utf8"); + expect(marker).toContain("start:http://127.0.0.1:4567"); + const helperPidMatch = marker.match(/^start:[^\n]*:pid=(\d+)$/m); + expect(helperPidMatch?.[1]).toBeTruthy(); + const helperPid = Number(helperPidMatch?.[1]); + for (let attempt = 0; attempt < 40 && isProcessAlive(helperPid); attempt += 1) { + await sleep(100); + } + expect(isProcessAlive(helperPid)).toBe(false); }); it("rewrites app-server account/read responses to the codex-multi-auth display name", () => { @@ -3217,6 +3232,7 @@ describe("codex bin wrapper", () => { ORIGINAL_CODEX_HOME: originalHome, CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY: "1", CODEX_MULTI_AUTH_TEST_PROXY_MARKER: markerPath, + CODEX_MULTI_AUTH_TEST_PROXY_MARKER_PID: "1", OPENAI_API_KEY: undefined, }); @@ -3236,7 +3252,21 @@ describe("codex bin wrapper", () => { expect(readFileSync(join(originalHome, "config.toml"), "utf8")).toBe( 'model_provider = "openai"\n', ); - await waitForFileText(markerPath, "start:http://127.0.0.1:4567\nclose\n"); + // The helper stopped with the server rather than being detached to idle + // out. Asserted by the helper's own liveness rather than by the proxy's + // `close` marker: on Windows the terminate is unconditional and never + // runs the child's shutdown handler, so the marker would never arrive + // even though cleanup did exactly what it should. + await waitForPath(markerPath); + const marker = readFileSync(markerPath, "utf8"); + expect(marker).toContain("start:http://127.0.0.1:4567"); + const helperPidMatch = marker.match(/^start:[^\n]*:pid=(\d+)$/m); + expect(helperPidMatch?.[1]).toBeTruthy(); + const helperPid = Number(helperPidMatch?.[1]); + for (let attempt = 0; attempt < 40 && isProcessAlive(helperPid); attempt += 1) { + await sleep(100); + } + expect(isProcessAlive(helperPid)).toBe(false); }); // Printing help makes no model requests, so it must not pay for the rotation From a6b4c7f5ff6ebc842d77570ae9c6dc80b700d84a Mon Sep 17 00:00:00 2001 From: Mike Bannister Date: Mon, 10 Aug 2026 13:38:16 -0400 Subject: [PATCH 5/5] docs: cover the app-server help flags and add an upgrade note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review round 3, both docs-only. The help exception named `app-server help` / `generate-ts` / `generate-json-schema` but scoped the flag forms to request commands. `app-server` is not one of those: the wrapper checks `hasHelpFlagAfterCommand` inside its own branch, so `app-server --help` and `app-server -h` skip the transport too and the reference did not say so. `docs/upgrade.md` gains a bullet under the existing runtime-rotation note. Nothing migrates, which is why this is a bullet rather than a section — that file is topic-scoped, and an app-server transport change is a runtime-rotation topic — but the observable behavior does change for anyone launching app-servers through the wrapper: no shadow home appears for the command, and the proxy moves from running inline in the wrapper to the same detached helper the interactive TUI uses, which stops when the server exits. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0134wwvqFQA87kf1K2cLeEk6 --- docs/configuration.md | 2 +- docs/reference/commands.md | 2 +- docs/upgrade.md | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index d5fa5002..67479ff7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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 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 `/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`, and `app-server help` / `generate-ts` / `generate-json-schema` — 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. +`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 `/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 ` (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). diff --git a/docs/reference/commands.md b/docs/reference/commands.md index c1cae0f2..715cf0aa 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -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` 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 `/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` on a request command, and `app-server help` / `generate-ts` / `generate-json-schema`, forward without starting a proxy. 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 `/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`. diff --git a/docs/upgrade.md b/docs/upgrade.md index 3617c3c4..09772fad 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -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: