Files
openclaw/src/node-host/invoke-system-run-plan.ts
2026-03-13 13:09:36 +00:00

1088 lines
27 KiB
TypeScript

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import type {
SystemRunApprovalFileOperand,
SystemRunApprovalPlan,
} from "../infra/exec-approvals.js";
import { resolveCommandResolutionFromArgv } from "../infra/exec-command-resolution.js";
import { isInterpreterLikeSafeBin } from "../infra/exec-safe-bin-runtime-policy.js";
import {
POSIX_SHELL_WRAPPERS,
normalizeExecutableToken,
unwrapKnownDispatchWrapperInvocation,
unwrapKnownShellMultiplexerInvocation,
} from "../infra/exec-wrapper-resolution.js";
import { sameFileIdentity } from "../infra/file-identity.js";
import {
POSIX_INLINE_COMMAND_FLAGS,
resolveInlineCommandMatch,
} from "../infra/shell-inline-command.js";
import { formatExecCommand, resolveSystemRunCommandRequest } from "../infra/system-run-command.js";
import { splitShellArgs } from "../utils/shell-argv.js";
export type ApprovedCwdSnapshot = {
cwd: string;
stat: fs.Stats;
};
const MUTABLE_ARGV1_INTERPRETER_PATTERNS = [
/^(?:node|nodejs)$/,
/^perl$/,
/^php$/,
/^python(?:\d+(?:\.\d+)*)?$/,
/^ruby$/,
] as const;
const GENERIC_MUTABLE_SCRIPT_RUNNERS = new Set([
"esno",
"jiti",
"ts-node",
"ts-node-esm",
"tsx",
"vite-node",
]);
const BUN_SUBCOMMANDS = new Set([
"add",
"audit",
"completions",
"create",
"exec",
"help",
"init",
"install",
"link",
"outdated",
"patch",
"pm",
"publish",
"remove",
"repl",
"run",
"test",
"unlink",
"update",
"upgrade",
"x",
]);
const BUN_OPTIONS_WITH_VALUE = new Set([
"--backend",
"--bunfig",
"--conditions",
"--config",
"--console-depth",
"--cwd",
"--define",
"--elide-lines",
"--env-file",
"--extension-order",
"--filter",
"--hot",
"--inspect",
"--inspect-brk",
"--inspect-wait",
"--install",
"--jsx-factory",
"--jsx-fragment",
"--jsx-import-source",
"--loader",
"--origin",
"--port",
"--preload",
"--smol",
"--tsconfig-override",
"-c",
"-e",
"-p",
"-r",
]);
const DENO_RUN_OPTIONS_WITH_VALUE = new Set([
"--cached-only",
"--cert",
"--config",
"--env-file",
"--ext",
"--harmony-import-attributes",
"--import-map",
"--inspect",
"--inspect-brk",
"--inspect-wait",
"--location",
"--log-level",
"--lock",
"--node-modules-dir",
"--no-check",
"--preload",
"--reload",
"--seed",
"--strace-ops",
"--unstable-bare-node-builtins",
"--v8-flags",
"--watch",
"--watch-exclude",
"-L",
]);
const NODE_OPTIONS_WITH_FILE_VALUE = new Set([
"-r",
"--experimental-loader",
"--import",
"--loader",
"--require",
]);
const RUBY_UNSAFE_APPROVAL_FLAGS = new Set(["-I", "-r", "--require"]);
const PERL_UNSAFE_APPROVAL_FLAGS = new Set(["-I", "-M", "-m"]);
const POSIX_SHELL_OPTIONS_WITH_VALUE = new Set([
"--init-file",
"--rcfile",
"--startup-script",
"-o",
]);
const NPM_EXEC_OPTIONS_WITH_VALUE = new Set([
"--cache",
"--package",
"--prefix",
"--script-shell",
"--userconfig",
"--workspace",
"-p",
"-w",
]);
const NPM_EXEC_FLAG_OPTIONS = new Set([
"--no",
"--quiet",
"--ws",
"--workspaces",
"--yes",
"-q",
"-y",
]);
const PNPM_OPTIONS_WITH_VALUE = new Set([
"--config",
"--dir",
"--filter",
"--reporter",
"--stream",
"--test-pattern",
"--workspace-concurrency",
"-C",
]);
const PNPM_FLAG_OPTIONS = new Set([
"--aggregate-output",
"--color",
"--recursive",
"--silent",
"--workspace-root",
"-r",
]);
type FileOperandCollection = {
hits: number[];
sawOptionValueFile: boolean;
};
function normalizeString(value: unknown): string | null {
if (typeof value !== "string") {
return null;
}
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
function pathComponentsFromRootSync(targetPath: string): string[] {
const absolute = path.resolve(targetPath);
const parts: string[] = [];
let cursor = absolute;
while (true) {
parts.unshift(cursor);
const parent = path.dirname(cursor);
if (parent === cursor) {
return parts;
}
cursor = parent;
}
}
function isWritableByCurrentProcessSync(candidate: string): boolean {
try {
fs.accessSync(candidate, fs.constants.W_OK);
return true;
} catch {
return false;
}
}
function hasMutableSymlinkPathComponentSync(targetPath: string): boolean {
for (const component of pathComponentsFromRootSync(targetPath)) {
try {
if (!fs.lstatSync(component).isSymbolicLink()) {
continue;
}
const parentDir = path.dirname(component);
if (isWritableByCurrentProcessSync(parentDir)) {
return true;
}
} catch {
return true;
}
}
return false;
}
function shouldPinExecutableForApproval(params: {
shellCommand: string | null;
wrapperChain: string[] | undefined;
}): boolean {
if (params.shellCommand !== null) {
return false;
}
return (params.wrapperChain?.length ?? 0) === 0;
}
function hashFileContentsSync(filePath: string): string {
return crypto.createHash("sha256").update(fs.readFileSync(filePath)).digest("hex");
}
function looksLikePathToken(token: string): boolean {
return (
token.startsWith(".") ||
token.startsWith("/") ||
token.startsWith("\\") ||
token.includes("/") ||
token.includes("\\") ||
path.extname(token).length > 0
);
}
function resolvesToExistingFileSync(rawOperand: string, cwd: string | undefined): boolean {
if (!rawOperand) {
return false;
}
try {
return fs.statSync(path.resolve(cwd ?? process.cwd(), rawOperand)).isFile();
} catch {
return false;
}
}
function unwrapArgvForMutableOperand(argv: string[]): { argv: string[]; baseIndex: number } {
let current = argv;
let baseIndex = 0;
while (true) {
const dispatchUnwrap = unwrapKnownDispatchWrapperInvocation(current);
if (dispatchUnwrap.kind === "unwrapped") {
baseIndex += current.length - dispatchUnwrap.argv.length;
current = dispatchUnwrap.argv;
continue;
}
const shellMultiplexerUnwrap = unwrapKnownShellMultiplexerInvocation(current);
if (shellMultiplexerUnwrap.kind === "unwrapped") {
baseIndex += current.length - shellMultiplexerUnwrap.argv.length;
current = shellMultiplexerUnwrap.argv;
continue;
}
const packageManagerUnwrap = unwrapKnownPackageManagerExecInvocation(current);
if (packageManagerUnwrap) {
baseIndex += current.length - packageManagerUnwrap.length;
current = packageManagerUnwrap;
continue;
}
return { argv: current, baseIndex };
}
}
function unwrapKnownPackageManagerExecInvocation(argv: string[]): string[] | null {
const executable = normalizePackageManagerExecToken(argv[0] ?? "");
switch (executable) {
case "npm":
return unwrapNpmExecInvocation(argv);
case "npx":
case "bunx":
return unwrapDirectPackageExecInvocation(argv);
case "pnpm":
return unwrapPnpmExecInvocation(argv);
default:
return null;
}
}
function normalizePackageManagerExecToken(token: string): string {
const normalized = normalizeExecutableToken(token);
if (!normalized) {
return normalized;
}
// Approval binding only promises best-effort recovery of the effective runtime
// command for common package-manager shims; it is not full package-manager semantics.
return normalized.replace(/\.(?:c|m)?js$/i, "");
}
function unwrapPnpmExecInvocation(argv: string[]): string[] | null {
let idx = 1;
while (idx < argv.length) {
const token = argv[idx]?.trim() ?? "";
if (!token) {
idx += 1;
continue;
}
if (token === "--") {
idx += 1;
continue;
}
if (!token.startsWith("-")) {
if (token === "exec") {
if (idx + 1 >= argv.length) {
return null;
}
const tail = argv.slice(idx + 1);
return tail[0] === "--" ? (tail.length > 1 ? tail.slice(1) : null) : tail;
}
if (token === "node") {
const tail = argv.slice(idx + 1);
const normalizedTail = tail[0] === "--" ? tail.slice(1) : tail;
return ["node", ...normalizedTail];
}
return null;
}
const [flag] = token.toLowerCase().split("=", 2);
if (PNPM_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (PNPM_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return null;
}
return null;
}
function unwrapDirectPackageExecInvocation(argv: string[]): string[] | null {
let idx = 1;
while (idx < argv.length) {
const token = argv[idx]?.trim() ?? "";
if (!token) {
idx += 1;
continue;
}
if (!token.startsWith("-")) {
return argv.slice(idx);
}
const [flag] = token.toLowerCase().split("=", 2);
if (flag === "-c" || flag === "--call") {
return null;
}
if (NPM_EXEC_OPTIONS_WITH_VALUE.has(flag)) {
idx += token.includes("=") ? 1 : 2;
continue;
}
if (NPM_EXEC_FLAG_OPTIONS.has(flag)) {
idx += 1;
continue;
}
return null;
}
return null;
}
function unwrapNpmExecInvocation(argv: string[]): string[] | null {
let idx = 1;
while (idx < argv.length) {
const token = argv[idx]?.trim() ?? "";
if (!token) {
idx += 1;
continue;
}
if (!token.startsWith("-")) {
if (token !== "exec") {
return null;
}
idx += 1;
break;
}
if (
(token === "-C" || token === "--prefix" || token === "--userconfig") &&
!token.includes("=")
) {
idx += 2;
continue;
}
idx += 1;
}
if (idx >= argv.length) {
return null;
}
const tail = argv.slice(idx);
if (tail[0] === "--") {
return tail.length > 1 ? tail.slice(1) : null;
}
return unwrapDirectPackageExecInvocation(["npx", ...tail]);
}
function resolvePosixShellScriptOperandIndex(argv: string[]): number | null {
if (
resolveInlineCommandMatch(argv, POSIX_INLINE_COMMAND_FLAGS, {
allowCombinedC: true,
}).valueTokenIndex !== null
) {
return null;
}
let afterDoubleDash = false;
for (let i = 1; i < argv.length; i += 1) {
const token = argv[i]?.trim() ?? "";
if (!token) {
continue;
}
if (token === "-") {
return null;
}
if (!afterDoubleDash && token === "--") {
afterDoubleDash = true;
continue;
}
if (!afterDoubleDash && token === "-s") {
return null;
}
if (!afterDoubleDash && token.startsWith("-")) {
const [flag] = token.toLowerCase().split("=", 2);
if (POSIX_SHELL_OPTIONS_WITH_VALUE.has(flag)) {
if (!token.includes("=")) {
i += 1;
}
continue;
}
continue;
}
return i;
}
return null;
}
function resolveOptionFilteredFileOperandIndex(params: {
argv: string[];
startIndex: number;
cwd: string | undefined;
optionsWithValue?: ReadonlySet<string>;
}): number | null {
let afterDoubleDash = false;
for (let i = params.startIndex; i < params.argv.length; i += 1) {
const token = params.argv[i]?.trim() ?? "";
if (!token) {
continue;
}
if (afterDoubleDash) {
return resolvesToExistingFileSync(token, params.cwd) ? i : null;
}
if (token === "--") {
afterDoubleDash = true;
continue;
}
if (token === "-") {
return null;
}
if (token.startsWith("-")) {
if (!token.includes("=") && params.optionsWithValue?.has(token)) {
i += 1;
}
continue;
}
return resolvesToExistingFileSync(token, params.cwd) ? i : null;
}
return null;
}
function resolveOptionFilteredPositionalIndex(params: {
argv: string[];
startIndex: number;
optionsWithValue?: ReadonlySet<string>;
}): number | null {
let afterDoubleDash = false;
for (let i = params.startIndex; i < params.argv.length; i += 1) {
const token = params.argv[i]?.trim() ?? "";
if (!token) {
continue;
}
if (afterDoubleDash) {
return i;
}
if (token === "--") {
afterDoubleDash = true;
continue;
}
if (token === "-") {
return null;
}
if (token.startsWith("-")) {
if (!token.includes("=") && params.optionsWithValue?.has(token)) {
i += 1;
}
continue;
}
return i;
}
return null;
}
function collectExistingFileOperandIndexes(params: {
argv: string[];
startIndex: number;
cwd: string | undefined;
optionsWithFileValue?: ReadonlySet<string>;
}): FileOperandCollection {
let afterDoubleDash = false;
const hits: number[] = [];
for (let i = params.startIndex; i < params.argv.length; i += 1) {
const token = params.argv[i]?.trim() ?? "";
if (!token) {
continue;
}
if (afterDoubleDash) {
if (resolvesToExistingFileSync(token, params.cwd)) {
hits.push(i);
}
continue;
}
if (token === "--") {
afterDoubleDash = true;
continue;
}
if (token === "-") {
return { hits: [], sawOptionValueFile: false };
}
if (token.startsWith("-")) {
const [flag, inlineValue] = token.split("=", 2);
if (params.optionsWithFileValue?.has(flag.toLowerCase())) {
if (inlineValue && resolvesToExistingFileSync(inlineValue, params.cwd)) {
hits.push(i);
return { hits, sawOptionValueFile: true };
}
const nextToken = params.argv[i + 1]?.trim() ?? "";
if (!inlineValue && nextToken && resolvesToExistingFileSync(nextToken, params.cwd)) {
hits.push(i + 1);
return { hits, sawOptionValueFile: true };
}
}
continue;
}
if (resolvesToExistingFileSync(token, params.cwd)) {
hits.push(i);
}
}
return { hits, sawOptionValueFile: false };
}
function resolveGenericInterpreterScriptOperandIndex(params: {
argv: string[];
cwd: string | undefined;
optionsWithFileValue?: ReadonlySet<string>;
}): number | null {
const collection = collectExistingFileOperandIndexes({
argv: params.argv,
startIndex: 1,
cwd: params.cwd,
optionsWithFileValue: params.optionsWithFileValue,
});
if (collection.sawOptionValueFile) {
return null;
}
return collection.hits.length === 1 ? collection.hits[0] : null;
}
function resolveBunScriptOperandIndex(params: {
argv: string[];
cwd: string | undefined;
}): number | null {
const directIndex = resolveOptionFilteredPositionalIndex({
argv: params.argv,
startIndex: 1,
optionsWithValue: BUN_OPTIONS_WITH_VALUE,
});
if (directIndex === null) {
return null;
}
const directToken = params.argv[directIndex]?.trim() ?? "";
if (directToken === "run") {
return resolveOptionFilteredFileOperandIndex({
argv: params.argv,
startIndex: directIndex + 1,
cwd: params.cwd,
optionsWithValue: BUN_OPTIONS_WITH_VALUE,
});
}
if (BUN_SUBCOMMANDS.has(directToken)) {
return null;
}
if (!looksLikePathToken(directToken)) {
return null;
}
return directIndex;
}
function resolveDenoRunScriptOperandIndex(params: {
argv: string[];
cwd: string | undefined;
}): number | null {
if ((params.argv[1]?.trim() ?? "") !== "run") {
return null;
}
return resolveOptionFilteredFileOperandIndex({
argv: params.argv,
startIndex: 2,
cwd: params.cwd,
optionsWithValue: DENO_RUN_OPTIONS_WITH_VALUE,
});
}
function hasRubyUnsafeApprovalFlag(argv: string[]): boolean {
let afterDoubleDash = false;
for (let i = 1; i < argv.length; i += 1) {
const token = argv[i]?.trim() ?? "";
if (!token) {
continue;
}
if (afterDoubleDash) {
return false;
}
if (token === "--") {
afterDoubleDash = true;
continue;
}
if (token === "-I" || token === "-r") {
return true;
}
if (token.startsWith("-I") || token.startsWith("-r")) {
return true;
}
if (RUBY_UNSAFE_APPROVAL_FLAGS.has(token.toLowerCase())) {
return true;
}
}
return false;
}
function hasPerlUnsafeApprovalFlag(argv: string[]): boolean {
let afterDoubleDash = false;
for (let i = 1; i < argv.length; i += 1) {
const token = argv[i]?.trim() ?? "";
if (!token) {
continue;
}
if (afterDoubleDash) {
return false;
}
if (token === "--") {
afterDoubleDash = true;
continue;
}
if (token === "-I" || token === "-M" || token === "-m") {
return true;
}
if (token.startsWith("-I") || token.startsWith("-M") || token.startsWith("-m")) {
return true;
}
if (PERL_UNSAFE_APPROVAL_FLAGS.has(token)) {
return true;
}
}
return false;
}
function isMutableScriptRunner(executable: string): boolean {
return GENERIC_MUTABLE_SCRIPT_RUNNERS.has(executable) || isInterpreterLikeSafeBin(executable);
}
function resolveMutableFileOperandIndex(argv: string[], cwd: string | undefined): number | null {
const unwrapped = unwrapArgvForMutableOperand(argv);
const executable = normalizeExecutableToken(unwrapped.argv[0] ?? "");
if (!executable) {
return null;
}
if ((POSIX_SHELL_WRAPPERS as ReadonlySet<string>).has(executable)) {
const shellIndex = resolvePosixShellScriptOperandIndex(unwrapped.argv);
return shellIndex === null ? null : unwrapped.baseIndex + shellIndex;
}
if (MUTABLE_ARGV1_INTERPRETER_PATTERNS.some((pattern) => pattern.test(executable))) {
const operand = unwrapped.argv[1]?.trim() ?? "";
if (operand && operand !== "-" && !operand.startsWith("-")) {
return unwrapped.baseIndex + 1;
}
}
if (executable === "bun") {
const bunIndex = resolveBunScriptOperandIndex({
argv: unwrapped.argv,
cwd,
});
if (bunIndex !== null) {
return unwrapped.baseIndex + bunIndex;
}
}
if (executable === "deno") {
const denoIndex = resolveDenoRunScriptOperandIndex({
argv: unwrapped.argv,
cwd,
});
if (denoIndex !== null) {
return unwrapped.baseIndex + denoIndex;
}
}
if (executable === "ruby" && hasRubyUnsafeApprovalFlag(unwrapped.argv)) {
return null;
}
if (executable === "perl" && hasPerlUnsafeApprovalFlag(unwrapped.argv)) {
return null;
}
if (!isMutableScriptRunner(executable)) {
return null;
}
const genericIndex = resolveGenericInterpreterScriptOperandIndex({
argv: unwrapped.argv,
cwd,
optionsWithFileValue:
executable === "node" || executable === "nodejs" ? NODE_OPTIONS_WITH_FILE_VALUE : undefined,
});
return genericIndex === null ? null : unwrapped.baseIndex + genericIndex;
}
function shellPayloadNeedsStableBinding(shellCommand: string, cwd: string | undefined): boolean {
const argv = splitShellArgs(shellCommand);
if (!argv || argv.length === 0) {
return false;
}
const snapshot = resolveMutableFileOperandSnapshotSync({
argv,
cwd,
shellCommand: null,
});
if (!snapshot.ok) {
return true;
}
if (snapshot.snapshot) {
return true;
}
const firstToken = argv[0]?.trim() ?? "";
return resolvesToExistingFileSync(firstToken, cwd);
}
function requiresStableInterpreterApprovalBindingWithShellCommand(params: {
argv: string[];
shellCommand: string | null;
cwd: string | undefined;
}): boolean {
if (params.shellCommand !== null) {
return shellPayloadNeedsStableBinding(params.shellCommand, params.cwd);
}
const unwrapped = unwrapArgvForMutableOperand(params.argv);
const executable = normalizeExecutableToken(unwrapped.argv[0] ?? "");
if (!executable) {
return false;
}
if ((POSIX_SHELL_WRAPPERS as ReadonlySet<string>).has(executable)) {
return false;
}
return isMutableScriptRunner(executable);
}
export function resolveMutableFileOperandSnapshotSync(params: {
argv: string[];
cwd: string | undefined;
shellCommand: string | null;
}): { ok: true; snapshot: SystemRunApprovalFileOperand | null } | { ok: false; message: string } {
const argvIndex = resolveMutableFileOperandIndex(params.argv, params.cwd);
if (argvIndex === null) {
if (
requiresStableInterpreterApprovalBindingWithShellCommand({
argv: params.argv,
shellCommand: params.shellCommand,
cwd: params.cwd,
})
) {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval cannot safely bind this interpreter/runtime command",
};
}
return { ok: true, snapshot: null };
}
const rawOperand = params.argv[argvIndex]?.trim();
if (!rawOperand) {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires a stable script operand",
};
}
const resolvedPath = path.resolve(params.cwd ?? process.cwd(), rawOperand);
let realPath: string;
let stat: fs.Stats;
try {
realPath = fs.realpathSync(resolvedPath);
stat = fs.statSync(realPath);
} catch {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires an existing script operand",
};
}
if (!stat.isFile()) {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires a file script operand",
};
}
return {
ok: true,
snapshot: {
argvIndex,
path: realPath,
sha256: hashFileContentsSync(realPath),
},
};
}
function resolveCanonicalApprovalCwdSync(cwd: string):
| {
ok: true;
snapshot: ApprovedCwdSnapshot;
}
| { ok: false; message: string } {
const requestedCwd = path.resolve(cwd);
let cwdLstat: fs.Stats;
let cwdStat: fs.Stats;
let cwdReal: string;
let cwdRealStat: fs.Stats;
try {
cwdLstat = fs.lstatSync(requestedCwd);
cwdStat = fs.statSync(requestedCwd);
cwdReal = fs.realpathSync(requestedCwd);
cwdRealStat = fs.statSync(cwdReal);
} catch {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires an existing canonical cwd",
};
}
if (!cwdStat.isDirectory()) {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires cwd to be a directory",
};
}
if (hasMutableSymlinkPathComponentSync(requestedCwd)) {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires canonical cwd (no symlink path components)",
};
}
if (cwdLstat.isSymbolicLink()) {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires canonical cwd (no symlink cwd)",
};
}
if (
!sameFileIdentity(cwdStat, cwdLstat) ||
!sameFileIdentity(cwdStat, cwdRealStat) ||
!sameFileIdentity(cwdLstat, cwdRealStat)
) {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval cwd identity mismatch",
};
}
return {
ok: true,
snapshot: {
cwd: cwdReal,
stat: cwdStat,
},
};
}
export function revalidateApprovedCwdSnapshot(params: { snapshot: ApprovedCwdSnapshot }): boolean {
const current = resolveCanonicalApprovalCwdSync(params.snapshot.cwd);
if (!current.ok) {
return false;
}
return sameFileIdentity(params.snapshot.stat, current.snapshot.stat);
}
export function revalidateApprovedMutableFileOperand(params: {
snapshot: SystemRunApprovalFileOperand;
argv: string[];
cwd: string | undefined;
}): boolean {
const operand = params.argv[params.snapshot.argvIndex]?.trim();
if (!operand) {
return false;
}
const resolvedPath = path.resolve(params.cwd ?? process.cwd(), operand);
let realPath: string;
try {
realPath = fs.realpathSync(resolvedPath);
} catch {
return false;
}
if (realPath !== params.snapshot.path) {
return false;
}
try {
return hashFileContentsSync(realPath) === params.snapshot.sha256;
} catch {
return false;
}
}
export function hardenApprovedExecutionPaths(params: {
approvedByAsk: boolean;
argv: string[];
shellCommand: string | null;
cwd: string | undefined;
}):
| {
ok: true;
argv: string[];
argvChanged: boolean;
cwd: string | undefined;
approvedCwdSnapshot: ApprovedCwdSnapshot | undefined;
}
| { ok: false; message: string } {
if (!params.approvedByAsk) {
return {
ok: true,
argv: params.argv,
argvChanged: false,
cwd: params.cwd,
approvedCwdSnapshot: undefined,
};
}
let hardenedCwd = params.cwd;
let approvedCwdSnapshot: ApprovedCwdSnapshot | undefined;
if (hardenedCwd) {
const canonicalCwd = resolveCanonicalApprovalCwdSync(hardenedCwd);
if (!canonicalCwd.ok) {
return canonicalCwd;
}
hardenedCwd = canonicalCwd.snapshot.cwd;
approvedCwdSnapshot = canonicalCwd.snapshot;
}
if (params.argv.length === 0) {
return {
ok: true,
argv: params.argv,
argvChanged: false,
cwd: hardenedCwd,
approvedCwdSnapshot,
};
}
const resolution = resolveCommandResolutionFromArgv(params.argv, hardenedCwd);
if (
!shouldPinExecutableForApproval({
shellCommand: params.shellCommand,
wrapperChain: resolution?.wrapperChain,
})
) {
// Preserve wrapper semantics for approval-based execution. Pinning the
// effective executable while keeping wrapper argv shape can shift positional
// arguments and execute a different command than approved.
return {
ok: true,
argv: params.argv,
argvChanged: false,
cwd: hardenedCwd,
approvedCwdSnapshot,
};
}
const pinnedExecutable = resolution?.resolvedRealPath ?? resolution?.resolvedPath;
if (!pinnedExecutable) {
return {
ok: false,
message: "SYSTEM_RUN_DENIED: approval requires a stable executable path",
};
}
if (pinnedExecutable === params.argv[0]) {
return {
ok: true,
argv: params.argv,
argvChanged: false,
cwd: hardenedCwd,
approvedCwdSnapshot,
};
}
const argv = [...params.argv];
argv[0] = pinnedExecutable;
return {
ok: true,
argv,
argvChanged: true,
cwd: hardenedCwd,
approvedCwdSnapshot,
};
}
export function buildSystemRunApprovalPlan(params: {
command?: unknown;
rawCommand?: unknown;
cwd?: unknown;
agentId?: unknown;
sessionKey?: unknown;
}): { ok: true; plan: SystemRunApprovalPlan } | { ok: false; message: string } {
const command = resolveSystemRunCommandRequest({
command: params.command,
rawCommand: params.rawCommand,
});
if (!command.ok) {
return { ok: false, message: command.message };
}
if (command.argv.length === 0) {
return { ok: false, message: "command required" };
}
const hardening = hardenApprovedExecutionPaths({
approvedByAsk: true,
argv: command.argv,
shellCommand: command.shellPayload,
cwd: normalizeString(params.cwd) ?? undefined,
});
if (!hardening.ok) {
return { ok: false, message: hardening.message };
}
const commandText = formatExecCommand(hardening.argv);
const commandPreview =
command.previewText?.trim() && command.previewText.trim() !== commandText
? command.previewText.trim()
: null;
const mutableFileOperand = resolveMutableFileOperandSnapshotSync({
argv: hardening.argv,
cwd: hardening.cwd,
shellCommand: command.shellPayload,
});
if (!mutableFileOperand.ok) {
return { ok: false, message: mutableFileOperand.message };
}
return {
ok: true,
plan: {
argv: hardening.argv,
cwd: hardening.cwd ?? null,
commandText,
commandPreview,
agentId: normalizeString(params.agentId),
sessionKey: normalizeString(params.sessionKey),
mutableFileOperand: mutableFileOperand.snapshot ?? undefined,
},
};
}