UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

200 lines (199 loc) 9.68 kB
import "./src-vebZIeLe.js"; import { t as expectDefined } from "./expect-CyE8FADM.js"; import { o as normalizeLowercaseStringOrEmpty } from "./string-coerce-CIXf7egm.js"; import { d as normalizeStringEntries } from "./string-normalization-DsCfAx8q.js"; import { n as resolvePathViaExistingAncestorSync } from "./boundary-path-DMNeww4q.js"; import { r as isPathInside } from "./path-guards-Cp-mGr3-.js"; import { r as splitShellArgs } from "./shell-argv-DE1kujuY.js"; import { n as buildCommandPayloadCandidates, t as buildCommandPayloadArgvCandidates } from "./risks-CTJz9z0K.js"; import { n as explainShellCommand, t as CommandExplanationWorkLimitError } from "./extract-rgReYDDg.js"; import { fileURLToPath } from "node:url"; import path from "node:path"; import os from "node:os"; //#region src/infra/exec-control-command-guard.ts const SQLITE_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Map([ ["cmd", 1], ["escape", 1], ["heap", 1], ["init", 1], ["lookaside", 2], ["maxsize", 1], ["mmap", 1], ["newline", 1], ["nonce", 1], ["nullvalue", 1], ["pagecache", 2], ["separator", 1], ["vfs", 1] ]); const SQLITE_OPEN_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Map([ ["hexkey", 1], ["key", 1], ["maxsize", 1], ["textkey", 1] ]); function parseExecApprovalShellCommand(raw) { const match = raw.trimStart().match(/^\/approve(?:@[^\s]+)?\s+([A-Za-z0-9][A-Za-z0-9._:-]*)\s+(allow-once|allow-always|always|deny)\b/i); if (!match) return null; return { approvalId: expectDefined(match[1], "exec control command guard regex capture 1"), decision: normalizeLowercaseStringOrEmpty(match[2]) === "always" ? "allow-always" : normalizeLowercaseStringOrEmpty(match[2]) }; } function normalizeCommandBaseName(token) { if (!token) return ""; return normalizeLowercaseStringOrEmpty(token.split(/[\\/]/u).at(-1)).replace(/\.(?:cmd|exe)$/u, ""); } function stripOpenClawPackageRunner(argv) { const commandName = normalizeCommandBaseName(argv[0]); if (commandName === "openclaw") return argv; if ((commandName === "pnpm" || commandName === "npm" || commandName === "yarn") && normalizeCommandBaseName(argv[1]) === "openclaw") return argv.slice(1); if ((commandName === "pnpm" || commandName === "npm" || commandName === "yarn") && (argv[1] === "exec" || argv[1] === "dlx" || argv[1] === "run") && normalizeCommandBaseName(argv[2]) === "openclaw") return argv.slice(2); if (commandName === "npx" || commandName === "bunx") { let idx = 1; while (idx < argv.length) { const token = expectDefined(argv[idx], "argv entry at idx"); if (token === "--") { idx += 1; break; } if (!token.startsWith("-") || token === "-") break; idx += 1; if ((token === "-p" || token === "--package") && idx < argv.length) idx += 1; } if (normalizeCommandBaseName(argv[idx]) === "openclaw") return argv.slice(idx); } return argv; } function parseOpenClawChannelsLoginShellCommand(raw) { const argv = splitShellArgs(raw); if (!argv) return false; const openclawArgv = stripOpenClawPackageRunner(argv); return normalizeCommandBaseName(openclawArgv[0]) === "openclaw" && (openclawArgv[1] === "channels" || openclawArgv[1] === "channel") && openclawArgv[2] === "login"; } function parseSqliteOpenCommandDatabaseToken(command) { const argv = splitShellArgs(command); if (!argv || !/^\.op(?:e(?:n)?)?$/u.test(argv[0] ?? "")) return null; for (let index = 1; index < argv.length; index += 1) { const token = expectDefined(argv[index], "sqlite3 .open argv entry"); if (token === "--") return argv[index + 1] ?? null; if (token === "-" || !token.startsWith("-")) return token; const optionName = token.replace(/^-+/u, "").split("=", 1)[0]?.toLowerCase() ?? ""; if (!token.includes("=")) index += SQLITE_OPEN_OPTIONS_WITH_VALUES.get(optionName) ?? 0; } return null; } function parseSqliteDatabaseTokens(argv) { if (normalizeCommandBaseName(argv[0]) !== "sqlite3") return []; const databaseTokens = []; const commandTokens = []; for (let index = 1; index < argv.length; index += 1) { const token = expectDefined(argv[index], "sqlite3 argv entry"); if (token === "--") { const databaseToken = argv[index + 1]; if (databaseToken) databaseTokens.push(databaseToken); commandTokens.push(...argv.slice(index + 2)); break; } if (token === "-" || !token.startsWith("-")) { databaseTokens.push(token); commandTokens.push(...argv.slice(index + 1)); break; } const optionName = token.replace(/^-+/u, "").split("=", 1)[0]?.toLowerCase() ?? ""; if (optionName === "cmd") { const commandToken = token.includes("=") ? token.slice(token.indexOf("=") + 1) : argv[index + 1]; if (commandToken) commandTokens.push(commandToken); } if (!token.includes("=")) index += SQLITE_OPTIONS_WITH_VALUES.get(optionName) ?? 0; } for (const commandToken of commandTokens) { const databaseToken = parseSqliteOpenCommandDatabaseToken(commandToken); if (databaseToken) databaseTokens.push(databaseToken); } return databaseTokens; } function expandSqliteDatabaseToken(token, stateDir) { let expanded = token.trim(); if (!expanded || expanded === ":memory:") return null; const stateVariable = expanded.match(/^\$(?:OPENCLAW_STATE_DIR|\{OPENCLAW_STATE_DIR\})(?=$|[\\/])/u); if (stateVariable) expanded = `${stateDir}${expanded.slice(stateVariable[0].length)}`; const homeVariable = expanded.match(/^\$(?:HOME|\{HOME\})(?=$|[\\/])/u); if (homeVariable) expanded = `${os.homedir()}${expanded.slice(homeVariable[0].length)}`; if (expanded === "~" || expanded.startsWith("~/") || expanded.startsWith("~\\")) expanded = path.join(os.homedir(), expanded.slice(2)); if (expanded.toLowerCase().startsWith("file:")) try { expanded = fileURLToPath(expanded); } catch { const filename = expanded.slice(5).split(/[?#]/u, 1)[0]; if (!filename) return null; try { expanded = decodeURIComponent(filename); } catch { expanded = filename; } } return expanded; } function targetsLiveStateSqliteDatabase(argv, context) { const stateDir = context.stateDir?.trim(); if (!stateDir) return false; const canonicalStateDir = resolvePathViaExistingAncestorSync(stateDir); return parseSqliteDatabaseTokens(argv).some((databaseToken) => { const expandedTarget = expandSqliteDatabaseToken(databaseToken, stateDir); if (!expandedTarget) return false; const targetPath = path.isAbsolute(expandedTarget) ? expandedTarget : path.resolve(context.workdir ?? process.cwd(), expandedTarget); const canonicalTarget = resolvePathViaExistingAncestorSync(targetPath); return canonicalTarget === canonicalStateDir || isPathInside(canonicalStateDir, canonicalTarget); }); } async function detectUnsafeExecControlShellCommand(command, context = {}) { const rawCommand = command.trim(); let explanation = null; try { explanation = await explainShellCommand(rawCommand); } catch (error) { if (error instanceof CommandExplanationWorkLimitError) return "incomplete-analysis"; } const { controlCandidates, argvCandidates } = (() => { if (explanation?.ok) { const commands = [...explanation.topLevelCommands, ...explanation.nestedCommands]; return { controlCandidates: commands.flatMap((step) => buildCommandPayloadCandidates(step.argv)), argvCandidates: commands.flatMap((step) => buildCommandPayloadArgvCandidates(step.argv)) }; } const fallbackArgv = normalizeStringEntries(rawCommand.split(/\r?\n/)).map((line) => { return { argv: splitShellArgs(line), line }; }); return { controlCandidates: fallbackArgv.flatMap(({ argv, line }) => argv ? buildCommandPayloadCandidates(argv) : [line]), argvCandidates: fallbackArgv.flatMap(({ argv, line }) => argv ? buildCommandPayloadArgvCandidates(argv) : [[line]]) }; })(); for (const candidate of controlCandidates) { if (parseExecApprovalShellCommand(candidate)) return "approve"; if (parseOpenClawChannelsLoginShellCommand(candidate)) return "channel-login"; } for (const candidateArgv of argvCandidates) if (targetsLiveStateSqliteDatabase(candidateArgv, context)) return "live-state-sqlite"; return null; } function rejectIncompleteCommandAnalysis(unsafeKind) { if (unsafeKind === "incomplete-analysis") throw new Error("exec cannot run a shell command that exceeds the command explanation work limit. Simplify the command so its complete syntax can be inspected before execution."); } async function rejectUnsafeExecControlShellCommand(command) { const unsafeKind = await detectUnsafeExecControlShellCommand(command); rejectIncompleteCommandAnalysis(unsafeKind); if (unsafeKind === "approve") throw new Error(["exec cannot run /approve commands.", "Show the /approve command to the user as chat text, or route it through the approval command handler instead of shell execution."].join(" ")); if (unsafeKind === "channel-login") throw new Error(["exec cannot run interactive OpenClaw channel login commands.", "Run `openclaw channels login` in a terminal on the gateway host, or use the channel-specific login agent tool when available (for WhatsApp: `whatsapp_login`)."].join(" ")); } async function rejectUnsafeExecLiveStateSqliteShellCommand(command, context) { const unsafeKind = await detectUnsafeExecControlShellCommand(command, context); rejectIncompleteCommandAnalysis(unsafeKind); if (unsafeKind !== "live-state-sqlite") return; throw new Error(["external sqlite3 cannot open databases under the active OpenClaw state directory.", "Use OpenClaw commands for live state, or inspect a private backup copy outside `OPENCLAW_STATE_DIR`."].join(" ")); } //#endregion export { rejectUnsafeExecControlShellCommand as n, rejectUnsafeExecLiveStateSqliteShellCommand as r, detectUnsafeExecControlShellCommand as t };