mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-05 22:32:12 +00:00
fix(exec): resolve remote approval regressions (#58792)
* fix(exec): restore remote approval policy defaults * fix(exec): handle headless cron approval conflicts * fix(exec): make allow-always durable * fix(exec): persist exact-command shell trust * fix(doctor): match host exec fallback * fix(exec): preserve blocked and inline approval state * Doctor: surface allow-always ask bypass * Doctor: match effective exec policy * Exec: match node durable command text * Exec: tighten durable approval security * Exec: restore owner approver fallback * Config: refresh Slack approval metadata --------- Co-authored-by: scoootscooob <zhentongfan@gmail.com>
This commit is contained in:
@@ -91,6 +91,20 @@ describe("evaluateSystemRunPolicy", () => {
|
||||
expect(denied.requiresAsk).toBe(true);
|
||||
});
|
||||
|
||||
it("still requires approval when ask=always even with durable trust", () => {
|
||||
const denied = expectDeniedDecision(
|
||||
evaluateSystemRunPolicy(
|
||||
buildPolicyParams({
|
||||
security: "full",
|
||||
ask: "always",
|
||||
durableApprovalSatisfied: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(denied.eventReason).toBe("approval-required");
|
||||
expect(denied.requiresAsk).toBe(true);
|
||||
});
|
||||
|
||||
it("allows allowlist miss when explicit approval is provided", () => {
|
||||
const allowed = expectAllowedDecision(
|
||||
evaluateSystemRunPolicy(
|
||||
|
||||
@@ -54,6 +54,7 @@ export function evaluateSystemRunPolicy(params: {
|
||||
ask: ExecAsk;
|
||||
analysisOk: boolean;
|
||||
allowlistSatisfied: boolean;
|
||||
durableApprovalSatisfied?: boolean;
|
||||
approvalDecision: ExecApprovalDecision;
|
||||
approved?: boolean;
|
||||
isWindows: boolean;
|
||||
@@ -87,6 +88,7 @@ export function evaluateSystemRunPolicy(params: {
|
||||
security: params.security,
|
||||
analysisOk,
|
||||
allowlistSatisfied,
|
||||
durableApprovalSatisfied: params.durableApprovalSatisfied,
|
||||
});
|
||||
if (requiresAsk && !approvedByAsk) {
|
||||
return {
|
||||
@@ -104,6 +106,18 @@ export function evaluateSystemRunPolicy(params: {
|
||||
}
|
||||
|
||||
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",
|
||||
|
||||
@@ -17,6 +17,7 @@ export type SystemRunAllowlistAnalysis = {
|
||||
allowlistMatches: ExecAllowlistEntry[];
|
||||
allowlistSatisfied: boolean;
|
||||
segments: ExecCommandSegment[];
|
||||
segmentAllowlistEntries: Array<ExecAllowlistEntry | null>;
|
||||
};
|
||||
|
||||
export function evaluateSystemRunAllowlist(params: {
|
||||
@@ -53,6 +54,7 @@ export function evaluateSystemRunAllowlist(params: {
|
||||
? allowlistEval.allowlistSatisfied
|
||||
: false,
|
||||
segments: allowlistEval.segments,
|
||||
segmentAllowlistEntries: allowlistEval.segmentAllowlistEntries,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -73,6 +75,7 @@ export function evaluateSystemRunAllowlist(params: {
|
||||
allowlistSatisfied:
|
||||
params.security === "allowlist" && analysis.ok ? allowlistEval.allowlistSatisfied : false,
|
||||
segments: analysis.segments,
|
||||
segmentAllowlistEntries: allowlistEval.segmentAllowlistEntries,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
@@ -1480,4 +1481,60 @@ describe("handleSystemRunInvoke mac app exec host routing", () => {
|
||||
clearRuntimeConfigSnapshot();
|
||||
}
|
||||
});
|
||||
|
||||
it("reuses exact-command durable trust for shell-wrapper reruns", async () => {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-shell-wrapper-allow-"));
|
||||
try {
|
||||
const prepared = buildSystemRunApprovalPlan({
|
||||
command: ["/bin/sh", "-lc", "cd ."],
|
||||
cwd: tempDir,
|
||||
});
|
||||
expect(prepared.ok).toBe(true);
|
||||
if (!prepared.ok) {
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
await withTempApprovalsHome({
|
||||
approvals: {
|
||||
version: 1,
|
||||
defaults: { security: "allowlist", ask: "on-miss", askFallback: "full" },
|
||||
agents: {
|
||||
main: {
|
||||
allowlist: [
|
||||
{
|
||||
pattern: `=command:${crypto
|
||||
.createHash("sha256")
|
||||
.update(prepared.plan.commandText)
|
||||
.digest("hex")
|
||||
.slice(0, 16)}`,
|
||||
source: "allow-always",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
run: async () => {
|
||||
const rerun = await runSystemInvoke({
|
||||
preferMacAppExecHost: false,
|
||||
command: prepared.plan.argv,
|
||||
rawCommand: prepared.plan.commandText,
|
||||
systemRunPlan: prepared.plan,
|
||||
cwd: prepared.plan.cwd ?? tempDir,
|
||||
security: "allowlist",
|
||||
ask: "on-miss",
|
||||
runCommand: vi.fn(async () => createLocalRunResult("shell-wrapper-reused")),
|
||||
});
|
||||
|
||||
expect(rerun.runCommand).toHaveBeenCalledTimes(1);
|
||||
expectInvokeOk(rerun.sendInvokeResult, { payloadContains: "shell-wrapper-reused" });
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,9 @@ import { resolveAgentConfig } from "../agents/agent-scope.js";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import type { GatewayClient } from "../gateway/client.js";
|
||||
import {
|
||||
addDurableCommandApproval,
|
||||
addAllowlistEntry,
|
||||
hasDurableExecApproval,
|
||||
recordAllowlistUse,
|
||||
resolveApprovalAuditCandidatePath,
|
||||
resolveAllowAlwaysPatterns,
|
||||
@@ -96,6 +98,7 @@ type SystemRunPolicyPhase = SystemRunParsePhase & {
|
||||
approvals: ResolvedExecApprovals;
|
||||
security: ExecSecurity;
|
||||
policy: ReturnType<typeof evaluateSystemRunPolicy>;
|
||||
durableApprovalSatisfied: boolean;
|
||||
strictInlineEval: boolean;
|
||||
inlineEvalHit: ReturnType<typeof detectInterpreterInlineEvalArgv>;
|
||||
allowlistMatches: ExecAllowlistEntry[];
|
||||
@@ -332,19 +335,20 @@ async function evaluateSystemRunPolicyPhase(
|
||||
onWarning: warnWritableTrustedDirOnce,
|
||||
});
|
||||
const bins = autoAllowSkills ? await opts.skillBins.current() : [];
|
||||
let { analysisOk, allowlistMatches, allowlistSatisfied, segments } = evaluateSystemRunAllowlist({
|
||||
shellCommand: parsed.shellPayload,
|
||||
argv: parsed.argv,
|
||||
approvals,
|
||||
security,
|
||||
safeBins,
|
||||
safeBinProfiles,
|
||||
trustedSafeBinDirs,
|
||||
cwd: parsed.cwd,
|
||||
env: parsed.env,
|
||||
skillBins: bins,
|
||||
autoAllowSkills,
|
||||
});
|
||||
let { analysisOk, allowlistMatches, allowlistSatisfied, segments, segmentAllowlistEntries } =
|
||||
evaluateSystemRunAllowlist({
|
||||
shellCommand: parsed.shellPayload,
|
||||
argv: parsed.argv,
|
||||
approvals,
|
||||
security,
|
||||
safeBins,
|
||||
safeBinProfiles,
|
||||
trustedSafeBinDirs,
|
||||
cwd: parsed.cwd,
|
||||
env: parsed.env,
|
||||
skillBins: bins,
|
||||
autoAllowSkills,
|
||||
});
|
||||
const strictInlineEval =
|
||||
agentExec?.strictInlineEval === true || cfg.tools?.exec?.strictInlineEval === true;
|
||||
const inlineEvalHit = strictInlineEval
|
||||
@@ -358,11 +362,18 @@ async function evaluateSystemRunPolicyPhase(
|
||||
const cmdInvocation = parsed.shellPayload
|
||||
? opts.isCmdExeInvocation(segments[0]?.argv ?? [])
|
||||
: opts.isCmdExeInvocation(parsed.argv);
|
||||
const durableApprovalSatisfied = hasDurableExecApproval({
|
||||
analysisOk,
|
||||
segmentAllowlistEntries,
|
||||
allowlist: approvals.allowlist,
|
||||
commandText: parsed.commandText,
|
||||
});
|
||||
const policy = evaluateSystemRunPolicy({
|
||||
security,
|
||||
ask,
|
||||
analysisOk,
|
||||
allowlistSatisfied,
|
||||
durableApprovalSatisfied,
|
||||
approvalDecision: parsed.approvalDecision,
|
||||
approved: parsed.approved,
|
||||
isWindows,
|
||||
@@ -390,7 +401,12 @@ async function evaluateSystemRunPolicyPhase(
|
||||
}
|
||||
|
||||
// Fail closed if policy/runtime drift re-allows unapproved shell wrappers.
|
||||
if (security === "allowlist" && parsed.shellPayload && !policy.approvedByAsk) {
|
||||
if (
|
||||
security === "allowlist" &&
|
||||
parsed.shellPayload &&
|
||||
!policy.approvedByAsk &&
|
||||
!durableApprovalSatisfied
|
||||
) {
|
||||
await sendSystemRunDenied(opts, parsed.execution, {
|
||||
reason: "approval-required",
|
||||
message: "SYSTEM_RUN_DENIED: approval required",
|
||||
@@ -440,6 +456,7 @@ async function evaluateSystemRunPolicyPhase(
|
||||
approvals,
|
||||
security,
|
||||
policy,
|
||||
durableApprovalSatisfied,
|
||||
strictInlineEval,
|
||||
inlineEvalHit,
|
||||
allowlistMatches,
|
||||
@@ -546,25 +563,24 @@ async function executeSystemRunPhase(
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
phase.policy.approvalDecision === "allow-always" &&
|
||||
phase.security === "allowlist" &&
|
||||
phase.inlineEvalHit === null
|
||||
) {
|
||||
if (phase.policy.analysisOk) {
|
||||
const patterns = resolveAllowAlwaysPatterns({
|
||||
segments: phase.segments,
|
||||
cwd: phase.cwd,
|
||||
env: phase.env,
|
||||
platform: process.platform,
|
||||
strictInlineEval: phase.strictInlineEval,
|
||||
});
|
||||
for (const pattern of patterns) {
|
||||
if (pattern) {
|
||||
addAllowlistEntry(phase.approvals.file, phase.agentId, pattern);
|
||||
}
|
||||
if (phase.policy.approvalDecision === "allow-always" && phase.inlineEvalHit === null) {
|
||||
const patterns = resolveAllowAlwaysPatterns({
|
||||
segments: phase.segments,
|
||||
cwd: phase.cwd,
|
||||
env: phase.env,
|
||||
platform: process.platform,
|
||||
strictInlineEval: phase.strictInlineEval,
|
||||
});
|
||||
for (const pattern of patterns) {
|
||||
if (pattern) {
|
||||
addAllowlistEntry(phase.approvals.file, phase.agentId, pattern, {
|
||||
source: "allow-always",
|
||||
});
|
||||
}
|
||||
}
|
||||
if (patterns.length === 0) {
|
||||
addDurableCommandApproval(phase.approvals.file, phase.agentId, phase.commandText);
|
||||
}
|
||||
}
|
||||
|
||||
if (phase.allowlistMatches.length > 0) {
|
||||
|
||||
Reference in New Issue
Block a user