Files
openclaw/src/agents/bash-tools.exec-script-target.ts
Peter Steinberger 73bead1239 refactor(agents): split exec tool pipeline (#113807)
* refactor(agents): split exec tool pipeline

* chore(agents): keep exec prep type private
2026-07-25 13:58:01 -07:00

298 lines
7.8 KiB
TypeScript

/** Parses direct Python and Node script targets from shell command text. */
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalLowercaseString,
} from "@openclaw/normalization-core/string-coerce";
import { splitShellArgs } from "../utils/shell-argv.js";
const PREFLIGHT_ENV_OPTIONS_WITH_VALUES = new Set([
"-C",
"-S",
"-u",
"--argv0",
"--block-signal",
"--chdir",
"--default-signal",
"--ignore-signal",
"--split-string",
"--unset",
]);
function isShellEnvAssignmentToken(token: string): boolean {
return /^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(token);
}
function isEnvExecutableToken(token: string | undefined): boolean {
if (!token) {
return false;
}
const base = normalizeOptionalLowercaseString(token.split(/[\\/]/u).at(-1)) ?? "";
const normalizedBase = base.endsWith(".exe") ? base.slice(0, -4) : base;
return normalizedBase === "env";
}
export function stripPreflightEnvPrefix(argv: string[]): string[] {
if (argv.length === 0) {
return argv;
}
let idx = 0;
while (idx < argv.length && isShellEnvAssignmentToken(argv.at(idx) ?? "")) {
idx += 1;
}
if (!isEnvExecutableToken(argv[idx])) {
return argv;
}
idx += 1;
while (idx < argv.length) {
const token = argv.at(idx);
if (token === undefined) {
break;
}
if (token === "--") {
idx += 1;
break;
}
if (isShellEnvAssignmentToken(token)) {
idx += 1;
continue;
}
if (!token.startsWith("-") || token === "-") {
break;
}
idx += 1;
const equalsIndex = token.indexOf("=");
const option = token.includes("=") ? token.slice(0, equalsIndex) : token;
if (
PREFLIGHT_ENV_OPTIONS_WITH_VALUES.has(option) &&
!token.includes("=") &&
idx < argv.length
) {
idx += 1;
}
}
return argv.slice(idx);
}
function findFirstPythonScriptArg(tokens: string[]): string | null {
const optionsWithSeparateValue = new Set(["-W", "-X", "-Q", "--check-hash-based-pycs"]);
for (let i = 0; i < tokens.length; i += 1) {
const token = tokens.at(i);
if (token === undefined) {
break;
}
if (token === "--") {
const next = tokens.at(i + 1);
return next && normalizeLowercaseStringOrEmpty(next).endsWith(".py") ? next : null;
}
if (token === "-") {
return null;
}
if (token === "-c" || token === "-m") {
return null;
}
if ((token.startsWith("-c") || token.startsWith("-m")) && token.length > 2) {
return null;
}
if (optionsWithSeparateValue.has(token)) {
i += 1;
continue;
}
if (token.startsWith("-")) {
continue;
}
return normalizeLowercaseStringOrEmpty(token).endsWith(".py") ? token : null;
}
return null;
}
function findNodeScriptArgs(tokens: string[]): string[] {
const optionsWithSeparateValue = new Set(["-r", "--require", "--import"]);
const preloadScripts: string[] = [];
let entryScript: string | null = null;
let hasInlineEvalOrPrint = false;
for (let i = 0; i < tokens.length; i += 1) {
const token = tokens.at(i);
if (token === undefined) {
break;
}
if (token === "--") {
if (!hasInlineEvalOrPrint && !entryScript) {
const next = tokens.at(i + 1);
if (next && normalizeLowercaseStringOrEmpty(next).endsWith(".js")) {
entryScript = next;
}
}
break;
}
if (
token === "-e" ||
token === "-p" ||
token === "--eval" ||
token === "--print" ||
token.startsWith("--eval=") ||
token.startsWith("--print=") ||
((token.startsWith("-e") || token.startsWith("-p")) && token.length > 2)
) {
hasInlineEvalOrPrint = true;
if (token === "-e" || token === "-p" || token === "--eval" || token === "--print") {
i += 1;
}
continue;
}
if (optionsWithSeparateValue.has(token)) {
const next = tokens.at(i + 1);
if (next && normalizeLowercaseStringOrEmpty(next).endsWith(".js")) {
preloadScripts.push(next);
}
i += 1;
continue;
}
if (
(token.startsWith("-r") && token.length > 2) ||
token.startsWith("--require=") ||
token.startsWith("--import=")
) {
const inlineValue = token.startsWith("-r")
? token.slice(2)
: token.slice(token.indexOf("=") + 1);
if (normalizeLowercaseStringOrEmpty(inlineValue).endsWith(".js")) {
preloadScripts.push(inlineValue);
}
continue;
}
if (token.startsWith("-")) {
continue;
}
if (
!hasInlineEvalOrPrint &&
!entryScript &&
normalizeLowercaseStringOrEmpty(token).endsWith(".js")
) {
entryScript = token;
}
break;
}
const targets = [...preloadScripts];
if (entryScript) {
targets.push(entryScript);
}
return targets;
}
function extractInterpreterScriptTargetFromArgv(
argv: string[] | null,
): { kind: "python"; relOrAbsPaths: string[] } | { kind: "node"; relOrAbsPaths: string[] } | null {
if (!argv || argv.length === 0) {
return null;
}
let commandIdx = 0;
while (
commandIdx < argv.length &&
/^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(argv.at(commandIdx) ?? "")
) {
commandIdx += 1;
}
const executable = normalizeOptionalLowercaseString(argv.at(commandIdx));
if (!executable) {
return null;
}
const args = argv.slice(commandIdx + 1);
if (/^python(?:3(?:\.\d+)?)?$/i.test(executable)) {
const script = findFirstPythonScriptArg(args);
return script ? { kind: "python", relOrAbsPaths: [script] } : null;
}
if (executable === "node") {
const scripts = findNodeScriptArgs(args);
return scripts.length > 0 ? { kind: "node", relOrAbsPaths: scripts } : null;
}
return null;
}
export function extractInterpreterScriptPathsFromSegment(rawSegment: string): string[] {
const argv = splitShellArgs(rawSegment.trim());
if (!argv || argv.length === 0) {
return [];
}
const withoutLeadingKeyword = /^(?:if|then|do|elif|else|while|until|time)$/i.test(argv[0] ?? "")
? argv.slice(1)
: argv;
const target = extractInterpreterScriptTargetFromArgv(
stripPreflightEnvPrefix(withoutLeadingKeyword),
);
return target?.relOrAbsPaths ?? [];
}
export function extractScriptTargetFromCommand(
command: string,
): { kind: "python"; relOrAbsPaths: string[] } | { kind: "node"; relOrAbsPaths: string[] } | null {
const raw = command.trim();
const splitShellArgsPreservingBackslashes = (value: string): string[] | null => {
const tokens: string[] = [];
let buf = "";
let inSingle = false;
let inDouble = false;
const pushToken = () => {
if (buf.length > 0) {
tokens.push(buf);
buf = "";
}
};
for (const ch of value) {
if (inSingle) {
if (ch === "'") {
inSingle = false;
} else {
buf += ch;
}
continue;
}
if (inDouble) {
if (ch === '"') {
inDouble = false;
} else {
buf += ch;
}
continue;
}
if (ch === "'") {
inSingle = true;
continue;
}
if (ch === '"') {
inDouble = true;
continue;
}
if (/\s/.test(ch)) {
pushToken();
continue;
}
buf += ch;
}
if (inSingle || inDouble) {
return null;
}
pushToken();
return tokens;
};
const shouldUseWindowsPathTokenizer =
process.platform === "win32" &&
/(?:^|[\s"'`])(?:[A-Za-z]:\\|\\\\|[^\s"'`|&;()<>]+\\[^\s"'`|&;()<>]+)/.test(raw);
const candidateArgv = shouldUseWindowsPathTokenizer
? [splitShellArgsPreservingBackslashes(raw)]
: [splitShellArgs(raw)];
for (const argv of candidateArgv) {
const attempts = [argv, argv ? stripPreflightEnvPrefix(argv) : null];
for (const attempt of attempts) {
const target = extractInterpreterScriptTargetFromArgv(attempt);
if (target) {
return target;
}
}
}
return null;
}