Files
openclaw/src/node-host/exec-policy.ts
Nimrod Gutman d008e2d015 fix(exec): align node shell allowlist wrappers (#62401)
* fix(exec): align node shell allowlist wrappers

* fix: align node shell allowlist wrappers (#62401) (thanks @ngutman)
2026-04-07 13:05:57 +03:00

156 lines
4.5 KiB
TypeScript

import { requiresExecApproval, type ExecAsk, type ExecSecurity } from "../infra/exec-approvals.js";
export type ExecApprovalDecision = "allow-once" | "allow-always" | null;
export type SystemRunPolicyDecision = {
analysisOk: boolean;
allowlistSatisfied: boolean;
shellWrapperBlocked: boolean;
windowsShellWrapperBlocked: boolean;
requiresAsk: boolean;
approvalDecision: ExecApprovalDecision;
approvedByAsk: boolean;
} & (
| {
allowed: true;
}
| {
allowed: false;
eventReason: "security=deny" | "approval-required" | "allowlist-miss";
errorMessage: string;
}
);
export function resolveExecApprovalDecision(value: unknown): ExecApprovalDecision {
if (value === "allow-once" || value === "allow-always") {
return value;
}
return null;
}
export function formatSystemRunAllowlistMissMessage(params?: {
shellWrapperBlocked?: boolean;
windowsShellWrapperBlocked?: boolean;
}): string {
if (params?.windowsShellWrapperBlocked) {
return (
"SYSTEM_RUN_DENIED: allowlist miss " +
"(Windows shell wrappers like cmd.exe /c require approval; " +
"approve once/always or run with --ask on-miss|always)"
);
}
if (params?.shellWrapperBlocked) {
return (
"SYSTEM_RUN_DENIED: allowlist miss " +
"(shell wrappers like sh/bash/zsh -c require approval; " +
"approve once/always or run with --ask on-miss|always)"
);
}
return "SYSTEM_RUN_DENIED: allowlist miss";
}
export function evaluateSystemRunPolicy(params: {
security: ExecSecurity;
ask: ExecAsk;
analysisOk: boolean;
allowlistSatisfied: boolean;
durableApprovalSatisfied?: boolean;
approvalDecision: ExecApprovalDecision;
approved?: boolean;
isWindows: boolean;
cmdInvocation: boolean;
shellWrapperInvocation: boolean;
}): SystemRunPolicyDecision {
// POSIX node execution intentionally uses `/bin/sh -lc` as a transport wrapper.
// Keep allowlist decisions based on the analyzed inner shell payload there.
// Windows `cmd.exe /c` wrappers still require explicit approval because they
// change execution semantics for builtins and quoting/parsing behavior.
const windowsShellWrapperBlocked =
params.security === "allowlist" &&
params.shellWrapperInvocation &&
params.isWindows &&
params.cmdInvocation;
const shellWrapperBlocked = windowsShellWrapperBlocked;
const analysisOk = shellWrapperBlocked ? false : params.analysisOk;
const allowlistSatisfied = shellWrapperBlocked ? false : params.allowlistSatisfied;
const approvedByAsk = params.approvalDecision !== null || params.approved === true;
if (params.security === "deny") {
return {
allowed: false,
eventReason: "security=deny",
errorMessage: "SYSTEM_RUN_DISABLED: security=deny",
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk: false,
approvalDecision: params.approvalDecision,
approvedByAsk,
};
}
const requiresAsk = requiresExecApproval({
ask: params.ask,
security: params.security,
analysisOk,
allowlistSatisfied,
durableApprovalSatisfied: params.durableApprovalSatisfied,
});
if (requiresAsk && !approvedByAsk) {
return {
allowed: false,
eventReason: "approval-required",
errorMessage: "SYSTEM_RUN_DENIED: approval required",
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk,
approvalDecision: params.approvalDecision,
approvedByAsk,
};
}
if (params.security === "allowlist" && (!analysisOk || !allowlistSatisfied) && !approvedByAsk) {
if (params.durableApprovalSatisfied) {
return {
allowed: true,
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk,
approvalDecision: params.approvalDecision,
approvedByAsk,
};
}
return {
allowed: false,
eventReason: "allowlist-miss",
errorMessage: formatSystemRunAllowlistMissMessage({
shellWrapperBlocked,
windowsShellWrapperBlocked,
}),
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk,
approvalDecision: params.approvalDecision,
approvedByAsk,
};
}
return {
allowed: true,
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk,
approvalDecision: params.approvalDecision,
approvedByAsk,
};
}