mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
Tests: fix boundary drift and shell preflight regressions
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
"ignoreDeprecations": "6.0",
|
||||
"noEmit": false,
|
||||
"noEmitOnError": false,
|
||||
"outDir": "dist/packages/plugin-sdk/src",
|
||||
"outDir": "dist",
|
||||
"rootDir": "../.."
|
||||
},
|
||||
"include": [
|
||||
@@ -32,6 +32,8 @@
|
||||
"../../src/plugin-sdk/telegram-command-config.ts",
|
||||
"../../src/plugin-sdk/testing.ts",
|
||||
"../../src/plugin-sdk/video-generation.ts",
|
||||
"../../src/video-generation/dashscope-compatible.ts",
|
||||
"../../src/video-generation/types.ts",
|
||||
"../../src/types/**/*.d.ts"
|
||||
],
|
||||
"exclude": [
|
||||
|
||||
@@ -787,10 +787,11 @@ describeWin("exec script preflight on windows path syntax", () => {
|
||||
describe("exec interpreter heuristics ReDoS guard", () => {
|
||||
it("does not hang on long commands with VAR=value assignments and whitespace-heavy text", async () => {
|
||||
const tool = createExecTool({ host: "gateway", security: "full", ask: "off" });
|
||||
// Simulate a heredoc with HTML content after a VAR= assignment — the pattern
|
||||
// that triggers catastrophic backtracking when .* is used instead of \S*
|
||||
// Simulate a heredoc with HTML content after a VAR= assignment. Keep the
|
||||
// command-substitution failure local so the test measures parser behavior,
|
||||
// not external network timing.
|
||||
const htmlBlock = '<section style="padding: 30px 20px; font-family: Arial;">'.repeat(50);
|
||||
const command = `ACCESS_TOKEN=$(curl -s https://api.example.com/token)\ncat > /tmp/out.html << 'EOF'\n${htmlBlock}\nEOF`;
|
||||
const command = `ACCESS_TOKEN=$(__openclaw_missing_redos_guard__)\ncat > /tmp/out.html << 'EOF'\n${htmlBlock}\nEOF`;
|
||||
|
||||
const start = Date.now();
|
||||
// The command itself will fail — we only care that the interpreter
|
||||
|
||||
+366
-244
@@ -151,6 +151,148 @@ function stripPreflightEnvPrefix(argv: string[]): string[] {
|
||||
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[i];
|
||||
if (token === "--") {
|
||||
const next = tokens[i + 1];
|
||||
return next?.toLowerCase().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 token.toLowerCase().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[i];
|
||||
if (token === "--") {
|
||||
if (!hasInlineEvalOrPrint && !entryScript) {
|
||||
const next = tokens[i + 1];
|
||||
if (next?.toLowerCase().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[i + 1];
|
||||
if (next?.toLowerCase().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 (inlineValue.toLowerCase().endsWith(".js")) {
|
||||
preloadScripts.push(inlineValue);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
if (!hasInlineEvalOrPrint && !entryScript && token.toLowerCase().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[commandIdx])) {
|
||||
commandIdx += 1;
|
||||
}
|
||||
const executable = argv[commandIdx]?.toLowerCase();
|
||||
if (!executable) {
|
||||
return null;
|
||||
}
|
||||
const args = argv.slice(commandIdx + 1);
|
||||
if (/^python(?:3(?:\.\d+)?)?$/i.test(executable)) {
|
||||
const script = findFirstPythonScriptArg(args);
|
||||
if (script) {
|
||||
return { kind: "python", relOrAbsPaths: [script] };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (executable === "node") {
|
||||
const scripts = findNodeScriptArgs(args);
|
||||
if (scripts.length > 0) {
|
||||
return { kind: "node", relOrAbsPaths: scripts };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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 ?? [];
|
||||
}
|
||||
|
||||
function extractScriptTargetFromCommand(
|
||||
command: string,
|
||||
): { kind: "python"; relOrAbsPaths: string[] } | { kind: "node"; relOrAbsPaths: string[] } | null {
|
||||
@@ -214,139 +356,10 @@ function extractScriptTargetFromCommand(
|
||||
? [splitShellArgsPreservingBackslashes(raw)]
|
||||
: [splitShellArgs(raw)];
|
||||
|
||||
const 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[i];
|
||||
if (token === "--") {
|
||||
const next = tokens[i + 1];
|
||||
return next?.toLowerCase().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 token.toLowerCase().endsWith(".py") ? token : null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const 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[i];
|
||||
if (token === "--") {
|
||||
if (!hasInlineEvalOrPrint && !entryScript) {
|
||||
const next = tokens[i + 1];
|
||||
if (next?.toLowerCase().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[i + 1];
|
||||
if (next?.toLowerCase().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 (inlineValue.toLowerCase().endsWith(".js")) {
|
||||
preloadScripts.push(inlineValue);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (token.startsWith("-")) {
|
||||
continue;
|
||||
}
|
||||
if (!hasInlineEvalOrPrint && !entryScript && token.toLowerCase().endsWith(".js")) {
|
||||
entryScript = token;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const targets = [...preloadScripts];
|
||||
if (entryScript) {
|
||||
targets.push(entryScript);
|
||||
}
|
||||
return targets;
|
||||
};
|
||||
const extractTargetFromArgv = (
|
||||
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[commandIdx])) {
|
||||
commandIdx += 1;
|
||||
}
|
||||
const executable = argv[commandIdx]?.toLowerCase();
|
||||
if (!executable) {
|
||||
return null;
|
||||
}
|
||||
const args = argv.slice(commandIdx + 1);
|
||||
if (/^python(?:3(?:\.\d+)?)?$/i.test(executable)) {
|
||||
const script = findFirstPythonScriptArg(args);
|
||||
if (script) {
|
||||
return { kind: "python", relOrAbsPaths: [script] };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (executable === "node") {
|
||||
const scripts = findNodeScriptArgs(args);
|
||||
if (scripts.length > 0) {
|
||||
return { kind: "node", relOrAbsPaths: scripts };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
for (const argv of candidateArgv) {
|
||||
const attempts = [argv, argv ? stripPreflightEnvPrefix(argv) : null];
|
||||
for (const attempt of attempts) {
|
||||
const target = extractTargetFromArgv(attempt);
|
||||
const target = extractInterpreterScriptTargetFromArgv(attempt);
|
||||
if (target) {
|
||||
return target;
|
||||
}
|
||||
@@ -410,6 +423,212 @@ function extractUnquotedShellText(raw: string): string | null {
|
||||
return out;
|
||||
}
|
||||
|
||||
function splitShellSegmentsOutsideQuotes(
|
||||
rawText: string,
|
||||
params: { splitPipes: boolean },
|
||||
): string[] {
|
||||
const segments: string[] = [];
|
||||
let buf = "";
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let escaped = false;
|
||||
|
||||
const pushSegment = () => {
|
||||
if (buf.trim().length > 0) {
|
||||
segments.push(buf);
|
||||
}
|
||||
buf = "";
|
||||
};
|
||||
|
||||
for (let i = 0; i < rawText.length; i += 1) {
|
||||
const ch = rawText[i];
|
||||
const next = rawText[i + 1];
|
||||
|
||||
if (escaped) {
|
||||
buf += ch;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inSingle && ch === "\\") {
|
||||
buf += ch;
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle) {
|
||||
buf += ch;
|
||||
if (ch === "'") {
|
||||
inSingle = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inDouble) {
|
||||
buf += ch;
|
||||
if (ch === '"') {
|
||||
inDouble = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'") {
|
||||
inSingle = true;
|
||||
buf += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"') {
|
||||
inDouble = true;
|
||||
buf += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "\n" || ch === "\r") {
|
||||
pushSegment();
|
||||
continue;
|
||||
}
|
||||
if (ch === ";") {
|
||||
pushSegment();
|
||||
continue;
|
||||
}
|
||||
if (ch === "&" && next === "&") {
|
||||
pushSegment();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "|" && next === "|") {
|
||||
pushSegment();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (params.splitPipes && ch === "|") {
|
||||
pushSegment();
|
||||
continue;
|
||||
}
|
||||
|
||||
buf += ch;
|
||||
}
|
||||
pushSegment();
|
||||
return segments;
|
||||
}
|
||||
|
||||
function isInterpreterExecutable(executable: string | undefined): boolean {
|
||||
if (!executable) {
|
||||
return false;
|
||||
}
|
||||
return /^python(?:3(?:\.\d+)?)?$/i.test(executable) || executable === "node";
|
||||
}
|
||||
|
||||
function hasUnescapedSequence(raw: string, sequence: string): boolean {
|
||||
if (sequence.length === 0) {
|
||||
return false;
|
||||
}
|
||||
let escaped = false;
|
||||
for (let i = 0; i < raw.length; i += 1) {
|
||||
const ch = raw[i];
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (raw.startsWith(sequence, i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function hasUnquotedScriptHint(raw: string): boolean {
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let escaped = false;
|
||||
let token = "";
|
||||
|
||||
const flushToken = (): boolean => {
|
||||
if (token.toLowerCase().endsWith(".py") || token.toLowerCase().endsWith(".js")) {
|
||||
return true;
|
||||
}
|
||||
token = "";
|
||||
return false;
|
||||
};
|
||||
|
||||
for (let i = 0; i < raw.length; i += 1) {
|
||||
const ch = raw[i];
|
||||
if (escaped) {
|
||||
if (!inSingle && !inDouble) {
|
||||
token += ch;
|
||||
}
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && ch === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (inSingle) {
|
||||
if (ch === "'") {
|
||||
inSingle = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (inDouble) {
|
||||
if (ch === '"') {
|
||||
inDouble = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") {
|
||||
if (flushToken()) {
|
||||
return true;
|
||||
}
|
||||
inSingle = true;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
if (flushToken()) {
|
||||
return true;
|
||||
}
|
||||
inDouble = true;
|
||||
continue;
|
||||
}
|
||||
if (/\s/u.test(ch) || "|&;()<>".includes(ch)) {
|
||||
if (flushToken()) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
token += ch;
|
||||
}
|
||||
return flushToken();
|
||||
}
|
||||
|
||||
function resolveLeadingShellSegmentExecutable(rawSegment: string): string | undefined {
|
||||
const segment = (extractUnquotedShellText(rawSegment) ?? rawSegment).trim();
|
||||
const argv = splitShellArgs(segment);
|
||||
if (!argv || argv.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const withoutLeadingKeyword = /^(?:if|then|do|elif|else|while|until|time)$/i.test(argv[0] ?? "")
|
||||
? argv.slice(1)
|
||||
: argv;
|
||||
if (withoutLeadingKeyword.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const normalizedArgv = stripPreflightEnvPrefix(withoutLeadingKeyword);
|
||||
let commandIdx = 0;
|
||||
while (
|
||||
commandIdx < normalizedArgv.length &&
|
||||
/^[A-Za-z_][A-Za-z0-9_]*=.*$/u.test(normalizedArgv[commandIdx] ?? "")
|
||||
) {
|
||||
commandIdx += 1;
|
||||
}
|
||||
return normalizedArgv[commandIdx]?.toLowerCase();
|
||||
}
|
||||
|
||||
function analyzeInterpreterHeuristicsFromUnquoted(raw: string): {
|
||||
hasPython: boolean;
|
||||
hasNode: boolean;
|
||||
@@ -417,26 +636,24 @@ function analyzeInterpreterHeuristicsFromUnquoted(raw: string): {
|
||||
hasProcessSubstitution: boolean;
|
||||
hasScriptHint: boolean;
|
||||
} {
|
||||
const hasPython =
|
||||
/(?:^|\s|(?<!\\)[|&;()])(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*python(?:3(?:\.\d+)?)?(?=$|[\s|&;()<>\n\r`$])/i.test(
|
||||
raw,
|
||||
);
|
||||
const hasNode =
|
||||
/(?:^|\s|(?<!\\)[|&;()])(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*node(?=$|[\s|&;()<>\n\r`$])/i.test(
|
||||
raw,
|
||||
);
|
||||
const hasProcessSubstitution = /(?<!\\)<\(|(?<!\\)>\(/u.test(raw);
|
||||
const hasPython = splitShellSegmentsOutsideQuotes(raw, { splitPipes: true }).some((segment) =>
|
||||
/^python(?:3(?:\.\d+)?)?$/i.test(resolveLeadingShellSegmentExecutable(segment) ?? ""),
|
||||
);
|
||||
const hasNode = splitShellSegmentsOutsideQuotes(raw, { splitPipes: true }).some(
|
||||
(segment) => resolveLeadingShellSegmentExecutable(segment) === "node",
|
||||
);
|
||||
const hasProcessSubstitution = hasUnescapedSequence(raw, "<(") || hasUnescapedSequence(raw, ">(");
|
||||
const hasComplexSyntax =
|
||||
/(?<!\\)\|/u.test(raw) ||
|
||||
/(?<!\\)&&/u.test(raw) ||
|
||||
/(?<!\\)\|\|/u.test(raw) ||
|
||||
/(?<!\\);/u.test(raw) ||
|
||||
hasUnescapedSequence(raw, "|") ||
|
||||
hasUnescapedSequence(raw, "&&") ||
|
||||
hasUnescapedSequence(raw, "||") ||
|
||||
hasUnescapedSequence(raw, ";") ||
|
||||
raw.includes("\n") ||
|
||||
raw.includes("\r") ||
|
||||
/(?<!\\)\$\(/u.test(raw) ||
|
||||
/(?<!\\)`/u.test(raw) ||
|
||||
hasUnescapedSequence(raw, "$(") ||
|
||||
hasUnescapedSequence(raw, "`") ||
|
||||
hasProcessSubstitution;
|
||||
const hasScriptHint = /(?:^|[\s|&;()<>])[^"'`\s|&;()<>]+\.(?:py|js)(?=$|[\s|&;()<>])/i.test(raw);
|
||||
const hasScriptHint = hasUnquotedScriptHint(raw);
|
||||
|
||||
return { hasPython, hasNode, hasComplexSyntax, hasProcessSubstitution, hasScriptHint };
|
||||
}
|
||||
@@ -531,101 +748,8 @@ function shouldFailClosedInterpreterPreflight(command: string): {
|
||||
hasProcessSubstitution: false,
|
||||
hasScriptHint: false,
|
||||
};
|
||||
const splitShellSegmentsOutsideQuotes = (
|
||||
rawText: string,
|
||||
params: { splitPipes: boolean },
|
||||
): string[] => {
|
||||
const segments: string[] = [];
|
||||
let buf = "";
|
||||
let inSingle = false;
|
||||
let inDouble = false;
|
||||
let escaped = false;
|
||||
|
||||
const pushSegment = () => {
|
||||
if (buf.trim().length > 0) {
|
||||
segments.push(buf);
|
||||
}
|
||||
buf = "";
|
||||
};
|
||||
|
||||
for (let i = 0; i < rawText.length; i += 1) {
|
||||
const ch = rawText[i];
|
||||
const next = rawText[i + 1];
|
||||
|
||||
if (escaped) {
|
||||
buf += ch;
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inSingle && ch === "\\") {
|
||||
buf += ch;
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inSingle) {
|
||||
buf += ch;
|
||||
if (ch === "'") {
|
||||
inSingle = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inDouble) {
|
||||
buf += ch;
|
||||
if (ch === '"') {
|
||||
inDouble = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "'") {
|
||||
inSingle = true;
|
||||
buf += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === '"') {
|
||||
inDouble = true;
|
||||
buf += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "\n" || ch === "\r") {
|
||||
pushSegment();
|
||||
continue;
|
||||
}
|
||||
if (ch === ";") {
|
||||
pushSegment();
|
||||
continue;
|
||||
}
|
||||
if (ch === "&" && next === "&") {
|
||||
pushSegment();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "|" && next === "|") {
|
||||
pushSegment();
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (params.splitPipes && ch === "|") {
|
||||
pushSegment();
|
||||
continue;
|
||||
}
|
||||
|
||||
buf += ch;
|
||||
}
|
||||
pushSegment();
|
||||
return segments;
|
||||
};
|
||||
const hasInterpreterInvocationInSegment = (rawSegment: string): boolean => {
|
||||
const segment = extractUnquotedShellText(rawSegment) ?? rawSegment;
|
||||
return /^\s*(?:(?:if|then|do|elif|else|while|until|time)\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*(?:python(?:3(?:\.\d+)?)?|node)(?=$|[\s|&;()<>\n\r`$])/i.test(
|
||||
segment,
|
||||
);
|
||||
};
|
||||
const hasInterpreterInvocationInSegment = (rawSegment: string): boolean =>
|
||||
isInterpreterExecutable(resolveLeadingShellSegmentExecutable(rawSegment));
|
||||
const isScriptExecutingInterpreterCommand = (rawCommand: string): boolean => {
|
||||
const argv = splitShellArgs(rawCommand.trim());
|
||||
if (!argv || argv.length === 0) {
|
||||
@@ -696,9 +820,7 @@ function shouldFailClosedInterpreterPreflight(command: string): {
|
||||
return false;
|
||||
};
|
||||
const hasScriptHintInSegment = (segment: string): boolean =>
|
||||
/(?:^|[\s()<>])(?:"[^"\n\r`|&;()<>]*\.(?:py|js)"|'[^'\n\r`|&;()<>]*\.(?:py|js)'|[^"'`\s|&;()<>]+\.(?:py|js))(?=$|[\s()<>])/i.test(
|
||||
segment,
|
||||
);
|
||||
extractInterpreterScriptPathsFromSegment(segment).length > 0 || hasUnquotedScriptHint(segment);
|
||||
const hasInterpreterAndScriptHintInSameSegment = (rawText: string): boolean => {
|
||||
const segments = splitShellSegmentsOutsideQuotes(rawText, { splitPipes: true });
|
||||
return segments.some((segment) => {
|
||||
|
||||
@@ -131,7 +131,7 @@ describe("runEmbeddedAttempt context injection", () => {
|
||||
expect(hoisted.resolveBootstrapContextForRunMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contextMode: "full",
|
||||
runKind: undefined,
|
||||
runKind: "default",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -460,8 +460,8 @@ function shouldRequireOAuthDir(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boo
|
||||
if (env.OPENCLAW_OAUTH_DIR?.trim()) {
|
||||
return true;
|
||||
}
|
||||
const channels = cfg.channels;
|
||||
if (!isRecord(channels)) {
|
||||
const channels = asNullableObjectRecord(cfg.channels);
|
||||
if (!channels) {
|
||||
return false;
|
||||
}
|
||||
for (const channelId of listBundledChannelPluginIds()) {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import { isPlainObject, isRecord } from "../utils.js";
|
||||
import type {
|
||||
ResolvedTalkConfig,
|
||||
TalkConfig,
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("opt-in extension package boundaries", () => {
|
||||
expect(tsconfig.extends).toBe("../../tsconfig.json");
|
||||
expect(tsconfig.compilerOptions?.declaration).toBe(true);
|
||||
expect(tsconfig.compilerOptions?.emitDeclarationOnly).toBe(true);
|
||||
expect(tsconfig.compilerOptions?.outDir).toBe("dist/packages/plugin-sdk/src");
|
||||
expect(tsconfig.compilerOptions?.outDir).toBe("dist");
|
||||
expect(tsconfig.compilerOptions?.rootDir).toBe("../..");
|
||||
expect(tsconfig.include).toEqual([
|
||||
"../../src/plugin-sdk/config-runtime.ts",
|
||||
@@ -108,6 +108,8 @@ describe("opt-in extension package boundaries", () => {
|
||||
"../../src/plugin-sdk/telegram-command-config.ts",
|
||||
"../../src/plugin-sdk/testing.ts",
|
||||
"../../src/plugin-sdk/video-generation.ts",
|
||||
"../../src/video-generation/dashscope-compatible.ts",
|
||||
"../../src/video-generation/types.ts",
|
||||
"../../src/types/**/*.d.ts",
|
||||
]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user