File size: 4,721 Bytes
253e783 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | /** Evaluates node-host exec policy from security, approval, and allowlist context. */
import { resolveAgentConfig } from "../agents/agent-scope-config.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
requiresExecApproval,
resolveExecModePolicy,
type ExecAsk,
type ExecSecurity,
} from "../infra/exec-approvals.js";
import { applyExecPolicyLayer } from "../infra/exec-policy.js";
/** One config owner for system.run and plugin-hosted execution. */
export function resolveNodeExecConfigPolicy(params: {
cfg: OpenClawConfig;
agentId: string | undefined;
defaultSecurity: ExecSecurity;
defaultAsk: ExecAsk;
}) {
const agentExec = params.agentId
? resolveAgentConfig(params.cfg, params.agentId)?.tools?.exec
: undefined;
const globalExec = params.cfg.tools?.exec;
const layered = applyExecPolicyLayer(
applyExecPolicyLayer({ security: params.defaultSecurity, ask: params.defaultAsk }, globalExec),
agentExec,
);
return { agentExec, globalExec, ...resolveExecModePolicy(layered) };
}
type ExecApprovalDecision = "allow-once" | "allow-always" | null;
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;
}
);
/** Normalizes raw approval decisions from node-host payloads. */
export function resolveExecApprovalDecision(value: unknown): ExecApprovalDecision {
if (value === "allow-once" || value === "allow-always") {
return value;
}
return null;
}
function formatSystemRunAllowlistMissMessage(params?: {
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)"
);
}
return "SYSTEM_RUN_DENIED: allowlist miss";
}
/** Combines exec security, allowlist analysis, and approval state into an allow/deny decision. */
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;
const requiresAsk =
params.security !== "deny" &&
requiresExecApproval({
ask: params.ask,
security: params.security,
analysisOk,
allowlistSatisfied,
durableApprovalSatisfied: params.durableApprovalSatisfied,
});
const context = {
analysisOk,
allowlistSatisfied,
shellWrapperBlocked,
windowsShellWrapperBlocked,
requiresAsk,
approvalDecision: params.approvalDecision,
approvedByAsk,
};
if (params.security === "deny") {
return {
allowed: false,
eventReason: "security=deny",
errorMessage: "SYSTEM_RUN_DISABLED: security=deny",
...context,
};
}
if (requiresAsk && !approvedByAsk) {
return {
allowed: false,
eventReason: "approval-required",
errorMessage: "SYSTEM_RUN_DENIED: approval required",
...context,
};
}
if (
params.security === "allowlist" &&
(!analysisOk || !allowlistSatisfied) &&
!approvedByAsk &&
!params.durableApprovalSatisfied
) {
return {
allowed: false,
eventReason: "allowlist-miss",
errorMessage: formatSystemRunAllowlistMissMessage({
windowsShellWrapperBlocked,
}),
...context,
};
}
return {
allowed: true,
...context,
};
}
|