mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-04 22:51:39 +00:00
* refactor(agents): split exec tool pipeline * chore(agents): keep exec prep type private
298 lines
7.8 KiB
TypeScript
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;
|
|
}
|