mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 01:41:40 +00:00
265 lines
8.0 KiB
TypeScript
265 lines
8.0 KiB
TypeScript
import {
|
|
MAX_DISPATCH_WRAPPER_DEPTH,
|
|
hasDispatchEnvManipulation,
|
|
unwrapKnownDispatchWrapperInvocation,
|
|
} from "./dispatch-wrapper-resolution.js";
|
|
import { normalizeExecutableToken } from "./exec-wrapper-tokens.js";
|
|
import {
|
|
POSIX_INLINE_COMMAND_FLAGS,
|
|
POWERSHELL_INLINE_COMMAND_FLAGS,
|
|
resolveInlineCommandMatch,
|
|
} from "./shell-inline-command.js";
|
|
|
|
const POSIX_SHELL_WRAPPER_NAMES = ["ash", "bash", "dash", "fish", "ksh", "sh", "zsh"] as const;
|
|
const WINDOWS_CMD_WRAPPER_NAMES = ["cmd"] as const;
|
|
const POWERSHELL_WRAPPER_NAMES = ["powershell", "pwsh"] as const;
|
|
const SHELL_MULTIPLEXER_WRAPPER_NAMES = ["busybox", "toybox"] as const;
|
|
|
|
function withWindowsExeAliases(names: readonly string[]): string[] {
|
|
const expanded = new Set<string>();
|
|
for (const name of names) {
|
|
expanded.add(name);
|
|
expanded.add(`${name}.exe`);
|
|
}
|
|
return Array.from(expanded);
|
|
}
|
|
|
|
export const POSIX_SHELL_WRAPPERS = new Set(POSIX_SHELL_WRAPPER_NAMES);
|
|
export const WINDOWS_CMD_WRAPPERS = new Set(withWindowsExeAliases(WINDOWS_CMD_WRAPPER_NAMES));
|
|
export const POWERSHELL_WRAPPERS = new Set(withWindowsExeAliases(POWERSHELL_WRAPPER_NAMES));
|
|
|
|
const POSIX_SHELL_WRAPPER_CANONICAL = new Set<string>(POSIX_SHELL_WRAPPER_NAMES);
|
|
const WINDOWS_CMD_WRAPPER_CANONICAL = new Set<string>(WINDOWS_CMD_WRAPPER_NAMES);
|
|
const POWERSHELL_WRAPPER_CANONICAL = new Set<string>(POWERSHELL_WRAPPER_NAMES);
|
|
const SHELL_MULTIPLEXER_WRAPPER_CANONICAL = new Set<string>(SHELL_MULTIPLEXER_WRAPPER_NAMES);
|
|
const SHELL_WRAPPER_CANONICAL = new Set<string>([
|
|
...POSIX_SHELL_WRAPPER_NAMES,
|
|
...WINDOWS_CMD_WRAPPER_NAMES,
|
|
...POWERSHELL_WRAPPER_NAMES,
|
|
]);
|
|
|
|
type ShellWrapperKind = "posix" | "cmd" | "powershell";
|
|
|
|
type ShellWrapperSpec = {
|
|
kind: ShellWrapperKind;
|
|
names: ReadonlySet<string>;
|
|
};
|
|
|
|
const SHELL_WRAPPER_SPECS: ReadonlyArray<ShellWrapperSpec> = [
|
|
{ kind: "posix", names: POSIX_SHELL_WRAPPER_CANONICAL },
|
|
{ kind: "cmd", names: WINDOWS_CMD_WRAPPER_CANONICAL },
|
|
{ kind: "powershell", names: POWERSHELL_WRAPPER_CANONICAL },
|
|
];
|
|
|
|
export type ShellWrapperCommand = {
|
|
isWrapper: boolean;
|
|
command: string | null;
|
|
};
|
|
|
|
function isWithinDispatchClassificationDepth(depth: number): boolean {
|
|
return depth <= MAX_DISPATCH_WRAPPER_DEPTH;
|
|
}
|
|
|
|
export function isShellWrapperExecutable(token: string): boolean {
|
|
return SHELL_WRAPPER_CANONICAL.has(normalizeExecutableToken(token));
|
|
}
|
|
|
|
function normalizeRawCommand(rawCommand?: string | null): string | null {
|
|
const trimmed = rawCommand?.trim() ?? "";
|
|
return trimmed.length > 0 ? trimmed : null;
|
|
}
|
|
|
|
function findShellWrapperSpec(baseExecutable: string): ShellWrapperSpec | null {
|
|
for (const spec of SHELL_WRAPPER_SPECS) {
|
|
if (spec.names.has(baseExecutable)) {
|
|
return spec;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export type ShellMultiplexerUnwrapResult =
|
|
| { kind: "not-wrapper" }
|
|
| { kind: "blocked"; wrapper: string }
|
|
| { kind: "unwrapped"; wrapper: string; argv: string[] };
|
|
|
|
export function unwrapKnownShellMultiplexerInvocation(
|
|
argv: string[],
|
|
): ShellMultiplexerUnwrapResult {
|
|
const token0 = argv[0]?.trim();
|
|
if (!token0) {
|
|
return { kind: "not-wrapper" };
|
|
}
|
|
const wrapper = normalizeExecutableToken(token0);
|
|
if (!SHELL_MULTIPLEXER_WRAPPER_CANONICAL.has(wrapper)) {
|
|
return { kind: "not-wrapper" };
|
|
}
|
|
|
|
let appletIndex = 1;
|
|
if (argv[appletIndex]?.trim() === "--") {
|
|
appletIndex += 1;
|
|
}
|
|
const applet = argv[appletIndex]?.trim();
|
|
if (!applet || !isShellWrapperExecutable(applet)) {
|
|
return { kind: "blocked", wrapper };
|
|
}
|
|
|
|
const unwrapped = argv.slice(appletIndex);
|
|
if (unwrapped.length === 0) {
|
|
return { kind: "blocked", wrapper };
|
|
}
|
|
return { kind: "unwrapped", wrapper, argv: unwrapped };
|
|
}
|
|
|
|
function extractPosixShellInlineCommand(argv: string[]): string | null {
|
|
return extractInlineCommandByFlags(argv, POSIX_INLINE_COMMAND_FLAGS, { allowCombinedC: true });
|
|
}
|
|
|
|
function extractCmdInlineCommand(argv: string[]): string | null {
|
|
const idx = argv.findIndex((item) => {
|
|
const token = item.trim().toLowerCase();
|
|
return token === "/c" || token === "/k";
|
|
});
|
|
if (idx === -1) {
|
|
return null;
|
|
}
|
|
const tail = argv.slice(idx + 1);
|
|
if (tail.length === 0) {
|
|
return null;
|
|
}
|
|
const cmd = tail.join(" ").trim();
|
|
return cmd.length > 0 ? cmd : null;
|
|
}
|
|
|
|
function extractPowerShellInlineCommand(argv: string[]): string | null {
|
|
return extractInlineCommandByFlags(argv, POWERSHELL_INLINE_COMMAND_FLAGS);
|
|
}
|
|
|
|
function extractInlineCommandByFlags(
|
|
argv: string[],
|
|
flags: ReadonlySet<string>,
|
|
options: { allowCombinedC?: boolean } = {},
|
|
): string | null {
|
|
return resolveInlineCommandMatch(argv, flags, options).command;
|
|
}
|
|
|
|
function extractShellWrapperPayload(argv: string[], spec: ShellWrapperSpec): string | null {
|
|
switch (spec.kind) {
|
|
case "posix":
|
|
return extractPosixShellInlineCommand(argv);
|
|
case "cmd":
|
|
return extractCmdInlineCommand(argv);
|
|
case "powershell":
|
|
return extractPowerShellInlineCommand(argv);
|
|
}
|
|
}
|
|
|
|
function hasEnvManipulationBeforeShellWrapperInternal(
|
|
argv: string[],
|
|
depth: number,
|
|
envManipulationSeen: boolean,
|
|
): boolean {
|
|
if (!isWithinDispatchClassificationDepth(depth)) {
|
|
return false;
|
|
}
|
|
|
|
const token0 = argv[0]?.trim();
|
|
if (!token0) {
|
|
return false;
|
|
}
|
|
|
|
const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(argv);
|
|
if (dispatchUnwrap.kind === "blocked") {
|
|
return false;
|
|
}
|
|
if (dispatchUnwrap.kind === "unwrapped") {
|
|
const nextEnvManipulationSeen = envManipulationSeen || hasDispatchEnvManipulation(argv);
|
|
return hasEnvManipulationBeforeShellWrapperInternal(
|
|
dispatchUnwrap.argv,
|
|
depth + 1,
|
|
nextEnvManipulationSeen,
|
|
);
|
|
}
|
|
|
|
const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(argv);
|
|
if (shellMultiplexerUnwrap.kind === "blocked") {
|
|
return false;
|
|
}
|
|
if (shellMultiplexerUnwrap.kind === "unwrapped") {
|
|
return hasEnvManipulationBeforeShellWrapperInternal(
|
|
shellMultiplexerUnwrap.argv,
|
|
depth + 1,
|
|
envManipulationSeen,
|
|
);
|
|
}
|
|
|
|
const wrapper = findShellWrapperSpec(normalizeExecutableToken(token0));
|
|
if (!wrapper) {
|
|
return false;
|
|
}
|
|
const payload = extractShellWrapperPayload(argv, wrapper);
|
|
if (!payload) {
|
|
return false;
|
|
}
|
|
return envManipulationSeen;
|
|
}
|
|
|
|
export function hasEnvManipulationBeforeShellWrapper(argv: string[]): boolean {
|
|
return hasEnvManipulationBeforeShellWrapperInternal(argv, 0, false);
|
|
}
|
|
|
|
function extractShellWrapperCommandInternal(
|
|
argv: string[],
|
|
rawCommand: string | null,
|
|
depth: number,
|
|
): ShellWrapperCommand {
|
|
if (!isWithinDispatchClassificationDepth(depth)) {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
|
|
const token0 = argv[0]?.trim();
|
|
if (!token0) {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
|
|
const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(argv);
|
|
if (dispatchUnwrap.kind === "blocked") {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
if (dispatchUnwrap.kind === "unwrapped") {
|
|
return extractShellWrapperCommandInternal(dispatchUnwrap.argv, rawCommand, depth + 1);
|
|
}
|
|
|
|
const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(argv);
|
|
if (shellMultiplexerUnwrap.kind === "blocked") {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
if (shellMultiplexerUnwrap.kind === "unwrapped") {
|
|
return extractShellWrapperCommandInternal(shellMultiplexerUnwrap.argv, rawCommand, depth + 1);
|
|
}
|
|
|
|
const wrapper = findShellWrapperSpec(normalizeExecutableToken(token0));
|
|
if (!wrapper) {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
|
|
const payload = extractShellWrapperPayload(argv, wrapper);
|
|
if (!payload) {
|
|
return { isWrapper: false, command: null };
|
|
}
|
|
|
|
return { isWrapper: true, command: rawCommand ?? payload };
|
|
}
|
|
|
|
export function extractShellWrapperInlineCommand(argv: string[]): string | null {
|
|
const extracted = extractShellWrapperCommandInternal(argv, null, 0);
|
|
return extracted.isWrapper ? extracted.command : null;
|
|
}
|
|
|
|
export function extractShellWrapperCommand(
|
|
argv: string[],
|
|
rawCommand?: string | null,
|
|
): ShellWrapperCommand {
|
|
return extractShellWrapperCommandInternal(argv, normalizeRawCommand(rawCommand), 0);
|
|
}
|