fix(codex): start short-lived Codex app-servers on Windows without cmd.exe (#23830)

* fix(codex): start short-lived Codex app-servers on Windows without cmd.exe

Orca's short-lived Codex app-server launches (history index passes, the
state-DB recovery claimant, hook trust grants, and the rate-limit and model
catalog probes) wrapped npm's codex.cmd in `cmd.exe /d /c` before handing it
to spawnProcess. That hid the shim from spawnProcess's resolver, so every one
of these launches went through cmd.exe instead of straight to node.

Hand the bare CLI path to the spawn chokepoint like the chat session already
does. The rate-limit probe and the recovery claimant also move off direct
node:child_process imports, shrinking that allowlist by two.

* chore(codex): drop stale cmd.exe premises from the probe kill and CLI pairing comments

Short-lived Codex app-servers now reach spawnProcess as the bare shim, so the
direct child is node for a resolved npm shim and cmd.exe only as the fallback.

* test(child-process): delist the rate-limit probe from the hidden-console ratchet

The probe now spawns through spawnProcess, which always hides the console.
This commit is contained in:
Brennan Benson
2026-09-29 17:52:22 -07:00
committed by GitHub
parent 7980ab9942
commit 7f8ca49e3d
17 changed files with 167 additions and 90 deletions
@@ -32,8 +32,8 @@ export function killCodexAppServerProcessTree(
return
}
try {
// Why: npm-installed Codex runs behind cmd.exe; killing only that wrapper
// leaves the app-server child alive after a timeout or failed shutdown.
// Why: npm-installed Codex runs behind a launcher (node, or cmd.exe for an unresolved shim);
// killing only that wrapper leaves the app-server child alive after a timeout.
const killer = spawnImpl('taskkill', ['/pid', String(child.pid), '/t', '/f'], {
stdio: 'ignore',
windowsHide: true
+1 -1
View File
@@ -25,7 +25,7 @@ export type CodexAppServerInvocation = {
*
* Required, and `null` only for a guest-side launcher (wsl.exe) where the host
* path means nothing. Optional would let a native builder omit it and silently
* fall back to pairing against a cmd.exe wrapper with no type error.
* skip the pairing with no type error.
*/
cliPath: string | null
/** Overlay applied on top of the inherited environment (e.g. CODEX_HOME). */
+2 -6
View File
@@ -1,4 +1,3 @@
import { getSpawnArgsForWindows } from '../win32-utils'
import { CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args'
import { runCodexAppServerSession } from './codex-app-server-session'
import { fetchCodexModelCatalogListing } from './codex-structured-model-catalog'
@@ -44,14 +43,11 @@ export function createCodexModelCatalogProbe(
): AgentModelCatalogProbe {
return async (accountHomePath: string): Promise<AgentModelCatalogSuccess> => {
const { command, environment } = await resolveCodexStructuredInvocation(deps)
const { spawnCmd, spawnArgs } = getSpawnArgsForWindows(command, [
...CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS
])
const run = deps.runSession ?? runCodexAppServerSession
const listing = await run(
{
command: spawnCmd,
args: spawnArgs,
command,
args: [...CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS],
cliPath: command,
env: { ...definedEnv(environment), CODEX_HOME: accountHomePath },
timeoutMs: CODEX_MODEL_CATALOG_PROBE_TIMEOUT_MS
+4 -8
View File
@@ -2,7 +2,6 @@ import { dirname, join } from 'node:path'
import { resolveCodexCommand } from '../codex-cli/command'
import { isTransientSqliteContention } from '../sqlite/sqlite-read-failure'
import { CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args'
import { getSpawnArgsForWindows } from '../win32-utils'
import { getCodexSessionBackfillStateDirPath } from './codex-home-paths'
import { resolveCodexSessionBackfillPaths } from './codex-session-backfill'
import {
@@ -291,14 +290,11 @@ export function buildNativeHealInvocation(
timeoutMs: number
): CodexAppServerInvocation {
const command = resolveCodexCommand()
// Why: each session is torn down after one batch, so plugin startup could
// leave marketplace clones running; indexing needs neither plugins nor tools.
const { spawnCmd, spawnArgs } = getSpawnArgsForWindows(command, [
...CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS
])
return {
command: spawnCmd,
args: spawnArgs,
command,
// Why: each session is torn down after one batch, so plugin startup could
// leave marketplace clones running; indexing needs neither plugins nor tools.
args: [...CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS],
cliPath: command,
// Why: pin the home explicitly — nested Orca launches can inherit a managed
// CODEX_HOME from the daemon environment, which would index the wrong sqlite DB.
@@ -0,0 +1,74 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args'
import type { CodexAppServerInvocation } from './codex-app-server-session'
const NPM_CODEX_SHIM = 'C:\\Users\\alice\\AppData\\Roaming\\npm\\codex.cmd'
vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: () => NPM_CODEX_SHIM }))
import { buildNativeHealInvocation } from './codex-session-index-heal'
import { resolveNativeCodexTrustGrantHost } from './codex-trust-grant-host'
import { createCodexModelCatalogProbe } from './codex-model-catalog-probe'
// Why: spawnProcess resolves npm's codex.cmd past cmd.exe, but only when it is handed the shim.
// A builder that pre-wraps it in `cmd.exe /d /c` puts cmd.exe back into every short-lived
// Codex launch — hundreds of them while a large history indexes.
describe('short-lived Codex app-server invocations on Windows', () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
beforeEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
})
afterEach(() => {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
})
it('hands the index-heal session the shim itself', () => {
const invocation = buildNativeHealInvocation('C:\\homes\\a', 1_000)
expect(invocation.command).toBe(NPM_CODEX_SHIM)
expect(invocation.cliPath).toBe(NPM_CODEX_SHIM)
expect(invocation.args).toEqual([...CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS])
})
it('hands the hook trust grant session the shim itself', () => {
const request = resolveNativeCodexTrustGrantHost().buildRequest({
runtimeHomePath: 'C:\\homes\\a',
managedCommand: 'orca-hook',
expectedTrustKeys: []
})
expect(request.invocation.command).toBe(NPM_CODEX_SHIM)
expect(request.invocation.args).toEqual(['app-server'])
})
it('hands the model catalog probe session the shim itself', async () => {
const invocations: CodexAppServerInvocation[] = []
const probe = createCodexModelCatalogProbe({
resolveEnvironment: async () => ({ PATH: 'C:\\bin' }),
resolveCommand: () => NPM_CODEX_SHIM,
runSession: async (invocation, body) => {
invocations.push(invocation)
return body({
request: async () => ({
data: [
{
model: 'gpt-live',
displayName: 'GPT Live',
hidden: false,
supportedReasoningEfforts: [{ reasoningEffort: 'high' }],
isDefault: true
}
],
nextCursor: null
}),
notify: () => {}
})
}
})
await probe('C:\\homes\\a')
expect(invocations[0]?.command).toBe(NPM_CODEX_SHIM)
expect(invocations[0]?.args).toEqual([...CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS])
})
})
@@ -5,7 +5,6 @@ import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as ownerIdentity from '../agent-hooks/managed-hook-owner-identity'
import { CODEX_READ_ONLY_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args'
import { getCmdExePath } from '../win32-utils'
import {
_internals,
resolveCodexBackfillSupervisorLockRoot,
@@ -365,7 +364,7 @@ describe('Codex state DB backfill recovery', () => {
expect(terminate).toHaveBeenCalledWith(child)
})
it('uses batch-safe read-only arguments for native Windows recovery', async () => {
it('hands the Codex shim itself to the spawn chokepoint for native Windows recovery', async () => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const child = createFakeChild()
const spawnProcess = vi.fn(() => child)
@@ -393,11 +392,9 @@ describe('Codex state DB backfill recovery', () => {
string[],
{ cwd?: string; env?: NodeJS.ProcessEnv }
]
expect(spawnFile).toBe(getCmdExePath())
// Why: spawnProcess resolves a recognised npm shim past cmd.exe; pre-wrapping hides the shim.
expect(spawnFile).toBe(codexCommand)
expect(spawnArgs).toEqual([
'/d',
'/c',
codexCommand,
'-c',
'approval_policy=never',
'-s',
@@ -1,4 +1,3 @@
import { spawn, type ChildProcess } from 'node:child_process'
import { createHash } from 'node:crypto'
import { join } from 'node:path'
import { setTimeout as delay } from 'node:timers/promises'
@@ -11,7 +10,11 @@ import { resolveCodexCommand } from '../codex-cli/command'
import { withCliRuntimeOnPath } from '../../shared/node-cli-command-resolution'
import { CODEX_READ_ONLY_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args'
import { terminateCodexProbeChild } from '../rate-limits/codex-probe-termination'
import { getSpawnArgsForWindows } from '../win32-utils'
import type { ChildProcessHandle } from '../../shared/child-process/process-spec'
import {
spawnCodexAppServerProcess,
type CodexAppServerSpawn
} from './codex-app-server-process-tree-kill'
import { getOrcaUserDataPath } from './codex-home-paths'
import {
BACKFILL_PENDING_MIN_SESSION_FILES,
@@ -34,17 +37,17 @@ export type CodexStateDbBackfillRecoverySummary = {
}
type RecoveryDependencies = {
spawnProcess: typeof spawn
spawnProcess: CodexAppServerSpawn
resolveCommand: () => string
readStatus: (codexHomePath: string) => CodexStateDbBackfillStatus
countSessions: (sessionsRoot: string, limit: number) => number
now: () => number
sleep: (ms: number, signal: AbortSignal) => Promise<void>
terminate: (child: ChildProcess) => Promise<void>
terminate: (child: ChildProcessHandle) => Promise<void>
}
const defaultDependencies: RecoveryDependencies = {
spawnProcess: spawn,
spawnProcess: spawnCodexAppServerProcess,
resolveCommand: resolveCodexCommand,
readStatus: readCodexStateDbBackfillStatus,
countSessions: countCodexSessionFilesUpTo,
@@ -86,7 +89,7 @@ function initialRecoveryDecision(
function spawnRecoveryProcess(
codexHomePath: string,
dependencies: RecoveryDependencies
): ChildProcess {
): ChildProcessHandle {
const wslHome = process.platform === 'win32' ? parseWslUncPath(codexHomePath) : null
if (wslHome) {
return dependencies.spawnProcess(
@@ -104,10 +107,7 @@ function spawnRecoveryProcess(
)
}
const command = dependencies.resolveCommand()
const { spawnCmd, spawnArgs } = getSpawnArgsForWindows(command, [
...CODEX_READ_ONLY_APP_SERVER_ARGS
])
return dependencies.spawnProcess(spawnCmd, spawnArgs, {
return dependencies.spawnProcess(command, [...CODEX_READ_ONLY_APP_SERVER_ARGS], {
cwd: codexHomePath,
stdio: ['pipe', 'ignore', 'ignore'],
windowsHide: true,
+2 -4
View File
@@ -1,6 +1,5 @@
import { runProcess } from '../../shared/child-process/run-process'
import { resolveCodexCommand } from '../codex-cli/command'
import { getSpawnArgsForWindows } from '../win32-utils'
import {
buildWslCodexAppServerArgs,
buildWslCodexIdentityProbe,
@@ -73,12 +72,11 @@ export function resolveNativeCodexTrustGrantHost(): ResolvedCodexTrustGrantHost
return {
binaryStamp: command === 'codex' ? null : buildNativeCodexBinaryStamp(command),
buildRequest: (input) => {
const { spawnCmd, spawnArgs } = getSpawnArgsForWindows(command, ['app-server'])
const useDefaultCodexHome = input.useDefaultCodexHome === true
return {
invocation: {
command: spawnCmd,
args: spawnArgs,
command,
args: ['app-server'],
cliPath: command,
...(useDefaultCodexHome
? { envToDelete: ['CODEX_HOME'] }
@@ -2,18 +2,27 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as Win32Utils from '../win32-utils'
import type * as RunProcess from '../../shared/child-process/run-process'
const { getSpawnArgsForWindowsMock, ptySpawnMock, resolveCodexCommandMock } = vi.hoisted(() => ({
getSpawnArgsForWindowsMock: vi.fn(),
const { ptySpawnMock, resolveCodexCommandMock, stubScript } = vi.hoisted(() => ({
stubScript: { path: '' },
ptySpawnMock: vi.fn(),
resolveCodexCommandMock: vi.fn()
}))
vi.mock('../win32-utils', async (importOriginal) => ({
...(await importOriginal<typeof Win32Utils>()),
getSpawnArgsForWindows: getSpawnArgsForWindowsMock
}))
// Runs the node stub in place of the resolved CLI, through the real chokepoint.
vi.mock('../../shared/child-process/run-process', async (importOriginal) => {
const actual = await importOriginal<typeof RunProcess>()
return {
...actual,
spawnProcess: (spec: Parameters<typeof actual.spawnProcess>[0]) =>
actual.spawnProcess({
...spec,
program: process.execPath,
args: [stubScript.path, ...(spec.args ?? [])]
})
}
})
vi.mock('../codex-cli/command', () => ({
resolveCodexCommand: resolveCodexCommandMock
@@ -105,10 +114,7 @@ describe('Codex rate-limit process contract', () => {
previousExpectedHome = process.env.ORCA_EXPECTED_CODEX_HOME
process.env.ORCA_EXPECTED_CODEX_HOME = join(tempRoot, 'managed-codex-home')
resolveCodexCommandMock.mockReturnValue('codex')
getSpawnArgsForWindowsMock.mockImplementation((_command: string, args: string[]) => ({
spawnCmd: process.execPath,
spawnArgs: [stubPath, ...args]
}))
stubScript.path = stubPath
})
afterEach(() => {
@@ -2,19 +2,28 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as Win32Utils from '../win32-utils'
import type * as RunProcess from '../../shared/child-process/run-process'
import { isCodexAuthError } from '../../shared/codex-auth-errors'
const { getSpawnArgsForWindowsMock, ptySpawnMock, resolveCodexCommandMock } = vi.hoisted(() => ({
getSpawnArgsForWindowsMock: vi.fn(),
const { ptySpawnMock, resolveCodexCommandMock, stubScript } = vi.hoisted(() => ({
stubScript: { path: '' },
ptySpawnMock: vi.fn(),
resolveCodexCommandMock: vi.fn()
}))
vi.mock('../win32-utils', async (importOriginal) => ({
...(await importOriginal<typeof Win32Utils>()),
getSpawnArgsForWindows: getSpawnArgsForWindowsMock
}))
// Runs the node stub in place of the resolved CLI, through the real chokepoint.
vi.mock('../../shared/child-process/run-process', async (importOriginal) => {
const actual = await importOriginal<typeof RunProcess>()
return {
...actual,
spawnProcess: (spec: Parameters<typeof actual.spawnProcess>[0]) =>
actual.spawnProcess({
...spec,
program: process.execPath,
args: [stubScript.path, ...(spec.args ?? [])]
})
}
})
vi.mock('../codex-cli/command', () => ({
resolveCodexCommand: resolveCodexCommandMock
@@ -62,10 +71,7 @@ describe('Codex RPC exit diagnostics', () => {
stubPath = join(tempRoot, 'codex-exit-stub.cjs')
writeFileSync(stubPath, STUB_CODEX_SOURCE)
resolveCodexCommandMock.mockReturnValue('codex')
getSpawnArgsForWindowsMock.mockImplementation((_command: string, args: string[]) => ({
spawnCmd: process.execPath,
spawnArgs: [stubPath, ...args]
}))
stubScript.path = stubPath
})
afterEach(() => {
@@ -3,6 +3,7 @@ import { EventEmitter } from 'node:events'
import { tmpdir } from 'node:os'
import { delimiter, join } from 'node:path'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as RunProcess from '../../shared/child-process/run-process'
const { childSpawnMock, readFileMock, resolveCodexCommandMock, ptySpawnMock } = vi.hoisted(() => ({
childSpawnMock: vi.fn(),
@@ -12,6 +13,12 @@ const { childSpawnMock, readFileMock, resolveCodexCommandMock, ptySpawnMock } =
}))
vi.mock('node:child_process', () => ({ spawn: childSpawnMock }))
// The chokepoint is the seam: assertions see what the fetcher asked for, before shim resolution.
vi.mock('../../shared/child-process/run-process', async (importOriginal) => ({
...(await importOriginal<typeof RunProcess>()),
spawnProcess: (spec: { program: string; args?: readonly string[] }) =>
childSpawnMock(spec.program, spec.args ?? [], spec)
}))
vi.mock('node:fs/promises', () => ({ readFile: readFileMock }))
vi.mock('../codex-cli/command', () => ({ resolveCodexCommand: resolveCodexCommandMock }))
vi.mock('node-pty', () => ({ spawn: ptySpawnMock }))
@@ -79,18 +86,17 @@ describe('codex rate-limit spawn runtime pairing', () => {
await vi.advanceTimersByTimeAsync(0)
const spawnEnv = childSpawnMock.mock.calls[0]?.[2]?.env as NodeJS.ProcessEnv
// Guards the argument choice: pairing spawnCmd (cmd.exe on win32) rather than
// the resolved CLI silently reverts the ABI fix (stablyai/orca#10932).
// Guards the argument choice: pairing anything but the resolved CLI silently
// reverts the ABI fix (stablyai/orca#10932).
expect(spawnEnv.PATH?.split(delimiter)[0]).toBe(bin)
rpcChild.emit('close')
await resultPromise
})
it('pairs the resolved CLI on win32, where the spawn command is cmd.exe', async () => {
// Why win32 specifically: on posix getSpawnArgsForWindows returns the CLI
// itself, so pairing the spawn command instead of the resolved CLI is
// indistinguishable. Only here does the wrong argument become cmd.exe.
it('hands an npm .cmd shim to the spawn chokepoint unwrapped, paired with its node', async () => {
// Why win32 specifically: only here could a pre-wrap turn the program into
// cmd.exe, which hides the shim from spawnProcess's resolver.
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
try {
@@ -103,7 +109,7 @@ describe('codex rate-limit spawn runtime pairing', () => {
await vi.advanceTimersByTimeAsync(0)
const spawnCommand = childSpawnMock.mock.calls[0]?.[0] as string
expect(spawnCommand.toLowerCase()).toContain('cmd.exe')
expect(spawnCommand).toBe(cli)
const spawnEnv = childSpawnMock.mock.calls[0]?.[2]?.env as NodeJS.ProcessEnv
expect((spawnEnv.Path ?? spawnEnv.PATH)?.split(';')[0]).toBe(bin)
+11 -8
View File
@@ -1,6 +1,7 @@
import { EventEmitter } from 'node:events'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as RunProcess from '../../shared/child-process/run-process'
const {
childSpawnMock,
@@ -22,6 +23,13 @@ vi.mock('node:child_process', () => ({
spawn: childSpawnMock
}))
// The chokepoint is the seam: assertions see what the fetcher asked for, before shim resolution.
vi.mock('../../shared/child-process/run-process', async (importOriginal) => ({
...(await importOriginal<typeof RunProcess>()),
spawnProcess: (spec: { program: string; args?: readonly string[] }) =>
childSpawnMock(spec.program, spec.args ?? [], spec)
}))
vi.mock('node:fs/promises', () => ({
readFile: readFileMock
}))
@@ -51,7 +59,6 @@ vi.mock('./codex-auth-presence', () => ({
import { fetchCodexRateLimits } from './codex-fetcher'
import { probeCodexAuthPresence } from './codex-auth-presence'
import { getActiveHiddenRateLimitPtyCount } from './hidden-pty-cleanup'
import { getCmdExePath } from '../win32-utils'
import { CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args'
function makeDisposable() {
@@ -787,13 +794,9 @@ describe('fetchCodexRateLimits', () => {
await resultPromise
const [spawnFile, spawnArgs, spawnOptions] = childSpawnMock.mock.calls[0]
expect(spawnFile).toBe(getCmdExePath())
expect(spawnArgs).toEqual([
'/d',
'/c',
codexCommand,
...CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS
])
// Pre-wrapping in cmd.exe would hide the npm shim from spawnProcess's resolver.
expect(spawnFile).toBe(codexCommand)
expect(spawnArgs).toEqual([...CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS])
expect(spawnOptions).toEqual(
expect.objectContaining({
env: expect.objectContaining({ CODEX_HOME: 'C:\\Users\\alice\\.codex' })
+8 -10
View File
@@ -1,5 +1,4 @@
import type { CodexRateLimitResetOutcome, ProviderRateLimits } from '../../shared/rate-limit-types'
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'
@@ -18,7 +17,8 @@ import {
import { isCodexStateDbBackfillPending } from '../codex/codex-state-db'
import { startCodexStateDbBackfillRecoveryInBackground } from '../codex/codex-state-db-backfill-recovery'
import { withMacTailscaleDnsHint } from '../network/macos-tailscale-dns-diagnostic'
import { getCmdExePath, getSpawnArgsForWindows } from '../win32-utils'
import { spawnProcess } from '../../shared/child-process/run-process'
import { getCmdExePath } from '../win32-utils'
import { probeCodexAuthPresence } from './codex-auth-presence'
import {
fetchCodexRateLimitsViaBackend,
@@ -109,19 +109,17 @@ async function fetchViaRpc(options?: CodexRateLimitFetchOptions): Promise<Provid
? buildWslCodexCommand(options.codexHomePath, codexArgs, true)
: null
const codexCommand = wslCodex ? 'codex' : resolveCodexCommand()
const { spawnCmd, spawnArgs } = wslCodex
? { spawnCmd: wslCodex.command, spawnArgs: wslCodex.args }
: getSpawnArgsForWindows(codexCommand, codexArgs)
const spawnOptions = {
stdio: ['pipe', 'pipe', 'pipe'] as ['pipe', 'pipe', 'pipe'],
// Why the bare CLI: spawnProcess resolves an npm `codex.cmd` shim past cmd.exe itself.
const child = spawnProcess({
program: wslCodex ? wslCodex.command : codexCommand,
args: wslCodex ? wslCodex.args : codexArgs,
stdio: ['pipe', 'pipe', 'pipe'],
cwd: resolveHiddenRateLimitPtyCwd(),
windowsHide: true,
env: withCliRuntimeOnPath(codexCommand, {
...(wslCodex ? processEnvWithoutCodexHome() : process.env),
...(options?.codexHomePath && !wslCodex ? { CODEX_HOME: options.codexHomePath } : {})
})
}
const child = spawn(spawnCmd, spawnArgs, spawnOptions)
})
return readCodexRateLimitsViaRpc({
child: child as CodexRpcRateLimitChild,
codexCommand,
@@ -88,8 +88,8 @@ export async function terminateCodexProbeChild(
}
if (platform === 'win32' && child.pid) {
try {
// npm-installed Codex runs beneath cmd.exe; killing only that wrapper can
// leave app-server alive after the credential-home lock is released.
// npm-installed Codex runs beneath a launcher (node, or cmd.exe for an unresolved
// shim); killing only that wrapper can leave app-server alive after the lock is released.
await (options?.killWindowsProcessTree ?? terminateWindowsProcessTree)(child.pid, {
site: 'codex-rate-limit-probe'
})
@@ -58,7 +58,6 @@ src/main/codex-accounts/legacy-wsl-runtime-auth-drain-script-interference-shims.
src/main/codex-accounts/service.ts
src/main/codex/codex-app-server-posix-supervisor.ts
src/main/codex/codex-app-server-session.ts
src/main/codex/codex-state-db-backfill-recovery.ts
src/main/codex/codex-wsl-hook-install-plan.ts
src/main/computer/desktop-script-provider-bridge.ts
src/main/computer/macos-computer-use-permission-status.ts
@@ -131,7 +130,6 @@ src/main/providers/windows-console-attached-processes.ts
src/main/pty-descendant-termination.ts
src/main/pty/posix-pty-foreground-group.ts
src/main/pty/windows-environment-path.ts
src/main/rate-limits/codex-fetcher.ts
src/main/rate-limits/gemini-cli-oauth-extractor.ts
src/main/runtime/tls-certificate.ts
src/main/runtime/windows-default-route-interfaces.ts
@@ -38,7 +38,6 @@ main/providers/process-cwd.ts
main/pty-descendant-termination.ts
main/pty/posix-pty-foreground-group.ts
main/pty/windows-environment-path.ts
main/rate-limits/codex-fetcher.ts
main/runtime/tls-certificate.ts
main/ssh/ssh-connection.ts
main/startup/ensure-virtual-display.ts
@@ -34,7 +34,7 @@ const ALLOWLIST: readonly string[] = readAllowlist(
* the allowlist does not bound this: a swap (one file fixed and delisted, one
* new file added with its entry) satisfies both membership assertions.
*/
const UNHIDDEN_SPAWNER_PIN = 61
const UNHIDDEN_SPAWNER_PIN = 60
const CHILD_PROCESS_IMPORT =
/from\s+['"](?:node:)?child_process['"]|require\(\s*['"](?:node:)?child_process['"]/