diff --git a/src/main/codex-cli/codex-read-only-app-server-args.ts b/src/main/codex-cli/codex-read-only-app-server-args.ts index 88d289c8512..346e9051dec 100644 --- a/src/main/codex-cli/codex-read-only-app-server-args.ts +++ b/src/main/codex-cli/codex-read-only-app-server-args.ts @@ -1,10 +1,19 @@ // The config override replaces legacy values before Codex validates config.toml. +export const CODEX_DISABLE_PLUGINS_ARGS = ['-c', 'features.plugins=false'] as const +const CODEX_READ_ONLY_APP_SERVER_TAIL = ['-s', 'read-only', '-a', 'never', 'app-server'] as const + export const CODEX_READ_ONLY_APP_SERVER_ARGS = [ '-c', 'approval_policy=never', - '-s', - 'read-only', - '-a', - 'never', - 'app-server' + ...CODEX_READ_ONLY_APP_SERVER_TAIL +] as const + +// Rate-limit probes are short-lived; plugin startup can launch marketplace +// clones that outlive the probe teardown. Other read-only app-servers retain +// normal plugin behavior. +export const CODEX_RATE_LIMIT_APP_SERVER_ARGS = [ + '-c', + 'approval_policy=never', + ...CODEX_DISABLE_PLUGINS_ARGS, + ...CODEX_READ_ONLY_APP_SERVER_TAIL ] as const diff --git a/src/main/rate-limits/codex-fetcher-process-contract.test.ts b/src/main/rate-limits/codex-fetcher-process-contract.test.ts index 51b0d82b73b..8d067a686ad 100644 --- a/src/main/rate-limits/codex-fetcher-process-contract.test.ts +++ b/src/main/rate-limits/codex-fetcher-process-contract.test.ts @@ -41,6 +41,8 @@ const STUB_CODEX_SOURCE = ` const expectedArgs = [ '-c', 'approval_policy=never', + '-c', + 'features.plugins=false', '-s', 'read-only', '-a', diff --git a/src/main/rate-limits/codex-fetcher.test.ts b/src/main/rate-limits/codex-fetcher.test.ts index 367d2504fb8..e3f9568767a 100644 --- a/src/main/rate-limits/codex-fetcher.test.ts +++ b/src/main/rate-limits/codex-fetcher.test.ts @@ -52,7 +52,7 @@ import { fetchCodexRateLimits } from './codex-fetcher' import { probeCodexAuthPresence } from './codex-auth-presence' import { getActiveHiddenRateLimitPtyCount } from './hidden-pty-cleanup' import { getCmdExePath } from '../win32-utils' -import { CODEX_READ_ONLY_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args' +import { CODEX_RATE_LIMIT_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args' function makeDisposable() { return { dispose: vi.fn() } @@ -724,7 +724,7 @@ describe('fetchCodexRateLimits', () => { "export CODEX_HOME='\\''/home/alice/.local/share/orca/account/home'\\''" ) expect(shellCommand).toContain( - "exec codex '\\''-c'\\'' '\\''approval_policy=never'\\'' '\\''-s'\\'' '\\''read-only'\\'' '\\''-a'\\'' '\\''never'\\'' '\\''app-server'\\'' <&3 >&4 3<&- 4>&-" + "exec codex '\\''-c'\\'' '\\''approval_policy=never'\\'' '\\''-c'\\'' '\\''features.plugins=false'\\'' '\\''-s'\\'' '\\''read-only'\\'' '\\''-a'\\'' '\\''never'\\'' '\\''app-server'\\'' <&3 >&4 3<&- 4>&-" ) expect(shellCommand.match(/<&3 >&4 3<&- 4>&-/g)).toHaveLength(3) expect(shellCommand.match(/exec codex [^\n]+<&3 >&4 3<&- 4>&-/g)).toHaveLength(3) @@ -788,7 +788,7 @@ describe('fetchCodexRateLimits', () => { const [spawnFile, spawnArgs, spawnOptions] = childSpawnMock.mock.calls[0] expect(spawnFile).toBe(getCmdExePath()) - expect(spawnArgs).toEqual(['/d', '/c', codexCommand, ...CODEX_READ_ONLY_APP_SERVER_ARGS]) + expect(spawnArgs).toEqual(['/d', '/c', codexCommand, ...CODEX_RATE_LIMIT_APP_SERVER_ARGS]) expect(spawnOptions).toEqual( expect.objectContaining({ env: expect.objectContaining({ CODEX_HOME: 'C:\\Users\\alice\\.codex' }) @@ -848,6 +848,7 @@ describe('fetchCodexRateLimits', () => { "export CODEX_HOME='\\''/home/alice/.local/share/orca/account/home'\\''" ) expect(shellCommand).toContain('exec codex ') + expect(shellCommand).toContain('features.plugins=false') expect(shellCommand).not.toContain('_orca_codex') expect(shellCommand).not.toContain('wsl-codex-path') expect(spawnOptions).toEqual( diff --git a/src/main/rate-limits/codex-fetcher.ts b/src/main/rate-limits/codex-fetcher.ts index da2904d524e..311843f241c 100644 --- a/src/main/rate-limits/codex-fetcher.ts +++ b/src/main/rate-limits/codex-fetcher.ts @@ -3,7 +3,10 @@ import { spawn } from 'node:child_process' import { isCodexAuthError } from '../../shared/codex-auth-errors' import { buildWslExecArgs, buildWslLoginShellCommand } from '../../shared/wsl-login-shell-command' import { parseWslUncPath } from '../../shared/wsl-paths' -import { CODEX_READ_ONLY_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args' +import { + CODEX_DISABLE_PLUGINS_ARGS, + CODEX_RATE_LIMIT_APP_SERVER_ARGS +} from '../codex-cli/codex-read-only-app-server-args' import { resolveCodexCommand } from '../codex-cli/command' // Why: import from the shared module, not the codex-cli re-export, so a test that // mocks '../codex-cli/command' does not have to restate this pure helper. @@ -44,6 +47,11 @@ const WSL_RPC_TIMEOUT_MS = 25_000 const RPC_INIT_TIMEOUT_MS = 30_000 const WSL_RPC_INIT_TIMEOUT_MS = 40_000 +// Keep the PTY fallback aligned with the RPC probe: rate-limit collection does +// not need marketplace/plugin startup, and those background clones can outlive +// the short-lived probe process. +const CODEX_RATE_LIMIT_PLUGIN_ARGS = CODEX_DISABLE_PLUGINS_ARGS + export type FetchCodexRateLimitsOptions = CodexRateLimitFetchOptions function buildWslCodexCommand( @@ -96,7 +104,7 @@ async function fetchViaRpc(options?: CodexRateLimitFetchOptions): Promise