mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 16:31:34 +00:00
* feat(runtime): allow Bun runtimes that provide node:sqlite * fix(process): drop execa buffer encoding under Bun spawn (Bun rejects non-spawn options) * chore(process): cite oven-sh/bun#36049 in bun spawn workaround * docs(install): bun with node:sqlite can run openclaw; bun install workspace caveat * fix(process): clear execa buffer encoding under Bun without mutating read-only options
139 lines
4.8 KiB
TypeScript
139 lines
4.8 KiB
TypeScript
import path from "node:path";
|
|
import process from "node:process";
|
|
import { execa, type Options as ExecaOptions, type ResultPromise } from "execa";
|
|
import { markOpenClawExecEnv } from "../infra/openclaw-exec-env.js";
|
|
import { resolveSafeChildProcessInvocation } from "./windows-command.js";
|
|
|
|
export const COMMAND_PROCESS_TREE_KILL_GRACE_MS = 300;
|
|
|
|
function assignChildEnvValue(params: {
|
|
env: NodeJS.ProcessEnv;
|
|
key: string;
|
|
platform: NodeJS.Platform;
|
|
value: string | undefined;
|
|
}): void {
|
|
if (params.platform === "win32") {
|
|
const normalizedKey = params.key.toLowerCase();
|
|
for (const existingKey of Object.keys(params.env)) {
|
|
if (existingKey.toLowerCase() === normalizedKey && existingKey !== params.key) {
|
|
delete params.env[existingKey];
|
|
}
|
|
}
|
|
}
|
|
if (params.value === undefined) {
|
|
delete params.env[params.key];
|
|
return;
|
|
}
|
|
params.env[params.key] = params.value;
|
|
}
|
|
|
|
function mergeChildEnv(params: {
|
|
baseEnv: NodeJS.ProcessEnv;
|
|
env?: NodeJS.ProcessEnv;
|
|
platform: NodeJS.Platform;
|
|
}): NodeJS.ProcessEnv {
|
|
const resolvedEnv: NodeJS.ProcessEnv = {};
|
|
for (const [key, value] of Object.entries(params.baseEnv)) {
|
|
assignChildEnvValue({ env: resolvedEnv, key, platform: params.platform, value });
|
|
}
|
|
for (const [key, value] of Object.entries(params.env ?? {})) {
|
|
assignChildEnvValue({ env: resolvedEnv, key, platform: params.platform, value });
|
|
}
|
|
return resolvedEnv;
|
|
}
|
|
|
|
export function shouldSpawnWithShell(params: {
|
|
resolvedCommand: string;
|
|
platform: NodeJS.Platform;
|
|
}): boolean {
|
|
// SECURITY: never enable `shell` for argv-based execution.
|
|
// `shell` routes through cmd.exe on Windows, which turns untrusted argv values
|
|
// (like chat prompts passed as CLI args) into command-injection primitives.
|
|
// If you need a shell, use an explicit shell-wrapper argv (e.g. `cmd.exe /c ...`)
|
|
// and validate/escape at the call site.
|
|
void params;
|
|
return false;
|
|
}
|
|
|
|
type SpawnCommandOptions = ExecaOptions & {
|
|
baseEnv?: NodeJS.ProcessEnv;
|
|
};
|
|
|
|
export function spawnCommandWithInvocation<
|
|
OptionsType extends SpawnCommandOptions = SpawnCommandOptions,
|
|
>(
|
|
argv: string[],
|
|
options: OptionsType = {} as OptionsType,
|
|
): {
|
|
child: ResultPromise<OptionsType>;
|
|
invocation: ReturnType<typeof resolveSafeChildProcessInvocation>;
|
|
} {
|
|
const { baseEnv, env, windowsVerbatimArguments, ...execaOptions } = options;
|
|
// Bun workaround (oven-sh/bun#36049): execa forwards its whole options object to
|
|
// child_process.spawn. Node ignores the extra `encoding` key there, but Bun's
|
|
// spawn feeds it into its stream constructor and throws ERR_UNKNOWN_ENCODING for
|
|
// execa's "buffer". Clearing it under Bun means buffered results arrive as utf8
|
|
// strings instead of Buffers; callers already normalize via Buffer.from. Binary-
|
|
// exact output paths are unverified under Bun. Remove once the Bun issue is fixed.
|
|
const dropBufferEncodingForBun =
|
|
Boolean(process.versions.bun) && execaOptions.encoding === "buffer";
|
|
const commandEnv = resolveCommandEnv({ argv, baseEnv, env });
|
|
const invocation = resolveSafeChildProcessInvocation({
|
|
argv,
|
|
cwd: execaOptions.cwd,
|
|
env: commandEnv,
|
|
windowsVerbatimArguments,
|
|
});
|
|
const child = execa(invocation.command, invocation.args, {
|
|
...execaOptions,
|
|
...(dropBufferEncodingForBun ? { encoding: undefined } : {}),
|
|
env: commandEnv,
|
|
extendEnv: false,
|
|
shell: false,
|
|
windowsHide: invocation.windowsHide,
|
|
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
} as ExecaOptions) as unknown as ResultPromise<OptionsType>;
|
|
return { child, invocation };
|
|
}
|
|
|
|
/** Spawn through the canonical argv, environment, and Windows safety boundary. */
|
|
export function spawnCommand<OptionsType extends SpawnCommandOptions = SpawnCommandOptions>(
|
|
argv: string[],
|
|
options: OptionsType = {} as OptionsType,
|
|
): ResultPromise<OptionsType> {
|
|
return spawnCommandWithInvocation(argv, options).child;
|
|
}
|
|
|
|
export function resolveCommandEnv(params: {
|
|
argv: string[];
|
|
env?: NodeJS.ProcessEnv;
|
|
baseEnv?: NodeJS.ProcessEnv;
|
|
platform?: NodeJS.Platform;
|
|
}): NodeJS.ProcessEnv {
|
|
const baseEnv = params.baseEnv ?? process.env;
|
|
const platform = params.platform ?? process.platform;
|
|
const argv = params.argv;
|
|
const shouldSuppressNpmFund = (() => {
|
|
const cmd = path.basename(argv[0] ?? "");
|
|
if (cmd === "npm" || cmd === "npm.cmd" || cmd === "npm.exe") {
|
|
return true;
|
|
}
|
|
if (cmd === "node" || cmd === "node.exe") {
|
|
const script = argv[1] ?? "";
|
|
return script.includes("npm-cli.js");
|
|
}
|
|
return false;
|
|
})();
|
|
|
|
const resolvedEnv = mergeChildEnv({ baseEnv, env: params.env, platform });
|
|
if (shouldSuppressNpmFund) {
|
|
if (resolvedEnv.NPM_CONFIG_FUND == null) {
|
|
resolvedEnv.NPM_CONFIG_FUND = "false";
|
|
}
|
|
if (resolvedEnv.npm_config_fund == null) {
|
|
resolvedEnv.npm_config_fund = "false";
|
|
}
|
|
}
|
|
return markOpenClawExecEnv(resolvedEnv);
|
|
}
|