Files
openclaw/src/plugin-sdk/windows-spawn.ts
Peter Steinberger 0b8aabe864 docs: document auth profile failure policy contract (#89613)
* docs: document markdown marker renderer

* docs: document rendered markdown chunking

* docs: document markdown text chunking

* docs: document shared text chunking

* docs: document plugin text chunking exports

* docs: document avatar policy constants

* docs: document node match candidates

* docs: document scoped expiring id cache

* docs: document runtime import normalization

* docs: document string sample summaries

* docs: document session usage timeseries types

* docs: document session usage response types

* docs: document manifest frontmatter shapes

* docs: document channel route input metadata

* docs: document pair loop guard settings

* docs: document migration config patch helpers

* docs: document api provider registry

* docs: document tool call repair payloads

* docs: document plugin tool payload helpers

* docs: document lazy promise loader

* docs: document store writer queue state

* docs: document thread binding lifecycle

* docs: document concurrency helper contract

* docs: document gateway client info contract

* docs: document delivery context contracts

* docs: document secret ref defaults contract

* docs: document command gating contract

* docs: document avatar policy contract

* docs: document node match policy

* docs: document message channel normalization

* docs: document boolean parsing contract

* docs: document zod parse helpers

* docs: document direct dm guard policy

* docs: document fixed window limiter contract

* docs: document node presence event contract

* docs: document secret normalization contract

* docs: document progress draft line removal

* docs: document usage formatting contracts

* docs: document agent run status contract

* docs: document runtime import helpers

* docs: document provider utility ownership

* docs: document invalid config helpers

* docs: document json compat parser

* docs: document channel config metadata ownership

* docs: document channel logging helpers

* docs: document sender identity validation ownership

* docs: document string sampling helper

* docs: document global singleton helpers

* docs: document transcript tool helpers

* docs: document exec safe-bin normalization

* docs: document reaction level resolver

* docs: document account snapshot redaction boundary

* docs: document messaging target helpers

* docs: document thread binding messages

* docs: document conversation binding context

* docs: document conversation resolution helper

* docs: document owner display secret retention

* docs: document provider request config types

* docs: document skills config types

* docs: document memory config types

* docs: document imessage config types

* docs: document crestodian config types

* docs: document tools config policies

* docs: document shared config base types

* docs: document channel config contracts

* docs: document openclaw config state types

* docs: document model config contracts

* docs: document shared agent config types

* docs: document agent defaults config types

* docs: document secret input contracts

* docs: document auth config contracts

* docs: document gateway config contracts

* docs: document tool call stream repair contracts

* docs: document memory host facades

* docs: document llm core contracts

* docs: document markdown core contracts

* docs: document gateway connect error contracts

* docs: document gateway protocol primitives

* docs: document gateway frame schemas

* docs: document gateway device schemas

* docs: document gateway environment schemas

* docs: document gateway push schemas

* docs: document gateway plugin schemas

* docs: document gateway artifact schemas

* docs: document gateway command schemas

* docs: document gateway task schemas

* docs: document gateway exec approval schemas

* docs: document gateway secret schemas

* docs: document gateway config schemas

* docs: document gateway snapshot schemas

* docs: document gateway chat schemas

* docs: document gateway wizard schemas

* docs: document gateway node schemas

* docs: document gateway plugin approval schemas

* docs: document gateway talk schemas

* docs: document gateway agent schemas

* docs: document gateway session schemas

* docs: document gateway cron schemas

* docs: document gateway agent model skill schemas

* docs: document gateway skill proposal tool schemas

* docs: document gateway protocol registry

* docs: document gateway channel status schemas

* docs: document gateway schema regression tests

* docs: document gateway schema barrel

* docs: document gateway validator tests

* docs: document gateway primitive push tests

* docs: document gateway contract tests

* docs: document native protocol guard

* docs: document channel schema tests

* docs: document gateway protocol smoke tests

* docs: document gateway protocol entrypoint

* docs: document gateway protocol type exports

* docs: document gateway error codes

* docs: document protocol schema registry

* docs: document talk audio codec

* docs: document talk activation names

* docs: document talk consult questions

* docs: document talk consult tool

* docs: document talk run control contracts

* docs: document talk run control adapter

* docs: document talkback consult queue

* docs: document talk consult transcript guard

* docs: document talk fast context runtime

* docs: document forced talk consult coordinator

* docs: document talk output activity tracker

* docs: document talk event metrics

* docs: document talk diagnostics

* docs: document talk observability hook

* docs: document talk provider resolver

* docs: document talk provider registry

* docs: document talk runtime primitives

* docs: document talk consult controller logs

* docs: document channel identity helpers

* docs: document channel account allowlist helpers

* docs: document channel metadata draft controls

* docs: document channel ingress policy

* docs: document channel sender access gates

* docs: document channel catalog message contracts

* docs: document channel account plugin helpers

* docs: document configured binding helpers

* docs: document channel acp approval config helpers

* docs: document channel bundled config write helpers

* docs: document channel plugin utility contracts

* docs: document channel config access helpers

* docs: document channel message action helpers

* docs: document channel outbound runtime helpers

* docs: document channel pairing promotion helpers

* docs: document channel registry helpers

* docs: document channel setup wizard helpers

* docs: document channel lifecycle status helpers

* docs: document channel target thread helpers

* docs: document channel session binding helpers

* docs: document channel package module probes

* docs: document channel setup wizard contracts

* docs: document channel plugin API barrels

* docs: document channel contract test helpers

* docs: document channel core helpers

* docs: document small core facades

* docs: document provider runtime helpers

* docs: document persistence and realtime helpers

* docs: document mcp and state helpers

* docs: document tool planner contracts

* docs: document music generation runtime

* docs: document crestodian command flow

* docs: document utility helpers

* docs: document node host helpers

* docs: document transcript contracts

* docs: document trajectory export contracts

* docs: document image generation contracts

* docs: document routing helper contracts

* docs: document session helper contracts

* docs: document video generation contracts

* docs: document model catalog contracts

* docs: document proxy capture contracts

* docs: document status rendering contracts

* docs: document test helper contracts

* docs: document wizard setup contracts

* docs: document process contracts

* docs: document memory host sdk contracts

* docs: document tts contracts

* docs: document secrets runtime contracts

* docs: document shared helper contracts

* docs: document hook runtime contracts

* docs: document security audit contracts

* docs: document flow contracts

* docs: document media understanding contracts

* docs: document tui contracts

* docs: document logging contracts

* docs: document llm contracts

* docs: document cron contracts

* docs: document daemon contracts

* docs: document task contracts

* docs: document acp contracts

* docs: document test utility contracts

* docs: document skill contracts

* docs: document config contracts

* docs: document outbound infra contracts

* docs: document command analysis contracts

* docs: document provider usage infra contracts

* docs: document file safety infra contracts

* docs: document exec approval infra contracts

* docs: document gateway runtime infra contracts

* docs: document infra utility contracts

* docs: document infra queue storage contracts

* docs: document heartbeat infra contracts

* docs: document remaining infra contracts

* docs: document gateway auth contracts

* docs: document gateway display helpers

* docs: document gateway http helpers

* docs: document gateway node helpers

* docs: document gateway mcp helpers

* docs: document gateway support helpers

* docs: document gateway server runtime helpers

* docs: document gateway runtime bootstrap helpers

* docs: document gateway session events

* docs: document gateway utility helpers

* docs: document gateway talk helpers

* docs: document gateway helper contracts

* docs: document gateway server method helpers

* docs: document gateway server auth helpers

* docs: document gateway server tests

* docs: document gateway test helpers

* docs: document gateway node tests

* docs: document gateway channel tests

* docs: document gateway session tests

* docs: document gateway server startup tests

* docs: document gateway tool test helpers

* docs: document gateway server test helpers

* docs: document gateway server method tests

* docs: document remaining gateway tests

* docs: document plugin sdk public subpaths

* docs: document plugin sdk runtime helpers

* docs: document plugin sdk memory provider helpers

* docs: document plugin sdk runtime facades

* docs: document plugin sdk command approval helpers

* docs: document plugin sdk runtime types

* docs: document plugin sdk browser account helpers

* docs: document plugin sdk media memory helpers

* docs: document plugin sdk core tests

* docs: document plugin sdk contract helpers

* docs: document plugin sdk test helpers

* docs: document remaining plugin sdk tests

* docs: document cli utility helpers

* docs: document cli runtime helpers

* docs: document cli command registration helpers

* docs: document node cli helpers

* docs: document cli program registration

* docs: document message cli registration

* docs: document daemon cli helpers

* docs: document cli route parsers
2026-06-03 15:20:39 -07:00

393 lines
12 KiB
TypeScript

import { readFileSync, statSync } from "node:fs";
import path from "node:path";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../../packages/normalization-core/src/string-coerce.js";
import { normalizeStringEntries } from "../../packages/normalization-core/src/string-normalization.js";
/** Final execution strategy chosen for a Windows spawn command. */
export type WindowsSpawnResolution =
| "direct"
| "node-entrypoint"
| "exe-entrypoint"
| "shell-fallback";
export type WindowsSpawnCandidateResolution = Exclude<WindowsSpawnResolution, "shell-fallback">;
/** Direct-spawn candidate before shell fallback policy is applied. */
export type WindowsSpawnProgramCandidate = {
/** Executable passed to child_process after wrapper resolution. */
command: string;
/** Arguments prepended before call-site argv, usually a resolved JS entrypoint. */
leadingArgv: string[];
/** Candidate resolution path, or unresolved-wrapper when shell policy must decide. */
resolution: WindowsSpawnCandidateResolution | "unresolved-wrapper";
/** Hide the transient Windows console for Node/exe entrypoint launches. */
windowsHide?: boolean;
};
/** Spawn program after Windows wrapper resolution and fallback policy. */
export type WindowsSpawnProgram = {
command: string;
leadingArgv: string[];
resolution: WindowsSpawnResolution;
shell?: boolean;
windowsHide?: boolean;
};
/** Fully materialized child_process invocation for a resolved Windows spawn program. */
export type WindowsSpawnInvocation = {
command: string;
argv: string[];
resolution: WindowsSpawnResolution;
shell?: boolean;
windowsHide?: boolean;
};
/** Inputs used to resolve a command into a Windows-safe direct spawn program. */
export type ResolveWindowsSpawnProgramParams = {
command: string;
platform?: NodeJS.Platform;
env?: NodeJS.ProcessEnv;
execPath?: string;
packageName?: string;
/** Trusted compatibility escape hatch for callers that intentionally accept shell-mediated wrapper execution. */
allowShellFallback?: boolean;
};
export type ResolveWindowsSpawnProgramCandidateParams = Omit<
ResolveWindowsSpawnProgramParams,
"allowShellFallback"
>;
export type WindowsSpawnCommandInlineArgs = {
executable: string;
arguments: string;
};
const INLINE_ARGUMENT_EXECUTABLES = new Set([
"node",
"node.exe",
"npm",
"npm.cmd",
"npm.exe",
"npx",
"npx.cmd",
"npx.exe",
"pnpm",
"pnpm.cmd",
"pnpm.exe",
"yarn",
"yarn.cmd",
"yarn.exe",
]);
function isFilePath(candidate: string): boolean {
try {
return statSync(candidate).isFile();
} catch {
return false;
}
}
function readCommandToken(command: string): { token: string; rest: string } | null {
const trimmed = command.trim();
if (!trimmed) {
return null;
}
if (trimmed.startsWith('"')) {
const closeIndex = trimmed.indexOf('"', 1);
if (closeIndex <= 0) {
return null;
}
return {
token: trimmed.slice(1, closeIndex),
rest: trimmed.slice(closeIndex + 1).trim(),
};
}
const match = trimmed.match(/^(\S+)\s+(.+)$/);
if (!match) {
return null;
}
return {
token: match[1] ?? "",
rest: (match[2] ?? "").trim(),
};
}
export function detectWindowsSpawnCommandInlineArgs(
command: string,
): WindowsSpawnCommandInlineArgs | null {
const parsed = readCommandToken(command);
if (!parsed?.rest) {
return null;
}
const normalizedToken = parsed.token.replace(/\\/g, "/");
const executable = normalizeLowercaseStringOrEmpty(path.posix.basename(normalizedToken));
if (!INLINE_ARGUMENT_EXECUTABLES.has(executable)) {
return null;
}
return {
executable: parsed.token,
arguments: parsed.rest,
};
}
/** Resolve a Windows command name through PATH and PATHEXT so wrapper inspection sees the real file. */
export function resolveWindowsExecutablePath(command: string, env: NodeJS.ProcessEnv): string {
if (command.includes("/") || command.includes("\\") || path.isAbsolute(command)) {
return command;
}
const pathValue = env.PATH ?? env.Path ?? process.env.PATH ?? process.env.Path ?? "";
const pathEntries = normalizeStringEntries(pathValue.split(";"));
const hasExtension = path.extname(command).length > 0;
const pathExtRaw =
env.PATHEXT ??
env.Pathext ??
process.env.PATHEXT ??
process.env.Pathext ??
".EXE;.CMD;.BAT;.COM";
const pathExt = hasExtension
? [""]
: normalizeStringEntries(pathExtRaw.split(";")).map((ext) =>
ext.startsWith(".") ? ext : `.${ext}`,
);
for (const dir of pathEntries) {
for (const ext of pathExt) {
const normalizedExt = normalizeLowercaseStringOrEmpty(ext);
const uppercaseExt = ext.toUpperCase();
for (const candidateExt of [ext, normalizedExt, uppercaseExt]) {
const candidate = path.join(dir, `${command}${candidateExt}`);
if (isFilePath(candidate)) {
return candidate;
}
}
}
}
return command;
}
function resolveEntrypointFromCmdShim(wrapperPath: string): string | null {
if (!isFilePath(wrapperPath)) {
return null;
}
try {
const content = readFileSync(wrapperPath, "utf8");
const candidates: string[] = [];
for (const match of content.matchAll(/"([^"\r\n]*)"/g)) {
const token = match[1] ?? "";
const relMatch = token.match(/%~?dp0%?\s*[\\/]*(.*)$/i);
const relative = relMatch?.[1]?.trim();
if (!relative) {
continue;
}
const normalizedRelative = relative.replace(/[\\/]+/g, path.sep).replace(/^[\\/]+/, "");
const candidate = path.resolve(path.dirname(wrapperPath), normalizedRelative);
if (isFilePath(candidate)) {
candidates.push(candidate);
}
}
const nonNode = candidates.find((candidate) => {
const base = normalizeLowercaseStringOrEmpty(path.basename(candidate));
return base !== "node.exe" && base !== "node";
});
return nonNode ?? null;
} catch {
return null;
}
}
function resolveBinEntry(
packageName: string | undefined,
binField: string | Record<string, string> | undefined,
): string | null {
if (typeof binField === "string") {
const trimmed = normalizeOptionalString(binField);
return trimmed || null;
}
if (!binField || typeof binField !== "object") {
return null;
}
if (packageName) {
const preferred = binField[packageName];
const normalizedPreferred =
typeof preferred === "string" ? normalizeOptionalString(preferred) : undefined;
if (normalizedPreferred) {
return normalizedPreferred;
}
}
for (const value of Object.values(binField)) {
const normalizedValue = typeof value === "string" ? normalizeOptionalString(value) : undefined;
if (normalizedValue) {
return normalizedValue;
}
}
return null;
}
function resolveEntrypointFromPackageJson(
wrapperPath: string,
packageName?: string,
): string | null {
if (!packageName) {
return null;
}
const wrapperDir = path.dirname(wrapperPath);
const packageDirs = [
path.resolve(wrapperDir, "..", packageName),
path.resolve(wrapperDir, "node_modules", packageName),
];
for (const packageDir of packageDirs) {
const packageJsonPath = path.join(packageDir, "package.json");
if (!isFilePath(packageJsonPath)) {
continue;
}
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
bin?: string | Record<string, string>;
};
const entryRel = resolveBinEntry(packageName, packageJson.bin);
if (!entryRel) {
continue;
}
const entryPath = path.resolve(packageDir, entryRel);
if (isFilePath(entryPath)) {
return entryPath;
}
} catch {
// Ignore malformed package metadata.
}
}
return null;
}
/** Resolve the safest direct spawn candidate for Windows wrappers, scripts, and binaries. */
export function resolveWindowsSpawnProgramCandidate(
params: ResolveWindowsSpawnProgramCandidateParams,
): WindowsSpawnProgramCandidate {
const platform = params.platform ?? process.platform;
const env = params.env ?? process.env;
const execPath = params.execPath ?? process.execPath;
if (platform !== "win32") {
return {
command: params.command,
leadingArgv: [],
resolution: "direct",
};
}
const inlineArgs = detectWindowsSpawnCommandInlineArgs(params.command);
if (inlineArgs) {
throw new Error(
`Windows spawn command must be an executable path only; "${inlineArgs.executable}" was configured with inline arguments "${inlineArgs.arguments}". Put arguments in the caller's args array instead.`,
);
}
const resolvedCommand = resolveWindowsExecutablePath(params.command, env);
const ext = normalizeLowercaseStringOrEmpty(path.extname(resolvedCommand));
if (ext === ".js" || ext === ".cjs" || ext === ".mjs") {
return {
command: execPath,
leadingArgv: [resolvedCommand],
resolution: "node-entrypoint",
windowsHide: true,
};
}
if (ext === ".cmd" || ext === ".bat") {
const entrypoint =
resolveEntrypointFromCmdShim(resolvedCommand) ??
resolveEntrypointFromPackageJson(resolvedCommand, params.packageName);
if (entrypoint) {
const entryExt = normalizeLowercaseStringOrEmpty(path.extname(entrypoint));
if (entryExt === ".exe") {
return {
command: entrypoint,
leadingArgv: [],
resolution: "exe-entrypoint",
windowsHide: true,
};
}
return {
command: execPath,
leadingArgv: [entrypoint],
resolution: "node-entrypoint",
windowsHide: true,
};
}
// Unresolved .cmd/.bat wrappers are not passed through cmd.exe unless the
// caller explicitly accepts shell metacharacter parsing with allowShellFallback.
return {
command: resolvedCommand,
leadingArgv: [],
resolution: "unresolved-wrapper",
};
}
return {
command: resolvedCommand,
leadingArgv: [],
resolution: "direct",
};
}
/** Apply shell-fallback policy when Windows wrapper resolution could not find a direct entrypoint. */
export function applyWindowsSpawnProgramPolicy(params: {
candidate: WindowsSpawnProgramCandidate;
allowShellFallback?: boolean;
}): WindowsSpawnProgram {
if (params.candidate.resolution !== "unresolved-wrapper") {
return {
command: params.candidate.command,
leadingArgv: params.candidate.leadingArgv,
resolution: params.candidate.resolution,
windowsHide: params.candidate.windowsHide,
};
}
if (params.allowShellFallback === true) {
return {
command: params.candidate.command,
leadingArgv: [],
resolution: "shell-fallback",
shell: true,
};
}
throw new Error(
`${path.basename(params.candidate.command)} wrapper resolved, but no executable/Node entrypoint could be resolved without shell execution.`,
);
}
/** Resolve the final Windows spawn program after candidate discovery and fallback policy. */
export function resolveWindowsSpawnProgram(
params: ResolveWindowsSpawnProgramParams,
): WindowsSpawnProgram {
const candidate = resolveWindowsSpawnProgramCandidate(params);
return applyWindowsSpawnProgramPolicy({
candidate,
allowShellFallback: params.allowShellFallback,
});
}
/** Combine a resolved Windows spawn program with call-site argv for actual process launch. */
export function materializeWindowsSpawnProgram(
program: WindowsSpawnProgram,
argv: string[],
): WindowsSpawnInvocation {
return {
command: program.command,
argv: [...program.leadingArgv, ...argv],
resolution: program.resolution,
shell: program.shell,
windowsHide: program.windowsHide,
};
}