[codex] Centralize subprocess env boundaries (#585)

* Centralize subprocess env boundaries

* Remove env boundary static audit test

* Tighten env boundary process launches

* Fix Windows native process launches

* Inline provider env pass-throughs
This commit is contained in:
Mohamed Boudra
2026-04-27 13:24:16 +08:00
committed by GitHub
parent 90032570ec
commit 15df680a64
40 changed files with 760 additions and 304 deletions
+60 -8
View File
@@ -1,13 +1,27 @@
import { execFile, spawn, type ChildProcess, type SpawnOptions } from "node:child_process";
import { extname } from "node:path";
import { promisify } from "node:util";
import { isWindowsCommandScript, quoteWindowsArgument, quoteWindowsCommand } from "./executable.js";
import { createExternalCommandProcessEnv, type ProcessEnvRecord } from "../server/paseo-env.js";
import {
isWindowsCommandScript,
quoteWindowsArgument,
quoteWindowsCommand,
} from "./windows-command.js";
const execFileAsync = promisify(execFile);
interface ExecCommandOptions {
interface ExternalEnvOptions {
baseEnv?: ProcessEnvRecord;
envMode?: "external" | "internal";
env?: ProcessEnvRecord;
envOverlay?: ProcessEnvRecord;
}
export type SpawnProcessOptions = Omit<SpawnOptions, "env"> & ExternalEnvOptions;
interface ExecCommandOptions extends ExternalEnvOptions {
cwd?: string;
env?: NodeJS.ProcessEnv;
encoding?: BufferEncoding;
timeout?: number;
maxBuffer?: number;
@@ -18,20 +32,48 @@ interface ExecCommandResult {
stderr: string;
}
function hasPathSeparator(value: string): boolean {
return value.includes("/") || value.includes("\\");
}
function shouldUseWindowsShell(
command: string,
requestedShell?: boolean | string,
): boolean | string {
if (isWindowsCommandScript(command)) {
return true;
}
if (requestedShell !== undefined) {
return requestedShell;
}
return process.platform === "win32" && !hasPathSeparator(command) && !extname(command);
}
export function spawnProcess(
command: string,
args: string[],
options?: SpawnOptions,
options?: SpawnProcessOptions,
): ChildProcess {
const { baseEnv, env, envOverlay, ...spawnOptions } = options ?? {};
const resolvedBaseEnv = env ?? baseEnv ?? process.env;
const isWindows = process.platform === "win32";
const shell = isWindowsCommandScript(command) ? true : (options?.shell ?? isWindows);
const shell = shouldUseWindowsShell(command, spawnOptions.shell);
const shouldQuoteForShell = isWindows && shell !== false;
const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command;
const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args;
const childEnv =
options?.envMode === "internal"
? ({ ...resolvedBaseEnv, ...envOverlay } as NodeJS.ProcessEnv)
: createExternalCommandProcessEnv(
command,
resolvedBaseEnv,
...(envOverlay ? [envOverlay] : []),
);
return spawn(resolvedCommand, resolvedArgs, {
...options,
...spawnOptions,
env: childEnv,
shell,
windowsHide: true,
});
@@ -42,15 +84,25 @@ export async function execCommand(
args: string[],
options?: ExecCommandOptions,
): Promise<ExecCommandResult> {
const { baseEnv, env, envOverlay } = options ?? {};
const resolvedBaseEnv = env ?? baseEnv ?? process.env;
const isWindows = process.platform === "win32";
const shell = isWindowsCommandScript(command) ? true : isWindows;
const shell = shouldUseWindowsShell(command);
const shouldQuoteForShell = isWindows && shell !== false;
const resolvedCommand = shouldQuoteForShell ? quoteWindowsCommand(command) : command;
const resolvedArgs = shouldQuoteForShell ? args.map(quoteWindowsArgument) : args;
const childEnv =
options?.envMode === "internal"
? ({ ...resolvedBaseEnv, ...envOverlay } as NodeJS.ProcessEnv)
: createExternalCommandProcessEnv(
command,
resolvedBaseEnv,
...(envOverlay ? [envOverlay] : []),
);
return execFileAsync(resolvedCommand, resolvedArgs, {
cwd: options?.cwd,
env: options?.env,
env: childEnv,
encoding: options?.encoding ?? "utf8",
timeout: options?.timeout,
maxBuffer: options?.maxBuffer,