openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
968 lines (967 loc) • 32.5 kB
JavaScript
import { O as normalizeExecutableToken, a as extractShellWrapperInlineCommand, c as isShellWrapperExecutable, i as extractShellWrapperCommand, t as POSIX_SHELL_WRAPPERS, u as resolveShellWrapperTransportArgv, w as SOURCE_EXECUTABLES } from "./shell-wrapper-resolution-CFL_Vekh.js";
import "./exec-wrapper-resolution-VZBsT_RG.js";
import { a as detectInlineEvalArgv, i as detectCommandCarrierArgv, n as detectCarriedShellBuiltinArgv, r as detectCarrierInlineEvalArgv, s as detectShellWrapperThroughCarrierArgv } from "./risks-DRxJ1pW5.js";
import { createRequire } from "node:module";
import fs from "node:fs";
import path from "node:path";
import * as TreeSitter from "web-tree-sitter";
//#region src/infra/command-explainer/tree-sitter-runtime.ts
const require = createRequire(import.meta.url);
let parserPromise = null;
let parserLoader = loadParser;
const MAX_COMMAND_EXPLANATION_SOURCE_CHARS = 128 * 1024;
const MAX_COMMAND_EXPLANATION_PARSE_MS = 500;
function resolvePackageFileForCommandExplanation(packageName, fileName) {
let packageEntry;
try {
packageEntry = require.resolve(packageName);
} catch (error) {
throw new Error(`Unable to resolve ${packageName} while loading the shell command explainer parser`, { cause: error });
}
let directory = path.dirname(packageEntry);
const searched = [];
for (let depth = 0; depth < 5; depth += 1) {
const candidate = path.join(directory, fileName);
searched.push(candidate);
if (fs.existsSync(candidate)) return candidate;
const parent = path.dirname(directory);
if (parent === directory) break;
directory = parent;
}
throw new Error(`Unable to locate ${fileName} in ${packageName} while loading the shell command explainer parser; searched ${searched.join(", ")}`);
}
function resolveWebTreeSitterFile(fileName) {
return resolvePackageFileForCommandExplanation("web-tree-sitter", fileName);
}
function resolveBashWasmPath() {
return resolvePackageFileForCommandExplanation("tree-sitter-bash", "tree-sitter-bash.wasm");
}
async function loadParser() {
await TreeSitter.Parser.init({ locateFile: resolveWebTreeSitterFile });
const language = await TreeSitter.Language.load(resolveBashWasmPath());
const parser = new TreeSitter.Parser();
parser.setLanguage(language);
return parser;
}
function getBashParserForCommandExplanation() {
parserPromise ??= parserLoader().catch((error) => {
parserPromise = null;
throw error;
});
return parserPromise;
}
/**
* Low-level parser access for tests and parser diagnostics.
* Callers own the returned Tree and must call tree.delete().
* Prefer explainShellCommand for normal command-explainer use.
*/
async function parseBashForCommandExplanation(source) {
if (source.length > MAX_COMMAND_EXPLANATION_SOURCE_CHARS) throw new Error("Shell command is too large to explain");
const parser = await getBashParserForCommandExplanation();
const deadlineMs = performance.now() + MAX_COMMAND_EXPLANATION_PARSE_MS;
let timedOut = false;
const tree = parser.parse(source, null, { progressCallback: () => {
timedOut = performance.now() > deadlineMs;
return timedOut;
} });
if (!tree) {
parser.reset();
if (timedOut) throw new Error(`tree-sitter-bash timed out after ${MAX_COMMAND_EXPLANATION_PARSE_MS}ms while parsing shell command`);
throw new Error("tree-sitter-bash returned no parse tree");
}
return tree;
}
//#endregion
//#region src/infra/command-explainer/extract.ts
const MAX_WRAPPER_PAYLOAD_DEPTH = 2;
const PARSEABLE_SHELL_WRAPPERS = new Set(POSIX_SHELL_WRAPPERS);
const ROOT_SPAN_BASE = {
startIndex: 0,
startPosition: {
row: 0,
column: 0
}
};
function children(node) {
return Array.from({ length: node.childCount }, (_, index) => node.child(index)).filter((child) => child !== null);
}
function namedChildren(node) {
return Array.from({ length: node.namedChildCount }, (_, index) => node.namedChild(index)).filter((child) => child !== null);
}
function hasDirectChildType(node, type) {
return children(node).some((child) => child.type === type);
}
function translatePosition(position, base) {
return {
row: base.row + position.row,
column: position.row === 0 ? base.column + position.column : position.column
};
}
function translateSpan(span, base) {
if (base.mapOffset) {
const start = base.mapOffset(span.startIndex);
const end = base.mapOffset(span.endIndex);
return {
startIndex: start.index,
endIndex: end.index,
startPosition: start.position,
endPosition: end.position
};
}
return {
startIndex: base.startIndex + span.startIndex,
endIndex: base.startIndex + span.endIndex,
startPosition: translatePosition(span.startPosition, base.startPosition),
endPosition: translatePosition(span.endPosition, base.startPosition)
};
}
function spanFromNode(node, base = ROOT_SPAN_BASE) {
return translateSpan({
startIndex: node.startIndex,
endIndex: node.endIndex,
startPosition: {
row: node.startPosition.row,
column: node.startPosition.column
},
endPosition: {
row: node.endPosition.row,
column: node.endPosition.column
}
}, base);
}
function advancePosition(position, text) {
let row = position.row;
let column = position.column;
for (let index = 0; index < text.length; index += 1) {
const ch = text[index];
if (ch === "\r") {
if (text[index + 1] === "\n") index += 1;
row += 1;
column = 0;
continue;
}
if (ch === "\n") {
row += 1;
column = 0;
continue;
}
column += 1;
}
return {
row,
column
};
}
function utf8ByteLengthForCodePoint(codePoint) {
if (codePoint <= 127) return 1;
if (codePoint <= 2047) return 2;
if (codePoint <= 65535) return 3;
return 4;
}
function utf8ByteLength(text) {
let length = 0;
for (let index = 0; index < text.length; index += 1) {
const codePoint = text.codePointAt(index);
if (codePoint === void 0) continue;
length += utf8ByteLengthForCodePoint(codePoint);
if (codePoint > 65535) index += 1;
}
return length;
}
function utf8ByteOffsetToStringIndex(text, byteOffset) {
if (byteOffset <= 0) return 0;
let currentByteOffset = 0;
for (let index = 0; index < text.length; index += 1) {
const codePoint = text.codePointAt(index);
if (codePoint === void 0) return text.length;
const codePointLength = utf8ByteLengthForCodePoint(codePoint);
if (currentByteOffset + codePointLength > byteOffset) return index;
currentByteOffset += codePointLength;
if (currentByteOffset === byteOffset) return codePoint > 65535 ? index + 2 : index + 1;
if (codePoint > 65535) index += 1;
}
return text.length;
}
function parserOffsetToStringIndex(source, rootNode) {
const utf8Length = utf8ByteLength(source);
if (utf8Length !== source.length && rootNode.endIndex === utf8Length) return (offset) => utf8ByteOffsetToStringIndex(source, offset);
return (offset) => offset;
}
function spanBaseForParserSource(source, rootNode, base) {
const offsetToStringIndex = parserOffsetToStringIndex(source, rootNode);
return {
startIndex: base.startIndex,
startPosition: base.startPosition,
mapOffset(offset) {
const sourceIndex = offsetToStringIndex(offset);
if (base.mapOffset) return base.mapOffset(sourceIndex);
return {
index: base.startIndex + sourceIndex,
position: advancePosition(base.startPosition, source.slice(0, sourceIndex))
};
}
};
}
function valuePrefixLength(node) {
if (node.type === "string" || node.type === "raw_string") return 1;
if (node.type === "ansi_c_string") return 2;
return 0;
}
function appendDecodedText(decoded, value, sourceEndOffset) {
decoded.value += value;
decoded.sourceOffsets.push(...Array.from({ length: value.length }, () => sourceEndOffset));
}
function identityDecodedShellText(text, sourceOffset = 0) {
return {
value: text,
sourceOffsets: Array.from({ length: text.length + 1 }, (_, index) => sourceOffset + index)
};
}
function decodedSourceOffsetsForNode(node, value) {
let decoded;
switch (node.type) {
case "raw_string":
decoded = identityDecodedShellText(node.text.slice(1, -1), 1);
break;
case "string":
decoded = decodeDoubleQuotedTextWithOffsets(node.text);
break;
case "ansi_c_string":
decoded = decodeAnsiCStringWithOffsets(node.text);
break;
default:
decoded = decodeUnquotedShellTextWithOffsets(node.text);
break;
}
if (decoded.value === value && decoded.sourceOffsets.length === value.length + 1) return decoded.sourceOffsets;
const prefixLength = valuePrefixLength(node);
return Array.from({ length: value.length + 1 }, (_, index) => prefixLength + index);
}
function argumentFromNode(index, node, value, base) {
const span = spanFromNode(node, base);
const decodedSourceOffsets = decodedSourceOffsetsForNode(node, value.value);
return {
index,
text: node.text,
value: value.value,
span,
decodedSourceOffsets
};
}
const DYNAMIC_WORD_NODE_TYPES = new Set([
"arithmetic_expansion",
"command_substitution",
"expansion",
"process_substitution",
"simple_expansion"
]);
const COMMAND_ARGUMENT_NODE_TYPES = new Set([
"ansi_c_string",
"arithmetic_expansion",
"command_substitution",
"concatenation",
"expansion",
"number",
"process_substitution",
"raw_string",
"simple_expansion",
"string",
"word"
]);
function hasEscapedLineContinuation(text) {
return /\\(?:\r\n|[\r\n])/.test(text);
}
function hasExecutableLineContinuation(text) {
return /^[^\s]*\\(?:\r\n|[\r\n])/.test(text);
}
function hasUnescapedDynamicPattern(text) {
for (let index = 0; index < text.length; index += 1) {
const ch = text[index];
if (ch === "\\") {
index += 1;
continue;
}
if (ch === "*" || ch === "?") return true;
if (ch === "[" && text.indexOf("]", index + 1) > index + 1) return true;
if (ch === "{" && text.indexOf("}", index + 1) > index + 1) return true;
}
return false;
}
function decodeUnquotedShellTextWithOffsets(text) {
const decoded = {
value: "",
sourceOffsets: [0]
};
for (let index = 0; index < text.length; index += 1) {
const ch = text[index];
const next = text[index + 1];
if (ch === "\\" && next !== void 0) {
if (next === "\r" && text[index + 2] === "\n") {
decoded.sourceOffsets[decoded.value.length] = index + 3;
index += 2;
continue;
}
if (next === "\n" || next === "\r") {
decoded.sourceOffsets[decoded.value.length] = index + 2;
index += 1;
continue;
}
appendDecodedText(decoded, next, index + 2);
index += 1;
continue;
}
appendDecodedText(decoded, ch, index + 1);
}
return decoded;
}
function decodeUnquotedShellText(text) {
return decodeUnquotedShellTextWithOffsets(text).value;
}
function decodeDoubleQuotedTextWithOffsets(text) {
const hasQuotes = text.startsWith("\"") && text.endsWith("\"");
const bodyStart = hasQuotes ? 1 : 0;
const body = hasQuotes ? text.slice(1, -1) : text;
const decoded = {
value: "",
sourceOffsets: [bodyStart]
};
for (let index = 0; index < body.length; index += 1) {
const ch = body[index];
const next = body[index + 1];
const sourceOffset = bodyStart + index;
if (ch === "\\" && next !== void 0) {
if (next === "\r" && body[index + 2] === "\n") {
decoded.sourceOffsets[decoded.value.length] = sourceOffset + 3;
index += 2;
continue;
}
if ([
"\\",
"\"",
"$",
"`",
"\n",
"\r"
].includes(next)) {
if (next !== "\n" && next !== "\r") appendDecodedText(decoded, next, sourceOffset + 2);
else decoded.sourceOffsets[decoded.value.length] = sourceOffset + 2;
index += 1;
continue;
}
}
appendDecodedText(decoded, ch, sourceOffset + 1);
}
return decoded;
}
function decodeDoubleQuotedText(text) {
return decodeDoubleQuotedTextWithOffsets(text).value;
}
const ANSI_C_SIMPLE_ESCAPES = {
"'": "'",
"\"": "\"",
"?": "?",
"\\": "\\",
a: "\x07",
b: "\b",
e: "\x1B",
E: "\x1B",
f: "\f",
n: "\n",
r: "\r",
t: " ",
v: "\v"
};
function decodeAnsiCStringWithOffsets(text) {
const hasQuotes = text.startsWith("$'") && text.endsWith("'");
const bodyStart = hasQuotes ? 2 : 0;
const body = hasQuotes ? text.slice(2, -1) : text;
const decoded = {
value: "",
sourceOffsets: [bodyStart]
};
for (let index = 0; index < body.length; index += 1) {
const ch = body[index];
const sourceOffset = bodyStart + index;
if (ch !== "\\") {
appendDecodedText(decoded, ch, sourceOffset + 1);
continue;
}
const next = body[index + 1];
if (next === void 0) {
appendDecodedText(decoded, "\\", sourceOffset + 1);
continue;
}
const simple = ANSI_C_SIMPLE_ESCAPES[next];
if (simple !== void 0) {
appendDecodedText(decoded, simple, sourceOffset + 2);
index += 1;
continue;
}
if (next === "x") {
const hex = body.slice(index + 2).match(/^[0-9A-Fa-f]{1,2}/)?.[0] ?? "";
if (hex) {
appendDecodedText(decoded, String.fromCodePoint(Number.parseInt(hex, 16)), sourceOffset + 2 + hex.length);
index += 1 + hex.length;
continue;
}
}
if (next === "u" || next === "U") {
const maxLength = next === "u" ? 4 : 8;
const hex = body.slice(index + 2).match(new RegExp(`^[0-9A-Fa-f]{1,${maxLength}}`))?.[0] ?? "";
if (hex) {
const codePoint = Number.parseInt(hex, 16);
try {
appendDecodedText(decoded, String.fromCodePoint(codePoint), sourceOffset + 2 + hex.length);
} catch {
appendDecodedText(decoded, `\\${next}${hex}`, sourceOffset + 2 + hex.length);
}
index += 1 + hex.length;
continue;
}
}
if (/^[0-7]$/.test(next)) {
const octal = body.slice(index + 1).match(/^[0-7]{1,3}/)?.[0] ?? "";
if (octal) {
appendDecodedText(decoded, String.fromCodePoint(Number.parseInt(octal, 8)), sourceOffset + 1 + octal.length);
index += octal.length;
continue;
}
}
appendDecodedText(decoded, next, sourceOffset + 2);
index += 1;
}
return decoded;
}
function decodeAnsiCString(text) {
return decodeAnsiCStringWithOffsets(text).value;
}
function hasDynamicWordPart(node) {
return DYNAMIC_WORD_NODE_TYPES.has(node.type) || namedChildren(node).some((child) => hasDynamicWordPart(child));
}
function shellWordValue(node) {
if (DYNAMIC_WORD_NODE_TYPES.has(node.type)) return {
kind: "dynamic",
value: node.text
};
if (node.type !== "command_name" && node.type !== "concatenation" && namedChildren(node).some((child) => hasDynamicWordPart(child))) return {
kind: "dynamic",
value: node.type === "string" ? decodeDoubleQuotedText(node.text) : node.text
};
switch (node.type) {
case "command_name": {
const parts = namedChildren(node);
if (parts.length === 0) return hasUnescapedDynamicPattern(node.text) ? {
kind: "dynamic",
value: decodeUnquotedShellText(node.text)
} : {
kind: "literal",
value: decodeUnquotedShellText(node.text)
};
let value = "";
for (const part of parts) {
const partValue = shellWordValue(part);
value += partValue.value;
if (partValue.kind !== "literal") return {
kind: "dynamic",
value
};
}
return {
kind: "literal",
value
};
}
case "word": return hasUnescapedDynamicPattern(node.text) ? {
kind: "dynamic",
value: decodeUnquotedShellText(node.text)
} : {
kind: "literal",
value: decodeUnquotedShellText(node.text)
};
case "raw_string": return {
kind: "literal",
value: node.text.slice(1, -1)
};
case "string": return {
kind: "literal",
value: decodeDoubleQuotedText(node.text)
};
case "ansi_c_string": return {
kind: "literal",
value: decodeAnsiCString(node.text)
};
case "concatenation": {
if (hasUnescapedDynamicPattern(node.text)) return {
kind: "dynamic",
value: decodeUnquotedShellText(node.text)
};
let value = "";
let dynamic = false;
for (const child of namedChildren(node)) {
const childValue = shellWordValue(child);
value += childValue.value;
if (childValue.kind !== "literal") dynamic = true;
}
return dynamic ? {
kind: "dynamic",
value
} : {
kind: "literal",
value
};
}
default: return namedChildren(node).some((child) => shellWordValue(child).kind === "dynamic") ? {
kind: "dynamic",
value: decodeUnquotedShellText(node.text)
} : {
kind: "literal",
value: decodeUnquotedShellText(node.text)
};
}
}
function commandNameNode(node) {
return node.childForFieldName("name") ?? namedChildren(node).find((child) => child.type === "command_name") ?? null;
}
function argvFromCommand(node, nameNode, state) {
if (hasEscapedLineContinuation(nameNode.text) || hasExecutableLineContinuation(node.text)) return null;
const executable = shellWordValue(nameNode);
if (executable.kind !== "literal") return null;
const skipped = new Set([nameNode, ...namedChildren(nameNode)]);
const argv = [executable.value];
const argumentsList = [];
const dynamicArguments = [];
for (const child of namedChildren(node)) {
if (skipped.has(child) || child.type === "command_name" || child.type === "variable_assignment" || !COMMAND_ARGUMENT_NODE_TYPES.has(child.type)) continue;
const value = shellWordValue(child);
const argument = argumentFromNode(argv.length, child, value, state.spanBase);
argumentsList.push(argument);
if (value.kind === "dynamic") dynamicArguments.push({
index: argument.index,
text: argument.text,
value: argument.value,
span: argument.span
});
argv.push(value.value);
}
return {
argv,
arguments: argumentsList,
dynamicArguments
};
}
function firstShellToken(text) {
return text.trimStart().match(/^\S+/)?.[0] ?? "";
}
function argvFromDeclarationCommand(node, state) {
const executable = firstShellToken(node.text);
if (!executable) return null;
const argv = [executable];
const argumentsList = [];
const dynamicArguments = [];
for (const child of namedChildren(node)) {
if (!COMMAND_ARGUMENT_NODE_TYPES.has(child.type) && child.type !== "variable_assignment") continue;
const value = shellWordValue(child);
const argument = argumentFromNode(argv.length, child, value, state.spanBase);
argumentsList.push(argument);
if (value.kind === "dynamic") dynamicArguments.push({
index: argument.index,
text: argument.text,
value: argument.value,
span: argument.span
});
argv.push(value.value);
}
return {
argv,
arguments: argumentsList,
dynamicArguments
};
}
function appendTestCommandArguments(node, argv, argumentsList, dynamicArguments, state) {
if (node.type === "test_operator" || COMMAND_ARGUMENT_NODE_TYPES.has(node.type)) {
const value = shellWordValue(node);
const argument = argumentFromNode(argv.length, node, value, state.spanBase);
argumentsList.push(argument);
if (value.kind === "dynamic") dynamicArguments.push({
index: argument.index,
text: argument.text,
value: argument.value,
span: argument.span
});
argv.push(value.value);
return;
}
for (const child of namedChildren(node)) appendTestCommandArguments(child, argv, argumentsList, dynamicArguments, state);
}
function argvFromTestCommand(node, state) {
const trimmed = node.text.trimStart();
const executable = trimmed.startsWith("[[") ? "[[" : trimmed.startsWith("[") ? "[" : "";
if (!executable) return null;
const argv = [executable];
const argumentsList = [];
const dynamicArguments = [];
for (const child of namedChildren(node)) appendTestCommandArguments(child, argv, argumentsList, dynamicArguments, state);
return {
argv,
arguments: argumentsList,
dynamicArguments
};
}
function isCommandLikeNode(node) {
return node.type === "command" || node.type === "declaration_command" || node.type === "test_command";
}
function recordShape(node, output) {
if ((node.type === "program" || node.type === "list") && (hasDirectChildType(node, ";") || namedChildren(node).filter(isCommandLikeNode).length > 1)) output.shapes.add("sequence");
if (hasDirectChildType(node, "&")) output.shapes.add("background");
if (node.type === "pipeline") output.shapes.add("pipeline");
if (node.type === "list") {
if (hasDirectChildType(node, "&&")) output.shapes.add("and");
if (hasDirectChildType(node, "||")) output.shapes.add("or");
}
if (node.type === "if_statement") output.shapes.add("if");
if (node.type === "for_statement") output.shapes.add("for");
if (node.type === "while_statement") output.shapes.add("while");
if (node.type === "case_statement") output.shapes.add("case");
if (node.type === "subshell") output.shapes.add("subshell");
if (node.type === "compound_statement") output.shapes.add("group");
}
function shellCommandFlag(argv, startIndex) {
const shell = normalizeExecutableToken(argv[startIndex - 1] ?? argv[0] ?? "");
for (let index = startIndex; index < argv.length; index += 1) {
const token = argv[index]?.trim();
if (!token) continue;
if (token === "--") break;
const lower = token.toLowerCase();
if (shell === "cmd") {
if (lower === "/c" || lower === "/k") return {
flag: token,
index
};
continue;
}
if (shell === "powershell" || shell === "pwsh") {
if (lower === "-c" || lower === "-command" || lower === "--command" || lower === "-encodedcommand" || lower === "-enc" || lower === "-e" || lower === "-f" || lower === "-file") return {
flag: token,
index
};
continue;
}
if (lower === "-c" || lower === "--command") return {
flag: token,
index
};
if (token.startsWith("-") && !token.startsWith("--") && lower.slice(1).includes("c")) return {
flag: token,
index
};
}
return null;
}
function canParseShellWrapperPayload(transportArgv, commandFlag) {
const shellExecutable = normalizeExecutableToken(transportArgv[0] ?? "");
if (!PARSEABLE_SHELL_WRAPPERS.has(shellExecutable)) return false;
const lowerFlag = commandFlag?.toLowerCase() ?? "";
return lowerFlag === "-c" || lowerFlag === "--command" || /^-[^-]*c[^-]*$/i.test(lowerFlag);
}
function isDynamicPayload(payload, dynamicArguments) {
return dynamicArguments.some((argument) => argument.value === payload);
}
function payloadBaseFromArgument(argument, payload) {
const payloadOffset = argument.value.indexOf(payload);
if (payloadOffset < 0) return null;
const rawPayloadOffset = argument.decodedSourceOffsets[payloadOffset];
if (rawPayloadOffset === void 0) return null;
const prefix = argument.text.slice(0, rawPayloadOffset);
return {
startIndex: argument.span.startIndex + rawPayloadOffset,
startPosition: advancePosition(argument.span.startPosition, prefix),
mapOffset(offset) {
const mappedRawOffset = argument.decodedSourceOffsets[payloadOffset + offset] ?? rawPayloadOffset + offset;
return {
index: argument.span.startIndex + mappedRawOffset,
position: advancePosition(argument.span.startPosition, argument.text.slice(0, mappedRawOffset))
};
}
};
}
function payloadBaseFromArguments(payload, argumentsList) {
const exactArgument = argumentsList.find((argument) => argument.value === payload);
if (exactArgument) return payloadBaseFromArgument(exactArgument, payload);
for (const argument of argumentsList) {
const base = payloadBaseFromArgument(argument, payload);
if (base) return base;
}
return null;
}
function shellWrapperPayloadForParsing(argv, argumentsList, dynamicArguments) {
const shellWrapper = extractShellWrapperCommand(argv);
const payload = shellWrapper.command ?? extractShellWrapperInlineCommand(argv);
if (!shellWrapper.isWrapper || !payload || isDynamicPayload(payload, dynamicArguments)) return null;
const spanBase = payloadBaseFromArguments(payload, argumentsList);
if (!spanBase) return null;
const transportArgv = resolveShellWrapperTransportArgv(argv) ?? argv;
if (!canParseShellWrapperPayload(transportArgv, (shellCommandFlag(transportArgv, 1) ?? shellCommandFlag(argv, 1))?.flag ?? null)) return null;
return {
command: payload,
spanBase
};
}
function recordInlineEvalRisk(inlineEval, text, span, output) {
output.risks.push({
kind: "inline-eval",
command: inlineEval.normalizedExecutable,
flag: inlineEval.flag,
text,
span
});
}
function recordDynamicArgumentRisks(command, dynamicArguments, output) {
for (const argument of dynamicArguments) output.risks.push({
kind: "dynamic-argument",
command,
argumentIndex: argument.index,
text: argument.text,
span: argument.span
});
}
function recordCommandRisks(argv, dynamicArguments, text, span, output) {
const executable = argv[0];
if (!executable) return;
const normalizedExecutable = normalizeExecutableToken(executable);
recordDynamicArgumentRisks(normalizedExecutable, dynamicArguments, output);
const inlineEval = detectInlineEvalArgv(argv) ?? detectCarrierInlineEvalArgv(argv);
if (inlineEval) recordInlineEvalRisk(inlineEval, text, span, output);
const shellWrapper = extractShellWrapperCommand(argv);
const shellWrapperPayload = shellWrapper.command ?? extractShellWrapperInlineCommand(argv);
if (shellWrapper.isWrapper && shellWrapperPayload) {
const transportArgv = resolveShellWrapperTransportArgv(argv) ?? argv;
const shellExecutable = transportArgv[0] ?? executable;
const commandFlag = shellCommandFlag(transportArgv, 1) ?? shellCommandFlag(argv, 1);
if (isShellWrapperExecutable(executable)) output.risks.push({
kind: "shell-wrapper",
executable: shellExecutable,
flag: commandFlag?.flag ?? "-c",
payload: shellWrapperPayload,
text,
span
});
else output.risks.push({
kind: "shell-wrapper-through-carrier",
command: normalizedExecutable,
text,
span
});
}
for (const carrier of detectCommandCarrierArgv(argv)) output.risks.push({
kind: "command-carrier",
command: carrier.command,
flag: carrier.flag,
text,
span
});
if (normalizedExecutable === "eval") output.risks.push({
kind: "eval",
text,
span
});
if (SOURCE_EXECUTABLES.has(normalizedExecutable)) output.risks.push({
kind: "source",
command: normalizedExecutable,
text,
span
});
if (normalizedExecutable === "alias") output.risks.push({
kind: "alias",
text,
span
});
const carrierShellWrapper = !shellWrapper.isWrapper ? detectShellWrapperThroughCarrierArgv(argv, shellCommandFlag) : null;
if (carrierShellWrapper) output.risks.push({
kind: "shell-wrapper-through-carrier",
command: carrierShellWrapper,
text,
span
});
const carriedShellBuiltin = detectCarriedShellBuiltinArgv(argv);
if (carriedShellBuiltin?.kind === "eval") output.risks.push({
kind: "eval",
text,
span
});
else if (carriedShellBuiltin?.kind === "source") output.risks.push({
kind: "source",
command: carriedShellBuiltin.command,
text,
span
});
}
async function walk(node, output, context, state) {
recordShape(node, output);
const span = spanFromNode(node, state.spanBase);
let childContext = context;
if (node.type === "program" && hasEscapedLineContinuation(node.text)) output.risks.push({
kind: "line-continuation",
text: node.text,
span
});
if (node.type === "function_definition") {
const nameNode = node.childForFieldName("name");
output.risks.push({
kind: "function-definition",
name: nameNode?.text ?? "",
text: node.text,
span
});
childContext = "function-definition";
} else if (node.type === "command_substitution") {
output.risks.push({
kind: "command-substitution",
text: node.text,
span
});
childContext = "command-substitution";
} else if (node.type === "process_substitution") {
output.risks.push({
kind: "process-substitution",
text: node.text,
span
});
childContext = "process-substitution";
} else if (node.type === "heredoc_redirect") output.risks.push({
kind: "heredoc",
text: node.text,
span
});
else if (node.type === "herestring_redirect") output.risks.push({
kind: "here-string",
text: node.text,
span
});
else if (node.type === "file_redirect") output.risks.push({
kind: "redirect",
text: node.text,
span
});
else if (node.type === "ERROR") output.risks.push({
kind: "syntax-error",
text: node.text,
span
});
if (node.type === "command" || node.type === "declaration_command" || node.type === "test_command") {
const nameNode = node.type === "command" ? commandNameNode(node) : null;
const parsed = node.type === "command" ? nameNode ? argvFromCommand(node, nameNode, state) : null : node.type === "declaration_command" ? argvFromDeclarationCommand(node, state) : argvFromTestCommand(node, state);
if (node.type === "command" && nameNode && !parsed) output.risks.push({
kind: "dynamic-executable",
text: nameNode.text,
span: spanFromNode(nameNode, state.spanBase)
});
else if (parsed) {
const step = {
context,
executable: parsed.argv[0] ?? "",
argv: parsed.argv,
text: node.text,
span,
executableSpan: nameNode !== null ? spanFromNode(nameNode, state.spanBase) : parsed.arguments[0]?.span ?? span
};
if (step.executable) {
output.commands.push(step);
recordCommandRisks(parsed.argv, parsed.dynamicArguments, node.text, span, output);
const wrapperPayload = shellWrapperPayloadForParsing(parsed.argv, parsed.arguments, parsed.dynamicArguments);
if (wrapperPayload && state.wrapperPayloadDepth < MAX_WRAPPER_PAYLOAD_DEPTH) {
const wrapperTree = await parseBashForCommandExplanation(wrapperPayload.command);
const wrapperSpanBase = spanBaseForParserSource(wrapperPayload.command, wrapperTree.rootNode, wrapperPayload.spanBase);
try {
if (wrapperTree.rootNode.hasError) {
output.hasParseError = true;
output.risks.push({
kind: "syntax-error",
text: wrapperPayload.command,
span: spanFromNode(wrapperTree.rootNode, wrapperSpanBase)
});
}
await walk(wrapperTree.rootNode, output, "wrapper-payload", {
wrapperPayloadDepth: state.wrapperPayloadDepth + 1,
spanBase: wrapperSpanBase
});
} finally {
wrapperTree.delete();
}
}
}
}
}
for (const child of namedChildren(node)) await walk(child, output, childContext, state);
}
/** Parses a shell command into command steps, shapes, risks, and source spans. */
async function explainShellCommand(source) {
const tree = await parseBashForCommandExplanation(source);
try {
const spanBase = spanBaseForParserSource(source, tree.rootNode, ROOT_SPAN_BASE);
const output = {
shapes: /* @__PURE__ */ new Set(),
commands: [],
risks: [],
hasParseError: tree.rootNode.hasError
};
await walk(tree.rootNode, output, "top-level", {
wrapperPayloadDepth: 0,
spanBase
});
const topLevelCommands = output.commands.filter((command) => command.context === "top-level");
return {
ok: !output.hasParseError,
source,
shapes: [...output.shapes],
topLevelCommands,
nestedCommands: output.commands.filter((command) => command.context !== "top-level"),
risks: output.risks
};
} finally {
tree.delete();
}
}
//#endregion
//#region src/infra/command-explainer/format.ts
const POSIX_COMMAND_HIGHLIGHT_SHELLS = POSIX_SHELL_WRAPPERS;
function spanToCommandSpan(span) {
if (!Number.isSafeInteger(span.startIndex) || !Number.isSafeInteger(span.endIndex)) return null;
if (span.startIndex < 0 || span.endIndex <= span.startIndex) return null;
return {
startIndex: span.startIndex,
endIndex: span.endIndex
};
}
function isUnsupportedShellWrapperArgv(argv) {
const executable = (resolveShellWrapperTransportArgv([...argv]) ?? argv)[0];
if (!executable) return false;
const normalizedExecutable = normalizeExecutableToken(executable);
return isShellWrapperExecutable(normalizedExecutable) && !POSIX_COMMAND_HIGHLIGHT_SHELLS.has(normalizedExecutable);
}
function hasUnsupportedShellWrapper(explanation) {
return explanation.topLevelCommands.some((command) => isUnsupportedShellWrapperArgv(command.argv));
}
/** Converts a parsed command explanation into source spans suitable for approval UI. */
function formatCommandSpans(explanation) {
if (hasUnsupportedShellWrapper(explanation)) return [];
const commandSpans = [];
for (const command of [...explanation.topLevelCommands, ...explanation.nestedCommands]) {
const commandSpan = spanToCommandSpan(command.executableSpan);
if (commandSpan) commandSpans.push(commandSpan);
}
return commandSpans;
}
//#endregion
//#region src/agents/bash-tools.exec-approval-request.runtime.ts
/**
* Lazy runtime for exec approval command highlighting.
* Kept separate so importing approval request code does not load the command
* explainer until command spans are explicitly requested.
*/
/** Resolve command spans used to highlight exec approval prompts. */
async function resolveExecApprovalCommandSpans(command) {
const commandSpans = formatCommandSpans(await explainShellCommand(command));
return commandSpans.length > 0 ? commandSpans : void 0;
}
//#endregion
export { resolveExecApprovalCommandSpans };