/** Tests node-host system.run policy, approval, allowlist, and execution behavior. */ import crypto from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, type Mock, vi, } from "vitest"; import { clearRuntimeConfigSnapshot, getRuntimeConfigSnapshot, setRuntimeConfigSnapshot, } from "../config/runtime-snapshot.js"; import { deleteExecApprovalsConfigRow } from "../infra/exec-approvals-sqlite.js"; import { testing as execApprovalsStoreTesting } from "../infra/exec-approvals-store.test-support.js"; import type { ExecAsk, ExecSecurity, SystemRunApprovalPlan } from "../infra/exec-approvals.js"; import { commitExecAuthorizationLocked, createExecApprovalPolicySnapshot, loadExecApprovals, saveExecApprovals, } from "../infra/exec-approvals.js"; import type { ExecAutoReviewer } from "../infra/exec-auto-review.js"; import * as commandResolution from "../infra/exec-command-resolution.js"; import type { ExecHostResponse } from "../infra/exec-host.js"; import { sanitizeHostExecEnv } from "../infra/host-env-security.js"; import { formatExecCommand } from "../infra/system-run-command.js"; import { closeOpenClawStateDatabaseForTest, openOpenClawStateDatabase, } from "../state/openclaw-state-db.js"; import { withEnvAsync } from "../test-utils/env.js"; import { buildSystemRunApprovalPlan } from "./invoke-system-run-plan.js"; import { handleSystemRunInvoke } from "./invoke-system-run.js"; type HandleSystemRunInvokeOptions = Parameters[0]; vi.mock("../logger.js", async (importOriginal) => ({ ...(await importOriginal()), logWarn: vi.fn(), })); type MockedRunCommand = Mock; type MockedRunViaMacAppExecHost = Mock; type MockedSendInvokeResult = Mock; type MockedSendExecFinishedEvent = Mock; type MockedSendNodeEvent = Mock; type InvokeSpies = { runCommand: MockedRunCommand; runViaMacAppExecHost: MockedRunViaMacAppExecHost; sendInvokeResult: MockedSendInvokeResult; sendExecFinishedEvent: MockedSendExecFinishedEvent; sendNodeEvent: MockedSendNodeEvent; }; type InvokeResult = { ok?: boolean; payloadJSON?: string; error?: { code?: string; message?: string }; }; type MacExecHostCall = { approvals?: { agent?: { security?: string; ask?: string } }; request?: { command?: string[]; rawCommand?: string; cwd?: string; approvalDecision?: string | null; approvalSource?: string | null; policySnapshot?: unknown; }; }; describe("handleSystemRunInvoke mac app exec host routing", () => { let sharedFixtureRoot = ""; let sharedOpenClawHome = ""; let sharedRuntimeBinDir = ""; let sharedFixtureId = 0; let previousOpenClawHome: string | undefined; const sharedRuntimeBins = new Set(); beforeAll(() => { closeOpenClawStateDatabaseForTest(); sharedFixtureRoot = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-node-host-fixtures-")), ); sharedOpenClawHome = path.join(sharedFixtureRoot, "openclaw-home"); sharedRuntimeBinDir = path.join(sharedFixtureRoot, "bin"); fs.mkdirSync(sharedOpenClawHome, { recursive: true }); fs.mkdirSync(sharedRuntimeBinDir, { recursive: true }); }); afterAll(() => { closeOpenClawStateDatabaseForTest(); if (sharedFixtureRoot) { fs.rmSync(sharedFixtureRoot, { recursive: true, force: true }); } }); function createFixtureDir(prefix: string): string { const dir = path.join(sharedFixtureRoot, `${prefix}${sharedFixtureId++}`); fs.mkdirSync(dir, { recursive: true }); return dir; } beforeEach(() => { previousOpenClawHome = process.env.OPENCLAW_HOME; process.env.OPENCLAW_HOME = sharedOpenClawHome; execApprovalsStoreTesting.reset(); // Cases isolate the canonical policy row, not shared-state schema bootstrap. deleteExecApprovalsConfigRow(openOpenClawStateDatabase().db); clearRuntimeConfigSnapshot(); }); afterEach(() => { execApprovalsStoreTesting.reset(); clearRuntimeConfigSnapshot(); if (previousOpenClawHome === undefined) { delete process.env.OPENCLAW_HOME; } else { process.env.OPENCLAW_HOME = previousOpenClawHome; } }); function createLocalRunResult(stdout = "local-ok") { return { success: true, stdout, stderr: "", timedOut: false, truncated: false, exitCode: 0, error: null, }; } function createTempExecutable(dir: string, name: string): string { const fileName = process.platform === "win32" ? `${name}.exe` : name; const executablePath = path.join(dir, fileName); fs.writeFileSync(executablePath, ""); fs.chmodSync(executablePath, 0o755); return executablePath; } function createStrictInlineEvalApprovalPlan(prefix: string): SystemRunApprovalPlan { const tempDir = createFixtureDir(prefix); const executablePath = createTempExecutable(tempDir, "gawk"); const scriptPath = path.join(tempDir, "library.awk"); fs.writeFileSync(scriptPath, "{ print }\n"); const prepared = buildSessionApprovalPlan( [executablePath, "-f", scriptPath, '--source=BEGIN{print "safe"}'], "agent:main:main", ); if (!prepared.ok) { throw new Error(prepared.message); } return prepared.plan; } function bindCurrentPolicyToPlan(plan: SystemRunApprovalPlan): SystemRunApprovalPlan { const agentId = plan.agentId ?? "main"; return { ...plan, agentId, sessionKey: plan.sessionKey ?? "agent:main:main", policySnapshot: createExecApprovalPolicySnapshot({ file: loadExecApprovals(), agentId, }), }; } function requireApprovalPlan( prepared: ReturnType, message: string, ): asserts prepared is Extract, { ok: true }> { if (!prepared.ok) { throw new Error(message); } } function buildSessionApprovalPlan(command: string[], sessionKey: string) { return buildSystemRunApprovalPlan({ command, sessionKey }); } function buildCwdApprovalPlan(command: string[], cwd: string) { return buildSystemRunApprovalPlan({ command, cwd }); } function buildCwdSessionApprovalPlan(command: string[], cwd: string, sessionKey: string) { return buildSystemRunApprovalPlan({ command, cwd, sessionKey }); } function expectInvokeOk(sendInvokeResult: MockedSendInvokeResult, payloadContains?: string) { const params = payloadContains === undefined ? undefined : { payloadContains }; const result = requireInvokeResult(sendInvokeResult); expect(result.ok).toBe(true); if (params?.payloadContains) { expect(result.payloadJSON).toContain(params.payloadContains); } } function expectInvokeErrorMessage( sendInvokeResult: MockedSendInvokeResult, expectedMessage: string, exact = false, ) { const params = { message: expectedMessage, exact }; const result = requireInvokeResult(sendInvokeResult); expect(result.ok).toBe(false); const message = result.error?.message; if (params.exact) { expect(message).toBe(params.message); } else { expect(message).toContain(params.message); } } function requireInvokeResult(sendInvokeResult: MockedSendInvokeResult): InvokeResult { const result = firstMockCallArg(sendInvokeResult, "sendInvokeResult", 0); return result as InvokeResult; } function requireFirstRunCommandArgs(runCommand: MockedRunCommand): string[] { return firstMockCallArg(vi.mocked(runCommand), "runCommand", 0) as string[]; } function requireMacExecHostCall( runViaMacAppExecHost: MockedRunViaMacAppExecHost, ): MacExecHostCall { const call = firstMockCallArg(runViaMacAppExecHost, "runViaMacAppExecHost", 0); return call as MacExecHostCall; } function firstMockCallArg( mock: { mock: { calls: readonly unknown[][] } }, label: string, argIndex: number, ): unknown { const [call] = mock.mock.calls; if (!call) { throw new Error(`expected ${label} call`); } return call[argIndex]; } function expectExecDeniedEvent( sendNodeEvent: MockedSendNodeEvent, reason = "approval-required", ): void { const call = sendNodeEvent.mock.calls[0]; if (!call) { throw new Error("expected sendNodeEvent call"); } expect(call[1]).toBe("exec.denied"); expect((call[2] as { reason?: string }).reason).toBe(reason); } function expectApprovalRequiredDenied( sendNodeEvent: MockedSendNodeEvent, sendInvokeResult: MockedSendInvokeResult, ) { const params = { sendNodeEvent, sendInvokeResult }; expectExecDeniedEvent(params.sendNodeEvent); expectInvokeErrorMessage(params.sendInvokeResult, "SYSTEM_RUN_DENIED: approval required", true); } function expectApprovalStateWriteDenied(params: { sendNodeEvent: MockedSendNodeEvent; sendInvokeResult: MockedSendInvokeResult; }) { expectExecDeniedEvent(params.sendNodeEvent, "approval-state-write-failed"); expect(requireInvokeResult(params.sendInvokeResult)).toMatchObject({ ok: false, error: { code: "SYSTEM_RUN_DENIED", message: "SYSTEM_RUN_DENIED: approval state could not be persisted", }, }); } function createMutableScriptOperandFixture(tmp: string): { command: string[]; scriptPath: string; initialBody: string; changedBody: string; } { if (process.platform === "win32") { const scriptPath = path.join(tmp, "run.js"); return { command: [process.execPath, "./run.js"], scriptPath, initialBody: 'console.log("SAFE");\n', changedBody: 'console.log("PWNED");\n', }; } const scriptPath = path.join(tmp, "run.sh"); return { command: ["/bin/sh", "./run.sh"], scriptPath, initialBody: "#!/bin/sh\necho SAFE\n", changedBody: "#!/bin/sh\necho PWNED\n", }; } function createRuntimeScriptOperandFixture( tmp: string, runtime: "bun" | "deno" | "jiti" | "tsx", ): { command: string[]; scriptPath: string; initialBody: string; changedBody: string; } { const scriptPath = path.join(tmp, "run.ts"); const initialBody = 'console.log("SAFE");\n'; const changedBody = 'console.log("PWNED");\n'; switch (runtime) { case "bun": return { command: ["bun", "run", "./run.ts"], scriptPath, initialBody, changedBody, }; case "deno": return { command: ["deno", "run", "-A", "--allow-read", "--", "./run.ts"], scriptPath, initialBody, changedBody, }; case "jiti": return { command: ["jiti", "./run.ts"], scriptPath, initialBody, changedBody, }; case "tsx": return { command: ["tsx", "./run.ts"], scriptPath, initialBody, changedBody, }; } const unsupportedRuntime: never = runtime; throw new Error(`unsupported runtime fixture: ${String(unsupportedRuntime)}`); } function buildNestedEnvShellCommand(params: { depth: number; payload: string }): string[] { return [...Array(params.depth).fill("/usr/bin/env"), "/bin/sh", "-c", params.payload]; } function createMacExecHostSuccess(stdout = "app-ok"): ExecHostResponse { return { ok: true, payload: { success: true, stdout, stderr: "", timedOut: false, exitCode: 0, error: null, }, }; } function createAllowlistOnMissApprovals(params?: { autoAllowSkills?: boolean; agents?: Parameters[0]["agents"]; }): Parameters[0] { return { version: 1, defaults: { security: "allowlist", ask: "on-miss", askFallback: "deny", ...(params?.autoAllowSkills ? { autoAllowSkills: true } : {}), }, agents: params?.agents ?? {}, }; } function createApprovals( security: ExecSecurity, ask: ExecAsk, askFallback: ExecSecurity, agents?: Parameters[0]["agents"], ): Parameters[0] { return { version: 1, defaults: { security, ask, askFallback }, ...(agents === undefined ? {} : { agents }), }; } function createExactCommandPattern(commandText: string): string { return `=command:${crypto.createHash("sha256").update(commandText).digest("hex").slice(0, 16)}`; } function resolveProductionExecSecurity(value?: string): "deny" | "allowlist" | "full" { return value === "deny" || value === "allowlist" || value === "full" ? value : "allowlist"; } function resolveProductionExecAsk(value?: string): "off" | "on-miss" | "always" { return value === "off" || value === "on-miss" || value === "always" ? value : "on-miss"; } function createInvokeSpies(params?: { runCommand?: HandleSystemRunInvokeOptions["runCommand"]; runViaMacAppExecHost?: HandleSystemRunInvokeOptions["runViaMacAppExecHost"]; sendInvokeResult?: HandleSystemRunInvokeOptions["sendInvokeResult"]; sendExecFinishedEvent?: HandleSystemRunInvokeOptions["sendExecFinishedEvent"]; sendNodeEvent?: HandleSystemRunInvokeOptions["sendNodeEvent"]; }): InvokeSpies { return { runCommand: vi.fn(params?.runCommand ?? (async () => createLocalRunResult())), runViaMacAppExecHost: vi.fn(params?.runViaMacAppExecHost ?? (async () => null)), sendInvokeResult: vi.fn(params?.sendInvokeResult ?? (async () => {})), sendExecFinishedEvent: vi.fn(params?.sendExecFinishedEvent ?? (async () => {})), sendNodeEvent: vi.fn(params?.sendNodeEvent ?? (async () => {})), }; } function createPolicyMutationCommit( mutate: (current: ReturnType) => void, ): Mock> { return vi.fn(async (params) => { const current = loadExecApprovals(); mutate(current); saveExecApprovals(current); return await commitExecAuthorizationLocked(params); }); } async function withTempApprovalsHome( approvals: Parameters[0], run: (ctx: { tempHome: string }) => Promise, ): Promise { const tempHome = sharedOpenClawHome; return await withEnvAsync({ OPENCLAW_HOME: tempHome }, async () => { saveExecApprovals(approvals); return await run({ tempHome }); }); } async function withPathTokenCommand( tmpPrefix: string, run: (ctx: { link: string; expected: string }) => Promise, ): Promise { const tmp = createFixtureDir(tmpPrefix); const binDir = path.join(tmp, "bin"); fs.mkdirSync(binDir, { recursive: true }); const link = path.join(binDir, "poccmd"); fs.symlinkSync("/bin/echo", link); const expected = fs.realpathSync(link); return await withEnvAsync({ PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}` }, () => run({ link, expected }), ); } async function withFakeRuntimeOnPath( runtime: "bun" | "deno" | "jiti" | "tsx", run: () => Promise, ): Promise { if (!sharedRuntimeBins.has(runtime)) { const runtimePath = process.platform === "win32" ? path.join(sharedRuntimeBinDir, `${runtime}.cmd`) : path.join(sharedRuntimeBinDir, runtime); const runtimeBody = process.platform === "win32" ? "@echo off\r\nexit /b 0\r\n" : "#!/bin/sh\nexit 0\n"; fs.writeFileSync(runtimePath, runtimeBody, { mode: 0o755 }); if (process.platform !== "win32") { fs.chmodSync(runtimePath, 0o755); } sharedRuntimeBins.add(runtime); } return await withEnvAsync( { PATH: `${sharedRuntimeBinDir}${path.delimiter}${process.env.PATH ?? ""}` }, run, ); } function expectCommandPinnedToCanonicalPath( runCommand: MockedRunCommand, expected: string, commandTail: string[], cwd?: string, ) { const params = { runCommand, expected, commandTail, cwd }; expect(params.runCommand).toHaveBeenCalledWith( [params.expected, ...params.commandTail], params.cwd, undefined, undefined, undefined, expect.any(Function), ); } function resolveStatTargetPath(target: string | Buffer | URL | number): string { if (typeof target === "string") { return path.resolve(target); } if (Buffer.isBuffer(target)) { return path.resolve(target.toString()); } if (target instanceof URL) { return path.resolve(target.pathname); } return path.resolve(String(target)); } async function withMockedCwdIdentityDrift(params: { canonicalCwd: string; driftDir: string; stableHitsBeforeDrift?: number; run: () => Promise; }): Promise { const stableHitsBeforeDrift = params.stableHitsBeforeDrift ?? 2; const realStatSync = fs.statSync.bind(fs); const baselineStat = realStatSync(params.canonicalCwd); const driftStat = realStatSync(params.driftDir); let canonicalHits = 0; const statSpy = vi.spyOn(fs, "statSync").mockImplementation((...args) => { const resolvedTarget = resolveStatTargetPath(args[0]); if (resolvedTarget === params.canonicalCwd) { canonicalHits += 1; if (canonicalHits > stableHitsBeforeDrift) { return driftStat; } return baselineStat; } return realStatSync(...args); }); try { return await params.run(); } finally { statSpy.mockRestore(); } } async function runSystemInvoke(params: { preferMacAppExecHost: boolean; execHostFallbackAllowed?: boolean; runViaResponse?: ExecHostResponse | null; command?: string[]; env?: Record; rawCommand?: string | null; systemRunPlan?: SystemRunApprovalPlan | null; preparedPlan?: SystemRunApprovalPlan; cwd?: string; agentId?: string; security?: "full" | "allowlist"; ask?: "off" | "on-miss" | "always"; approvalDecision?: "allow" | "allow-once" | "allow-always" | "deny" | null; approvalSource?: string | null; approved?: boolean; needsScreenRecording?: boolean; suppressNotifyOnExit?: boolean; runCommand?: HandleSystemRunInvokeOptions["runCommand"]; runViaMacAppExecHost?: HandleSystemRunInvokeOptions["runViaMacAppExecHost"]; sendInvokeResult?: HandleSystemRunInvokeOptions["sendInvokeResult"]; sendExecFinishedEvent?: HandleSystemRunInvokeOptions["sendExecFinishedEvent"]; sendNodeEvent?: HandleSystemRunInvokeOptions["sendNodeEvent"]; skillBinsCurrent?: () => Promise>; isCmdExeInvocation?: HandleSystemRunInvokeOptions["isCmdExeInvocation"]; sanitizeEnv?: HandleSystemRunInvokeOptions["sanitizeEnv"]; resolveExecSecurity?: HandleSystemRunInvokeOptions["resolveExecSecurity"]; resolveExecAsk?: HandleSystemRunInvokeOptions["resolveExecAsk"]; autoReviewer?: ExecAutoReviewer; commitExecAuthorization?: HandleSystemRunInvokeOptions["commitExecAuthorization"]; prepareDelayedApprovalPlan?: boolean; signal?: AbortSignal; }): Promise { const spies = createInvokeSpies({ runCommand: params.runCommand, runViaMacAppExecHost: params.runViaMacAppExecHost ?? (async () => params.runViaResponse ?? null), sendInvokeResult: params.sendInvokeResult, sendExecFinishedEvent: params.sendExecFinishedEvent, sendNodeEvent: params.sendNodeEvent, }); const command = params.command ?? params.preparedPlan?.argv ?? ["echo", "ok"]; let dispatchCommand = command; let dispatchRawCommand = params.rawCommand ?? params.preparedPlan?.commandText; let dispatchCwd = params.cwd ?? params.preparedPlan?.cwd ?? undefined; let dispatchAgentId: string | undefined = params.agentId ?? "main"; const forwardsDelayedApproval = params.approvalSource === "auto-review" || params.approved === true || params.approvalDecision === "allow" || params.approvalDecision === "allow-once" || params.approvalDecision === "allow-always"; const providedPlan = params.preparedPlan ?? params.systemRunPlan ?? undefined; let systemRunPlan: SystemRunApprovalPlan | undefined = providedPlan ? { ...providedPlan, agentId: providedPlan.agentId ?? dispatchAgentId, sessionKey: providedPlan.sessionKey ?? "agent:main:main", } : undefined; if (forwardsDelayedApproval && params.prepareDelayedApprovalPlan !== false) { if (!systemRunPlan) { const prepared = buildSystemRunApprovalPlan({ command, rawCommand: params.rawCommand, cwd: params.cwd, agentId: dispatchAgentId, sessionKey: "agent:main:main", }); if (!prepared.ok) { throw new Error(prepared.message); } systemRunPlan = prepared.plan; dispatchCommand = prepared.plan.argv; dispatchRawCommand = prepared.plan.commandText; dispatchCwd = prepared.plan.cwd ?? undefined; dispatchAgentId = prepared.plan.agentId ?? undefined; } systemRunPlan = bindCurrentPolicyToPlan(systemRunPlan); } await handleSystemRunInvoke({ client: {} as never, params: { command: dispatchCommand, env: params.env, rawCommand: dispatchRawCommand, systemRunPlan, cwd: dispatchCwd, agentId: dispatchAgentId, approvalDecision: params.approvalDecision, approvalSource: params.approvalSource, approved: params.approved, needsScreenRecording: params.needsScreenRecording, suppressNotifyOnExit: params.suppressNotifyOnExit, sessionKey: "agent:main:main", }, skillBins: { current: params.skillBinsCurrent ?? (async () => []), }, signal: params.signal, execHostEnforced: false, execHostFallbackAllowed: params.execHostFallbackAllowed ?? true, resolveExecSecurity: params.resolveExecSecurity ?? (() => params.security ?? "full"), resolveExecAsk: params.resolveExecAsk ?? (() => params.ask ?? "off"), isCmdExeInvocation: params.isCmdExeInvocation ?? (() => false), sanitizeEnv: params.sanitizeEnv ?? (() => undefined), ...spies, buildExecEventPayload: (payload) => payload, preferMacAppExecHost: params.preferMacAppExecHost, getRuntimeConfig: () => getRuntimeConfigSnapshot() ?? {}, autoReviewer: params.autoReviewer, commitExecAuthorization: params.commitExecAuthorization, }); return spies; } type SystemInvokeFixtureParams = Parameters[0]; async function runLocalSystemInvoke( params: Omit = {}, ) { return await runSystemInvoke({ ...params, preferMacAppExecHost: false }); } async function runMacSystemInvoke( params: Omit = {}, ) { return await runSystemInvoke({ ...params, preferMacAppExecHost: true }); } type ExplicitSystemInvokePolicy = { security: NonNullable; ask: NonNullable; }; async function runLocalSystemInvokeWithPolicy( security: ExplicitSystemInvokePolicy["security"], ask: ExplicitSystemInvokePolicy["ask"], params: Omit = {}, ) { return await runLocalSystemInvoke({ ...params, security, ask }); } async function runMacSystemInvokeWithPolicy( security: ExplicitSystemInvokePolicy["security"], ask: ExplicitSystemInvokePolicy["ask"], params: Omit = {}, ) { return await runMacSystemInvoke({ ...params, security, ask }); } it("preserves a native cwd refusal without labelling it approval-required", async () => { const result = await runMacSystemInvoke({ runViaResponse: { ok: false, error: { code: "UNAVAILABLE", reason: "cwd-unavailable", message: "Working directory does not exist, is inaccessible, or is not a directory.", }, }, }); expectExecDeniedEvent(result.sendNodeEvent, "cwd-unavailable"); expect(result.runCommand).not.toHaveBeenCalled(); }); it("keeps a lost companion response ambiguous", async () => { const result = await runMacSystemInvoke({ execHostFallbackAllowed: false }); expect(result.runViaMacAppExecHost).toHaveBeenCalledOnce(); expect(result.runCommand).not.toHaveBeenCalled(); expect(requireInvokeResult(result.sendInvokeResult)).toMatchObject({ ok: false, error: { code: "UNAVAILABLE" }, }); }); it("forwards cancellation to locally spawned node commands", async () => { const controller = new AbortController(); const result = await runLocalSystemInvoke({ signal: controller.signal }); expect(result.runCommand.mock.calls[0]?.[4]).toBe(controller.signal); }); it("does not spawn an already-cancelled node command", async () => { const controller = new AbortController(); controller.abort(); const result = await runLocalSystemInvoke({ signal: controller.signal }); expect(result.runCommand).not.toHaveBeenCalled(); expect(result.runViaMacAppExecHost).not.toHaveBeenCalled(); }); it("does not publish a cancelled local command completion", async () => { const controller = new AbortController(); const result = await runLocalSystemInvoke({ signal: controller.signal, runCommand: async () => { controller.abort(); return createLocalRunResult("cancelled"); }, }); expect(result.runCommand).toHaveBeenCalledOnce(); expect(result.sendInvokeResult).not.toHaveBeenCalled(); expect(result.sendExecFinishedEvent).not.toHaveBeenCalled(); }); it.each([null, createMacExecHostSuccess()])( "cancels pending Mac exec without replay or publication (%j)", async (response) => { const controller = new AbortController(); const result = await runMacSystemInvoke({ signal: controller.signal, runViaMacAppExecHost: ({ signal }) => { expect(signal).toBe(controller.signal); return new Promise((resolve) => { signal?.addEventListener("abort", () => resolve(response), { once: true }); queueMicrotask(() => controller.abort()); }); }, }); expect(result.runViaMacAppExecHost).toHaveBeenCalledOnce(); expect(result.runCommand).not.toHaveBeenCalled(); expect(result.sendNodeEvent).not.toHaveBeenCalled(); expect(result.sendInvokeResult).not.toHaveBeenCalled(); expect(result.sendExecFinishedEvent).not.toHaveBeenCalled(); }, ); it("routes local, mac host, and canonical shell-wrapper requests", async () => { const localInvoke = await runLocalSystemInvoke({}); expect(localInvoke.runViaMacAppExecHost).not.toHaveBeenCalled(); expect(localInvoke.runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(localInvoke.sendInvokeResult, "local-ok"); const macHostInvoke = await runMacSystemInvoke({ runViaResponse: createMacExecHostSuccess() }); const macHostCall = requireMacExecHostCall(macHostInvoke.runViaMacAppExecHost); expect(macHostCall.approvals?.agent?.security).toBe("full"); expect(macHostCall.approvals?.agent?.ask).toBe("off"); expect(macHostCall.request?.command).toEqual(["echo", "ok"]); expect(macHostInvoke.runCommand).not.toHaveBeenCalled(); expectInvokeOk(macHostInvoke.sendInvokeResult, "app-ok"); const shellWrapperInvoke = await runMacSystemInvoke({ command: ["/bin/sh", "-lc", '$0 "$1"', "/usr/bin/touch", "/tmp/marker"], runViaResponse: createMacExecHostSuccess(), }); const shellWrapperCall = requireMacExecHostCall(shellWrapperInvoke.runViaMacAppExecHost); if (shellWrapperCall.approvals === undefined) { throw new Error("Expected shell-wrapper approvals"); } expect(shellWrapperCall.request?.command).toEqual([ "/bin/sh", "-lc", '$0 "$1"', "/usr/bin/touch", "/tmp/marker", ]); expect(shellWrapperCall.request?.rawCommand).toBe( '/bin/sh -lc "$0 \\"$1\\"" /usr/bin/touch /tmp/marker', ); }); it.each(["low", "medium"] as const)( "uses auto reviewer for system.run approval misses with %s risk when exec mode is auto", async (risk) => { const tmp = createFixtureDir("openclaw-system-run-auto-review-"); const executablePath = createTempExecutable(tmp, "read-info"); setRuntimeConfigSnapshot({ tools: { exec: { mode: "auto", }, }, }); try { const autoReviewer = vi.fn(() => ({ decision: "allow-once", rationale: "reads fixture metadata only", risk, })); const commitAuthorization = vi.fn(commitExecAuthorizationLocked); const runCommand = vi.fn(async () => createLocalRunResult("auto-reviewed")); const prepared = buildCwdApprovalPlan([executablePath], tmp); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const invoke = await runLocalSystemInvoke({ command: prepared.plan.argv, cwd: prepared.plan.cwd ?? tmp, systemRunPlan: prepared.plan, runCommand, resolveExecSecurity: resolveProductionExecSecurity, resolveExecAsk: resolveProductionExecAsk, autoReviewer, commitExecAuthorization: commitAuthorization, }); expect(autoReviewer).toHaveBeenCalledTimes(1); expect(autoReviewer).toHaveBeenCalledWith( expect.objectContaining({ command: executablePath, argv: [executablePath], cwd: tmp, host: "node", reason: "approval-required", analysis: expect.objectContaining({ parsed: true, allowlistMatched: false, inlineEval: false, }), }), ); expect(runCommand).toHaveBeenCalledTimes(1); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "auto-review" }), }), ); expectInvokeOk(invoke.sendInvokeResult, "auto-reviewed"); const macInvoke = await runMacSystemInvoke({ runViaResponse: createMacExecHostSuccess(), command: prepared.plan.argv, cwd: prepared.plan.cwd ?? tmp, systemRunPlan: prepared.plan, resolveExecSecurity: resolveProductionExecSecurity, resolveExecAsk: resolveProductionExecAsk, autoReviewer, }); const macCall = requireMacExecHostCall(macInvoke.runViaMacAppExecHost); expect(macCall.request?.approvalSource).toBe("auto-review"); expect(macCall.request?.approvalDecision).toBeNull(); expect(macCall.request?.policySnapshot).toEqual( createExecApprovalPolicySnapshot({ file: loadExecApprovals(), agentId: undefined }), ); expect(macInvoke.runCommand).not.toHaveBeenCalled(); expectInvokeOk(macInvoke.sendInvokeResult, "app-ok"); } finally { clearRuntimeConfigSnapshot(); } }, ); it("does not auto-review direct system.run approval misses without an approval plan", async () => { const tmp = createFixtureDir("openclaw-system-run-auto-review-no-plan-"); const executablePath = createTempExecutable(tmp, "read-info"); setRuntimeConfigSnapshot({ tools: { exec: { mode: "auto", }, }, }); try { const autoReviewer = vi.fn(() => ({ decision: "allow-once", rationale: "reads fixture metadata only", risk: "low", })); const runCommand = vi.fn(async () => createLocalRunResult("should-not-run")); const invoke = await runLocalSystemInvoke({ command: [executablePath], cwd: tmp, runCommand, resolveExecSecurity: resolveProductionExecSecurity, resolveExecAsk: resolveProductionExecAsk, autoReviewer, }); expect(autoReviewer).not.toHaveBeenCalled(); expect(runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage(invoke.sendInvokeResult, "SYSTEM_RUN_DENIED: approval required"); } finally { clearRuntimeConfigSnapshot(); } }); it.each([ { name: "throws synchronously", reviewer: () => { throw new Error("provider\n\u001b[31mfailed\u001b[0m\u202e"); }, }, { name: "rejects asynchronously", reviewer: async () => { throw new Error("provider\n\u001b[31mfailed\u001b[0m\u202e"); }, }, ])("denies direct system.run when its reviewer $name", async ({ reviewer }) => { const tmp = createFixtureDir("openclaw-system-run-auto-review-failure-"); const executablePath = createTempExecutable(tmp, "read-info"); setRuntimeConfigSnapshot({ tools: { exec: { mode: "auto" } } }); const autoReviewer = vi.fn(reviewer); const runCommand = vi.fn(async () => createLocalRunResult("should-not-run")); const prepared = buildCwdApprovalPlan([executablePath], tmp); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "expected a bound system.run approval plan"); const invoke = await runLocalSystemInvoke({ command: prepared.plan.argv, cwd: prepared.plan.cwd ?? tmp, systemRunPlan: prepared.plan, runCommand, resolveExecSecurity: resolveProductionExecSecurity, resolveExecAsk: resolveProductionExecAsk, autoReviewer, }); expect(autoReviewer).toHaveBeenCalledTimes(1); expect(runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "exec auto-review deferred to human approval: exec reviewer failed: provider\\nfailed", ); }); it.runIf(process.platform !== "win32").each(["bash", "sh", "/bin/sh"])( "does not auto-review direct %s login-shell startup", async (shell) => { const tmp = createFixtureDir("openclaw-system-run-auto-review-login-"); setRuntimeConfigSnapshot({ tools: { exec: { mode: "auto" } } }); try { const autoReviewer = vi.fn(() => ({ decision: "allow-once", rationale: "unsafe startup wrapper must not reach the reviewer", risk: "low", })); const loginCommand = `${shell} -lc "echo auto-review-startup-proof"`; const command = ["/bin/sh", "-lc", loginCommand]; // The real plan builder already rejects this wrapper. Exercise the // node trust boundary against a hostile, otherwise well-formed plan. const approvalPlan = { argv: command, cwd: tmp, commandText: formatExecCommand(command), agentId: "main", sessionKey: "agent:main:main", } satisfies SystemRunApprovalPlan; const invoke = await runLocalSystemInvoke({ command, rawCommand: approvalPlan.commandText, cwd: tmp, systemRunPlan: approvalPlan, resolveExecSecurity: resolveProductionExecSecurity, resolveExecAsk: resolveProductionExecAsk, autoReviewer, }); expect(autoReviewer).not.toHaveBeenCalled(); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage(invoke.sendInvokeResult, "SYSTEM_RUN_DENIED: approval required"); } finally { clearRuntimeConfigSnapshot(); } }, ); it("does not auto-review direct system.run security audit suppression edits", async () => { const tmp = createFixtureDir("openclaw-system-run-auto-review-suppression-"); const executablePath = createTempExecutable(tmp, "openclaw"); setRuntimeConfigSnapshot({ tools: { exec: { mode: "auto", }, }, }); try { const autoReviewer = vi.fn(() => ({ decision: "allow-once", rationale: "test reviewer would allow it", risk: "low", })); const runCommand = vi.fn(async () => createLocalRunResult("should-not-run")); const prepared = buildCwdApprovalPlan( [executablePath, "config", "set", "security.audit.suppressions", "[]"], tmp, ); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const invoke = await runLocalSystemInvoke({ command: prepared.plan.argv, cwd: prepared.plan.cwd ?? tmp, systemRunPlan: prepared.plan, runCommand, resolveExecSecurity: resolveProductionExecSecurity, resolveExecAsk: resolveProductionExecAsk, autoReviewer, }); expect(autoReviewer).not.toHaveBeenCalled(); expect(runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage(invoke.sendInvokeResult, "SYSTEM_RUN_DENIED: approval required"); } finally { clearRuntimeConfigSnapshot(); } }); it.each(["ask", "deny"] as const)( "does not execute when system.run auto reviewer returns %s", async (decision) => { const tmp = createFixtureDir("openclaw-system-run-auto-review-ask-"); const executablePath = createTempExecutable(tmp, "read-info"); setRuntimeConfigSnapshot({ tools: { exec: { mode: "auto", }, }, }); try { const autoReviewer = vi.fn(() => ({ decision, rationale: "needs a person", risk: "medium", })); const runCommand = vi.fn(async () => createLocalRunResult("should-not-run")); const prepared = buildCwdApprovalPlan([executablePath], tmp); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const invoke = await runLocalSystemInvoke({ command: prepared.plan.argv, cwd: prepared.plan.cwd ?? tmp, systemRunPlan: prepared.plan, runCommand, resolveExecSecurity: resolveProductionExecSecurity, resolveExecAsk: resolveProductionExecAsk, autoReviewer, }); expect(autoReviewer).toHaveBeenCalledTimes(1); expect(runCommand).not.toHaveBeenCalled(); if (decision === "deny") { expect(requireInvokeResult(invoke.sendInvokeResult)).toEqual({ ok: false, error: { code: "SYSTEM_RUN_DENIED", message: "SYSTEM_RUN_DENIED: auto-review denied (risk=medium): needs a person\n" + "Do not attempt the same outcome through a workaround, indirect execution, or policy circumvention. Proceed only with a materially safer alternative, or ask the user to approve this exact command after explaining the risk.", }, }); expect(invoke.sendNodeEvent).toHaveBeenCalledWith( expect.anything(), "exec.denied", expect.objectContaining({ reason: "auto-review-denied" }), ); } else { expectInvokeErrorMessage( invoke.sendInvokeResult, "exec auto-review deferred to human approval", ); } } finally { clearRuntimeConfigSnapshot(); } }, ); const approvedEnvShellWrapperCases = [ { name: "preserves wrapper argv for approved env shell commands in local execution", preferMacAppExecHost: false, }, { name: "preserves wrapper argv for approved env shell commands in mac app exec host forwarding", preferMacAppExecHost: true, }, ] as const; it.runIf(process.platform !== "win32")( "preserves wrapper argv for approved env shell commands", async () => { for (const testCase of approvedEnvShellWrapperCases) { const tmp = createFixtureDir("openclaw-approved-wrapper-"); const marker = path.join(tmp, "marker"); const attackerScript = path.join(tmp, "sh"); fs.writeFileSync(attackerScript, "#!/bin/sh\necho exploited > marker\n"); fs.chmodSync(attackerScript, 0o755); const runCommand = vi.fn(async (argv: string[]) => { if (argv[0] === "/bin/sh" && argv[1] === "sh" && argv[2] === "-c") { fs.writeFileSync(marker, "rewritten"); } return createLocalRunResult(); }); const sendInvokeResult = vi.fn(async () => {}); const invoke = await runSystemInvoke({ preferMacAppExecHost: testCase.preferMacAppExecHost, command: ["env", "sh", "-c", "echo SAFE"], cwd: tmp, approved: true, security: "allowlist", ask: "on-miss", runCommand, sendInvokeResult, runViaResponse: testCase.preferMacAppExecHost ? { ok: true, payload: { success: true, stdout: "app-ok", stderr: "", timedOut: false, exitCode: 0, error: null, }, } : undefined, }); if (testCase.preferMacAppExecHost) { const canonicalCwd = fs.realpathSync(tmp); expect(invoke.runCommand).not.toHaveBeenCalled(); const macHostCall = requireMacExecHostCall(invoke.runViaMacAppExecHost); if (macHostCall.approvals === undefined) { throw new Error("Expected Mac host approvals"); } expect(macHostCall.request?.command).toEqual(["env", "sh", "-c", "echo SAFE"]); expect(macHostCall.request?.rawCommand).toBe('env sh -c "echo SAFE"'); expect(macHostCall.request?.cwd).toBe(canonicalCwd); expect(macHostCall.request?.approvalDecision).toBe("allow-once"); expect(macHostCall.request?.approvalSource).toBeUndefined(); expect(macHostCall.request?.policySnapshot).toEqual( createExecApprovalPolicySnapshot({ file: loadExecApprovals(), agentId: undefined }), ); expectInvokeOk(invoke.sendInvokeResult, "app-ok"); continue; } expect(requireFirstRunCommandArgs(invoke.runCommand)).toEqual([ "env", "sh", "-c", "echo SAFE", ]); expect(fs.existsSync(marker)).toBe(false); expectInvokeOk(invoke.sendInvokeResult); } }, ); it("handles transparent and semantic env wrappers in allowlist mode", async () => { const oldPath = process.env.PATH; if (process.platform !== "win32") { process.env.PATH = "/usr/bin:/bin"; } try { const transparent = await runLocalSystemInvoke({ security: "allowlist", command: ["env", "tr", "a", "b"], }); if (process.platform === "win32") { expect(transparent.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage(transparent.sendInvokeResult, "allowlist miss"); } else { const expectedTrPath = fs.realpathSync( fs.existsSync("/usr/bin/tr") ? "/usr/bin/tr" : "/bin/tr", ); expect(requireFirstRunCommandArgs(transparent.runCommand)).toEqual([ expectedTrPath, "a", "b", ]); expectInvokeOk(transparent.sendInvokeResult); } const semantic = await runLocalSystemInvoke({ security: "allowlist", command: ["env", "FOO=bar", "tr", "a", "b"], }); expect(semantic.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage(semantic.sendInvokeResult, "allowlist miss"); } finally { if (oldPath === undefined) { delete process.env.PATH; } else { process.env.PATH = oldPath; } } }); it("denies shell payload carriers in allowlist mode without explicit approval", async () => { const shellPayloadCases: Array< | { label: string; command: string[]; ask?: "off" | "on-miss"; message: string; approvalRequired?: false; } | { label: string; command: string[]; ask?: "off" | "on-miss"; approvalRequired: true; } > = [ { label: "env -S", command: ["env", "-S", 'sh -c "echo pwned"'], message: "allowlist miss", ask: "off", }, { label: "semicolon chain simple command", command: process.platform === "win32" ? ["cmd.exe", "/d", "/s", "/c", "openclaw status; id"] : ["/bin/sh", "-lc", "openclaw status; id"], approvalRequired: true, }, { label: "semicolon chain path read", command: process.platform === "win32" ? ["cmd.exe", "/d", "/s", "/c", "openclaw status; cat /etc/passwd"] : ["/bin/sh", "-lc", "openclaw status; cat /etc/passwd"], approvalRequired: true, }, { label: "PowerShell encoded command", command: ["pwsh", "-EncodedCommand", "ZQBjAGgAbwAgAHAAdwBuAGUAZAA="], approvalRequired: true, }, ]; for (const testCase of shellPayloadCases) { const { runCommand, sendInvokeResult, sendNodeEvent } = await runLocalSystemInvokeWithPolicy( "allowlist", testCase.ask ?? "on-miss", { command: testCase.command }, ); expect(runCommand, testCase.label).not.toHaveBeenCalled(); if (testCase.approvalRequired) { expectApprovalRequiredDenied(sendNodeEvent, sendInvokeResult); } else if ("message" in testCase) { expectInvokeErrorMessage(sendInvokeResult, testCase.message); } } }); it.runIf(process.platform !== "win32")( "denies safe-bin shell expansion carriers in allowlist mode", async () => { const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy( "allowlist", "off", { command: ["/bin/sh", "-lc", "head -c${IFS}16${IFS}${OPENCLAW_CONFIG_PATH}"], rawCommand: "head -c${IFS}16${IFS}${OPENCLAW_CONFIG_PATH}", }, ); expect(runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage(sendInvokeResult, "allowlist miss"); }, ); it.runIf(process.platform !== "win32")( "rewrites safe-bin shell payloads before execution in allowlist mode", async () => { const oldPath = process.env.PATH; process.env.PATH = "/usr/bin:/bin"; try { const expectedHeadPath = fs.realpathSync( fs.existsSync("/usr/bin/head") ? "/usr/bin/head" : "/bin/head", ); const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy( "allowlist", "off", { command: ["/bin/sh", "-lc", "head -c 16"], rawCommand: "head -c 16" }, ); expect(requireFirstRunCommandArgs(runCommand)).toEqual([ "/bin/sh", "-lc", `${expectedHeadPath} -c 16`, ]); expectInvokeOk(sendInvokeResult); } finally { if (oldPath === undefined) { delete process.env.PATH; } else { process.env.PATH = oldPath; } } }, ); it.runIf(process.platform !== "win32")( "rewrites nested safe-bin shell chains before execution in allowlist mode", async () => { const oldPath = process.env.PATH; process.env.PATH = "/usr/bin:/bin"; try { const expectedTrPath = fs.realpathSync( fs.existsSync("/usr/bin/tr") ? "/usr/bin/tr" : "/bin/tr", ); const expectedHeadPath = fs.realpathSync( fs.existsSync("/usr/bin/head") ? "/usr/bin/head" : "/bin/head", ); const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy( "allowlist", "off", { command: ["/bin/sh", "-lc", "sh -c 'tr a b && head -c 16'"], rawCommand: "sh -c 'tr a b && head -c 16'", }, ); const payload = requireFirstRunCommandArgs(runCommand)[2] ?? ""; expect(payload).not.toContain("tr a b && head -c 16"); expect(payload).toContain(expectedTrPath); expect(payload).toContain(expectedHeadPath); expectInvokeOk(sendInvokeResult); } finally { if (oldPath === undefined) { delete process.env.PATH; } else { process.env.PATH = oldPath; } } }, ); it.runIf(process.platform !== "win32")( "does not apply POSIX safe-bin shell rewrites to PowerShell wrappers", async () => { const oldPath = process.env.PATH; process.env.PATH = "/usr/bin:/bin"; try { const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy( "allowlist", "off", { command: ["pwsh", "-Command", "head -c 16"] }, ); expect(requireFirstRunCommandArgs(runCommand)).toEqual(["pwsh", "-Command", "head -c 16"]); expectInvokeOk(sendInvokeResult); } finally { if (oldPath === undefined) { delete process.env.PATH; } else { process.env.PATH = oldPath; } } }, ); it("denies abbreviated PowerShell encoded payloads even when the wrapper is allowlisted", async () => { const binDir = createFixtureDir("openclaw-pwsh-allowlist-"); const executablePath = createTempExecutable(binDir, "pwsh"); await withTempApprovalsHome( createAllowlistOnMissApprovals({ agents: { main: { allowlist: [{ pattern: executablePath }], }, }, }), async () => { const { runCommand, sendInvokeResult, sendNodeEvent } = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command: [ executablePath, "-win", "hidden", "-if", "XML", "-config", "SomeConfig", "/NoProfile", "/ec", "VwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIABoAGkA", ], }); expect(runCommand).not.toHaveBeenCalled(); expectApprovalRequiredDenied(sendNodeEvent, sendInvokeResult); const commandWithArgs = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command: [executablePath, "-cwa", "Write-Output", "hi"], }); expect(commandWithArgs.runCommand).not.toHaveBeenCalled(); expectApprovalRequiredDenied( commandWithArgs.sendNodeEvent, commandWithArgs.sendInvokeResult, ); }, ); }); it.runIf(process.platform !== "win32")( "pins PATH-token executable to canonical path", async () => { await withPathTokenCommand("openclaw-approval-path-pin-", async ({ expected }) => { const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy( "full", "off", { command: ["poccmd", "-n", "SAFE"], approved: true }, ); expectCommandPinnedToCanonicalPath( runCommand, expected, ["-n", "SAFE"], fs.realpathSync(process.cwd()), ); expectInvokeOk(sendInvokeResult); }); }, ); it.runIf(process.platform !== "win32")( "pins PATH-token executable to canonical path for allowlist runs", async () => { const runCommand = vi.fn(async () => ({ ...createLocalRunResult(), })); const sendInvokeResult = vi.fn(async () => {}); await withPathTokenCommand( "openclaw-allowlist-path-pin-", async ({ link: _link, expected }) => { await withTempApprovalsHome( createApprovals("allowlist", "off", "deny", { main: { allowlist: [{ pattern: expected }], }, }), async () => { await runLocalSystemInvokeWithPolicy("allowlist", "off", { command: ["poccmd", "-n", "SAFE"], runCommand, sendInvokeResult, }); }, ); expectCommandPinnedToCanonicalPath( runCommand, expected, ["-n", "SAFE"], fs.realpathSync(process.cwd()), ); expectInvokeOk(sendInvokeResult); }, ); }, ); it.runIf(process.platform !== "win32")( "rejects symlinked cwd paths during approval preparation", async () => { for (const testCase of [ { label: "cwd symlink", setup: () => { const tmp = createFixtureDir("openclaw-approval-cwd-link-"); const safeDir = path.join(tmp, "safe"); const linkDir = path.join(tmp, "cwd-link"); const script = path.join(safeDir, "run.sh"); fs.mkdirSync(safeDir, { recursive: true }); fs.writeFileSync(script, "#!/bin/sh\necho SAFE\n"); fs.chmodSync(script, 0o755); fs.symlinkSync(safeDir, linkDir, "dir"); return { cwd: linkDir, message: "canonical cwd", }; }, }, { label: "parent symlink", setup: () => { const tmp = createFixtureDir("openclaw-approval-cwd-parent-link-"); const safeSymlinkRoot = path.join(tmp, "safe-root"); const safeSymlinkSub = path.join(safeSymlinkRoot, "sub"); const linkRoot = path.join(tmp, "approved-link"); fs.mkdirSync(safeSymlinkSub, { recursive: true }); fs.symlinkSync(safeSymlinkRoot, linkRoot, "dir"); return { cwd: path.join(linkRoot, "sub"), message: "no symlink path components", }; }, }, ]) { const { cwd, message } = testCase.setup(); const prepared = buildSystemRunApprovalPlan({ command: ["./run.sh"], cwd, }); expect(prepared.ok, testCase.label).toBe(false); if (!prepared.ok) { expect(prepared.message, testCase.label).toContain(message); } } }, ); it("uses canonical executable path for approval-based relative command execution", async () => { const tmp = createFixtureDir("openclaw-approval-cwd-real-"); const script = path.join(tmp, "run.sh"); fs.writeFileSync(script, "#!/bin/sh\necho SAFE\n"); fs.chmodSync(script, 0o755); const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy("full", "off", { command: ["./run.sh", "--flag"], cwd: tmp, approved: true, }); if (process.platform === "win32") { expect(runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( sendInvokeResult, "SYSTEM_RUN_DENIED: approval requires a stable executable path", true, ); return; } expectCommandPinnedToCanonicalPath( runCommand, fs.realpathSync(script), ["--flag"], fs.realpathSync(tmp), ); expectInvokeOk(sendInvokeResult); }); it("denies approval-based execution when cwd identity drifts before execution", async () => { const tmp = createFixtureDir("openclaw-approval-cwd-drift-"); const fallback = createFixtureDir("openclaw-approval-cwd-drift-alt-"); const script = path.join(tmp, "run.sh"); fs.writeFileSync(script, "#!/bin/sh\necho SAFE\n"); fs.chmodSync(script, 0o755); const canonicalCwd = fs.realpathSync(tmp); const prepared = buildCwdSessionApprovalPlan(["./run.sh"], tmp, "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withMockedCwdIdentityDrift({ canonicalCwd, driftDir: fallback, run: async () => { const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy( "full", "off", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tmp, approved: true }, ); expect(runCommand).not.toHaveBeenCalled(); if (process.platform === "win32") { expectInvokeErrorMessage( sendInvokeResult, "SYSTEM_RUN_DENIED: approval requires a stable executable path", true, ); return; } expectInvokeErrorMessage( sendInvokeResult, "SYSTEM_RUN_DENIED: approval cwd changed before execution", true, ); }, }); }); it("validates approved script operand bindings at dispatch", async () => { for (const mutate of [true, false]) { const tmp = createFixtureDir( mutate ? "openclaw-approval-script-drift-" : "openclaw-approval-script-stable-", ); const fixture = createMutableScriptOperandFixture(tmp); fs.writeFileSync(fixture.scriptPath, fixture.initialBody); if (process.platform !== "win32") { fs.chmodSync(fixture.scriptPath, 0o755); } const prepared = buildCwdApprovalPlan(fixture.command, tmp); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); if (mutate) { fs.writeFileSync(fixture.scriptPath, fixture.changedBody); } const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy("full", "off", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tmp, approved: true, }); if (mutate) { expect(runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( sendInvokeResult, "SYSTEM_RUN_DENIED: approval script operand changed before execution", true, ); } else { expect(runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(sendInvokeResult); } } }); it.runIf(process.platform !== "win32")( "revalidates approved cwd identity after authorization commit", async () => { const tmp = createFixtureDir("openclaw-approval-cwd-post-commit-drift-"); const moved = `${tmp}-approved`; const script = path.join(tmp, "run.sh"); fs.writeFileSync(script, "#!/bin/sh\necho SAFE\n"); fs.chmodSync(script, 0o755); const commitAuthorization: HandleSystemRunInvokeOptions["commitExecAuthorization"] = async ( params, ) => { const assertCurrent = await commitExecAuthorizationLocked(params); fs.renameSync(tmp, moved); fs.mkdirSync(tmp); fs.writeFileSync(path.join(tmp, "run.sh"), "#!/bin/sh\necho CHANGED\n", { mode: 0o755 }); return assertCurrent; }; const invoke = await runLocalSystemInvokeWithPolicy("full", "off", { command: ["./run.sh"], cwd: tmp, approved: true, commitExecAuthorization: commitAuthorization, }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "SYSTEM_RUN_DENIED: approval cwd changed before execution", true, ); }, ); it.runIf(process.platform !== "win32").each([ { boundary: "commit", revoke: true }, { boundary: "commit", revoke: false }, { boundary: "callback", revoke: true }, { boundary: "callback", revoke: false }, ] as const)( "checks live node policy at $boundary before real execution (revoke=$revoke)", async ({ boundary, revoke }) => { const { testing } = await import("./invoke.test-support.js"); const cwd = createFixtureDir("openclaw-node-policy-before-spawn-"); fs.writeFileSync(path.join(cwd, "approved.txt"), ""); const revokePolicy = () => { if (!revoke) { return; } const current = loadExecApprovals(); current.defaults = { ...current.defaults, security: "deny", ask: "off" }; current.agents = { ...current.agents, main: { security: "deny", ask: "off" } }; saveExecApprovals(current); }; let stdout = ""; const invoke = await runLocalSystemInvokeWithPolicy("full", "off", { command: ["/bin/ls", "approved.txt"], cwd, commitExecAuthorization: async (params) => { const assertCurrent = await commitExecAuthorizationLocked(params); if (boundary === "commit") { revokePolicy(); } return assertCurrent; }, runCommand: async (argv, runCwd, _env, timeoutMs, signal, assertCurrent) => { await Promise.resolve(); if (boundary === "callback") { revokePolicy(); } const result = await testing.runCommand( argv, runCwd, { PATH: "/usr/bin:/bin", HOME: cwd }, timeoutMs, signal, assertCurrent, ); stdout = result.stdout; return result; }, }); expect(stdout).toBe(revoke ? "" : "approved.txt\n"); expect(requireInvokeResult(invoke.sendInvokeResult).ok).toBe(!revoke); expect(invoke.sendExecFinishedEvent.mock.calls.length).toBe(revoke ? 0 : 1); if (revoke) { expect(requireInvokeResult(invoke.sendInvokeResult).error?.code).toBe("SYSTEM_RUN_DENIED"); expectInvokeErrorMessage(invoke.sendInvokeResult, "exec approval changed before execution"); expectExecDeniedEvent(invoke.sendNodeEvent); } }, ); it.runIf(process.platform !== "win32").each([ { approval: "auto", driftAt: "unchanged" }, { approval: "human", driftAt: "commit" }, { approval: "human", driftAt: "unchanged" }, ] as const)( "checks executable identity for $approval approval when resolution is $driftAt", async ({ approval, driftAt }) => { const tmp = createFixtureDir("openclaw-approval-executable-identity-"); const prepared = buildCwdApprovalPlan(["/bin/sh", "-c", "ls *.ts"], tmp); requireApprovalPlan(prepared, "expected a bound shell command plan"); const resolveCommand = commandResolution.resolveCommandResolutionFromArgv; let changed = false; const resolutionSpy = vi .spyOn(commandResolution, "resolveCommandResolutionFromArgv") .mockImplementation((...args) => { const resolution = resolveCommand(...args); if (!changed || args[0][0] !== "ls" || !resolution) { return resolution; } return { ...resolution, execution: { ...resolution.execution, resolvedPath: "/synthetic/changed/ls", resolvedRealPath: "/synthetic/changed/ls", }, }; }); const autoReviewer = vi.fn(() => ({ decision: "allow-once", rationale: "lists fixture files", risk: "low", })); const commitAuthorization: HandleSystemRunInvokeOptions["commitExecAuthorization"] = async ( params, ) => { const assertCurrent = await commitExecAuthorizationLocked(params); changed = driftAt === "commit"; return assertCurrent; }; setRuntimeConfigSnapshot({ tools: { exec: { mode: "auto" } } }); try { const invoke = await runLocalSystemInvoke({ command: prepared.plan.argv, cwd: prepared.plan.cwd ?? tmp, systemRunPlan: prepared.plan, ...(approval === "human" ? { approvalDecision: "allow-once" } : {}), resolveExecSecurity: resolveProductionExecSecurity, resolveExecAsk: resolveProductionExecAsk, autoReviewer, commitExecAuthorization: commitAuthorization, }); expect(autoReviewer).not.toHaveBeenCalled(); if (approval === "auto") { expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "Exec auto-review skipped: dispatch chain cannot be bound", ); } else if (driftAt === "unchanged") { expect(invoke.runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(invoke.sendInvokeResult); } else { expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "SYSTEM_RUN_DENIED: approval script operand changed before execution", true, ); } } finally { resolutionSpy.mockRestore(); } }, ); it("revalidates approved script operands after authorization commit", async () => { const tmp = createFixtureDir("openclaw-approval-script-post-commit-drift-"); const fixture = createMutableScriptOperandFixture(tmp); fs.writeFileSync(fixture.scriptPath, fixture.initialBody); if (process.platform !== "win32") { fs.chmodSync(fixture.scriptPath, 0o755); } const prepared = buildCwdApprovalPlan(fixture.command, tmp); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const commitAuthorization: HandleSystemRunInvokeOptions["commitExecAuthorization"] = async ( params, ) => { const assertCurrent = await commitExecAuthorizationLocked(params); fs.writeFileSync(fixture.scriptPath, fixture.changedBody); return assertCurrent; }; const invoke = await runLocalSystemInvokeWithPolicy("full", "off", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tmp, approved: true, commitExecAuthorization: commitAuthorization, }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "SYSTEM_RUN_DENIED: approval script operand changed before execution", true, ); }); it("validates approved runtime script operand bindings at dispatch", async () => { await withFakeRuntimeOnPath("tsx", async () => { const tmp = createFixtureDir("openclaw-approval-tsx-script-drift-"); const fixture = createRuntimeScriptOperandFixture(tmp, "tsx"); fs.writeFileSync(fixture.scriptPath, fixture.initialBody); const prepared = buildCwdApprovalPlan(fixture.command, tmp); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); fs.writeFileSync(fixture.scriptPath, fixture.changedBody); const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy("full", "off", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tmp, approved: true, }); expect(runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( sendInvokeResult, "SYSTEM_RUN_DENIED: approval script operand changed before execution", true, ); const missingBindingTmp = createFixtureDir("openclaw-approval-tsx-missing-binding-"); const missingBindingFixture = createRuntimeScriptOperandFixture(missingBindingTmp, "tsx"); fs.writeFileSync(missingBindingFixture.scriptPath, missingBindingFixture.initialBody); const missingBindingPrepared = buildCwdApprovalPlan( missingBindingFixture.command, missingBindingTmp, ); expect(missingBindingPrepared.ok).toBe(true); if (!missingBindingPrepared.ok) { throw new Error("unreachable"); } const planWithoutBinding = { ...missingBindingPrepared.plan }; delete planWithoutBinding.mutableFileOperand; const missingBindingRun = await runLocalSystemInvokeWithPolicy("full", "off", { preparedPlan: planWithoutBinding, cwd: missingBindingPrepared.plan.cwd ?? missingBindingTmp, approved: true, }); expect(missingBindingRun.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( missingBindingRun.sendInvokeResult, "SYSTEM_RUN_DENIED: approval missing script operand binding", true, ); }); }); it("denies ./sh wrapper spoof in allowlist on-miss mode before execution", async () => { const marker = path.join(os.tmpdir(), `openclaw-wrapper-spoof-${process.pid}-${Date.now()}`); const runCommand = vi.fn(async () => { fs.writeFileSync(marker, "executed"); return createLocalRunResult(); }); const sendInvokeResult = vi.fn(async () => {}); const sendNodeEvent = vi.fn(async () => {}); await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command: ["./sh", "-lc", "/bin/echo approved-only"], runCommand, sendInvokeResult, sendNodeEvent, }); expect(runCommand).not.toHaveBeenCalled(); expect(fs.existsSync(marker)).toBe(false); expectApprovalRequiredDenied(sendNodeEvent, sendInvokeResult); try { fs.unlinkSync(marker); } catch { // no-op } }); it("denies ./skill-bin even when autoAllowSkills trust entry exists", async () => { const { runCommand, sendInvokeResult, sendNodeEvent } = createInvokeSpies(); await withTempApprovalsHome( createAllowlistOnMissApprovals({ autoAllowSkills: true }), async ({ tempHome }) => { const skillBinPath = path.join(tempHome, "skill-bin"); fs.writeFileSync(skillBinPath, "#!/bin/sh\necho should-not-run\n", { mode: 0o755 }); fs.chmodSync(skillBinPath, 0o755); await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command: ["./skill-bin", "--help"], cwd: tempHome, skillBinsCurrent: async () => [{ name: "skill-bin", resolvedPath: skillBinPath }], runCommand, sendInvokeResult, sendNodeEvent, }); }, ); expect(runCommand).not.toHaveBeenCalled(); expectApprovalRequiredDenied(sendNodeEvent, sendInvokeResult); }); it("rejects unsafe environment inputs before execution", async () => { const shellCommand = process.platform === "win32" ? ["cmd.exe", "/d", "/s", "/c", "echo ok"] : ["/bin/sh", "-lc", "echo ok"]; const cases: Array<{ label: string; command?: string[]; env?: Record; message: string; details: string[]; }> = [ { label: "blocked override", env: { CLASSPATH: "/tmp/evil-classpath" }, message: "SYSTEM_RUN_DENIED: environment override rejected", details: ["CLASSPATH"], }, { label: "blocked override for shell-wrapper", command: shellCommand, env: { CLASSPATH: "/tmp/evil-classpath", LANG: "C", }, message: "SYSTEM_RUN_DENIED: environment override rejected", details: ["CLASSPATH"], }, { label: "blocked argv assignment", command: ["/usr/bin/env", "SHELLOPTS=xtrace", "PS4=$(id)", "bash", "-lc", "echo ok"], message: "SYSTEM_RUN_DENIED: command env assignment rejected", details: ["SHELLOPTS", "PS4"], }, { label: "invalid override key", env: { "BAD-KEY": "x" }, message: "SYSTEM_RUN_DENIED: environment override rejected", details: ["BAD-KEY"], }, ]; for (const testCase of cases) { const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy("full", "off", { command: testCase.command, env: testCase.env, }); expect(runCommand, testCase.label).not.toHaveBeenCalled(); expectInvokeErrorMessage(sendInvokeResult, testCase.message); for (const detail of testCase.details) { expectInvokeErrorMessage(sendInvokeResult, detail); } } }); it.each([ ["echo", "ok"], ["/bin/sh", "./script.sh"], ])("normalizes pager overrides before node execution: %j", async (...command) => { const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy("full", "off", { command, env: { GIT_PAGER: "cat", PAGER: "cat" }, sanitizeEnv: (overrides) => sanitizeHostExecEnv({ baseEnv: {}, overrides }), }); expect(runCommand).toHaveBeenCalledTimes(1); expect(firstMockCallArg(runCommand, "runCommand", 2)).toMatchObject({ GIT_PAGER: "", PAGER: "", }); expectInvokeOk(sendInvokeResult); }); it("applies shell-wrapper env allowlist for shell executable commands without inline payload", async () => { const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy("full", "off", { command: ["/bin/sh", "./script.sh"], env: { OPENCLAW_TEST: "1", LANG: "C", LC_TIME: "C", }, sanitizeEnv: (overrides) => overrides ?? undefined, }); expect(runCommand).toHaveBeenCalledTimes(1); const passedEnv = firstMockCallArg(runCommand, "runCommand", 2); expect(passedEnv).toEqual({ LANG: "C", LC_TIME: "C", }); expectInvokeOk(sendInvokeResult); }); async function expectNestedEnvShellDenied(params: { depth: number; markerName: string; errorLabel: string; }) { const { runCommand, sendInvokeResult, sendNodeEvent } = createInvokeSpies({ runCommand: vi.fn(async () => { throw new Error(params.errorLabel); }), }); await withTempApprovalsHome( createAllowlistOnMissApprovals({ agents: { main: { allowlist: [{ pattern: "/usr/bin/env" }], }, }, }), async ({ tempHome }) => { const marker = path.join(tempHome, params.markerName); await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command: buildNestedEnvShellCommand({ depth: params.depth, payload: `echo PWNED > ${marker}`, }), runCommand, sendInvokeResult, sendNodeEvent, }); expect(fs.existsSync(marker)).toBe(false); }, ); expect(runCommand).not.toHaveBeenCalled(); expectApprovalRequiredDenied(sendNodeEvent, sendInvokeResult); } it("denies env-wrapped shell payloads at and past the dispatch depth boundary", async () => { if (process.platform === "win32") { return; } for (const testCase of [ { depth: 4, markerName: "depth4-pwned.txt", errorLabel: "runCommand should not be called for depth-boundary shell wrappers", }, { depth: 5, markerName: "pwned.txt", errorLabel: "runCommand should not be called for nested env depth overflow", }, ]) { await expectNestedEnvShellDenied(testCase); } }); it("requires explicit approval for strict inline-eval carriers", async () => { // The full carrier matrix lives in command-analysis tests; this is the // handle-level smoke for strictInlineEval denial wiring. const cases = [ { command: ["python3", "-c", "print('hi')"], expected: "python3 -c requires explicit approval in strictInlineEval mode", }, { command: ["python3.13", "-c", "print('hi')"], expected: "python3.13 -c requires explicit approval in strictInlineEval mode", }, ] as const; setRuntimeConfigSnapshot({ tools: { exec: { strictInlineEval: true, }, }, }); try { for (const testCase of cases) { const { runCommand, sendInvokeResult, sendNodeEvent } = await runLocalSystemInvokeWithPolicy("full", "off", { command: [...testCase.command] }); expect(runCommand, testCase.command.join(" ")).not.toHaveBeenCalled(); expectExecDeniedEvent(sendNodeEvent); expectInvokeErrorMessage(sendInvokeResult, testCase.expected); } } finally { clearRuntimeConfigSnapshot(); } }); it("prefers strict inline-eval denial over generic allowlist prompts", async () => { setRuntimeConfigSnapshot({ tools: { exec: { strictInlineEval: true, }, }, }); try { const { runCommand, sendInvokeResult, sendNodeEvent } = await runLocalSystemInvokeWithPolicy( "allowlist", "on-miss", { command: ["awk", 'BEGIN{system("id")}', "/dev/null"] }, ); expect(runCommand).not.toHaveBeenCalled(); expectExecDeniedEvent(sendNodeEvent); expectInvokeErrorMessage( sendInvokeResult, "awk inline program requires explicit approval in strictInlineEval mode", ); } finally { clearRuntimeConfigSnapshot(); } }); it("fails closed when allow-always approval persistence fails", async () => { await withTempApprovalsHome(createAllowlistOnMissApprovals(), async () => { const tempDir = createFixtureDir("openclaw-allow-always-write-failure-"); const executablePath = createTempExecutable(tempDir, "approved-tool"); const commitAuthorization = vi.fn(async () => { throw new Error("approval lock unavailable"); }); const invoke = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command: [executablePath], approvalDecision: "allow-always", approved: true, commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ allowAlwaysDecision: expect.any(Object) }), ); expect(invoke.runCommand).not.toHaveBeenCalled(); expect(invoke.sendExecFinishedEvent).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }); }); it("does not restore a revoked allowlist rule during explicit allow-always persistence", async () => { const tempDir = createFixtureDir("openclaw-allow-always-revoked-rule-"); const executablePath = createTempExecutable(tempDir, "approved-tool"); const matchedEntry = { pattern: fs.realpathSync(executablePath) }; const expectedPolicySnapshot = { security: "allowlist" as const, ask: "always" as const, askFallback: "deny" as const, autoAllowSkills: false, allowlistRules: [matchedEntry], }; await withTempApprovalsHome( createApprovals("allowlist", "always", "deny", { main: { allowlist: [matchedEntry] } }), async () => { let capturedAuthorization: | Parameters[0]["authorization"] | undefined; const commitAuthorization = vi.fn( async (params: Parameters[0]) => { capturedAuthorization = params.authorization; const current = loadExecApprovals(); const main = current.agents?.main; saveExecApprovals({ ...current, agents: { ...current.agents, main: { ...main, allowlist: [] }, }, }); return await commitExecAuthorizationLocked(params); }, ); const invoke = await runLocalSystemInvokeWithPolicy("allowlist", "always", { command: [executablePath], approvalDecision: "allow-always", approved: true, commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledTimes(1); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ allowAlwaysDecision: expect.objectContaining({ kind: "patterns" }), }), ); expect(capturedAuthorization).toEqual({ source: "explicit-approval", security: "allowlist", ask: "always", allowlistSatisfied: true, policySnapshot: expectedPolicySnapshot, requireAutoAllowSkills: false, requireExactCommandApproval: false, requireDurableAllowlistApproval: false, }); expect(invoke.runCommand).not.toHaveBeenCalled(); expect(invoke.sendExecFinishedEvent).not.toHaveBeenCalled(); expect(loadExecApprovals().agents?.main?.allowlist ?? []).toStrictEqual([]); expectApprovalStateWriteDenied(invoke); }, ); }); it("fails closed when allowlist usage persistence fails", async () => { const tempDir = createFixtureDir("openclaw-allowlist-usage-write-failure-"); const executablePath = createTempExecutable(tempDir, "allowlisted-tool"); await withTempApprovalsHome( createAllowlistOnMissApprovals({ agents: { main: { allowlist: [{ pattern: fs.realpathSync(executablePath) }], }, }, }), async () => { const commitAuthorization = vi.fn(async () => { throw new Error("approval lock unavailable"); }); const invoke = await runLocalSystemInvokeWithPolicy("allowlist", "off", { command: [executablePath], commitExecAuthorization: commitAuthorization, }); expect(invoke.runCommand).not.toHaveBeenCalled(); expect(invoke.sendExecFinishedEvent).not.toHaveBeenCalled(); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ security: "allowlist", ask: "on-miss", }), }), ); expectApprovalStateWriteDenied(invoke); }, ); }); it("revalidates unprompted full policy before local execution", async () => { await withTempApprovalsHome(createApprovals("full", "off", "deny"), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.defaults = { ...current.defaults, security: "deny" }; }); const invoke = await runLocalSystemInvokeWithPolicy("full", "off", { commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "current-policy" }), }), ); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }); }); it("rejects unprompted full execution after ask policy tightens", async () => { await withTempApprovalsHome(createApprovals("full", "off", "deny"), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.defaults = { ...current.defaults, ask: "on-miss" }; }); const invoke = await runLocalSystemInvokeWithPolicy("full", "off", { commitExecAuthorization: commitAuthorization, }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }); }); it("revalidates explicit approval against a current deny policy", async () => { await withTempApprovalsHome(createApprovals("full", "always", "deny"), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.defaults = { ...current.defaults, security: "deny" }; }); const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { approvalDecision: "allow-once", approved: true, commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "explicit-approval" }), }), ); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }); }); it("rejects explicit allow-once when persisted security tightens to allowlist", async () => { await withTempApprovalsHome(createApprovals("full", "always", "deny"), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.defaults = { ...current.defaults, security: "allowlist" }; }); const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { approvalDecision: "allow-once", approved: true, commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "explicit-approval", policySnapshot: expect.any(Object), }), }), ); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }); }); it("treats authenticated auto-review provenance as marker-only one-shot authority", async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "on-miss", "deny"), async () => { const autoReviewer = vi.fn(() => ({ decision: "ask", rationale: "must not be called for forwarded provenance", risk: "medium", })); const commitAuthorization = vi.fn(commitExecAuthorizationLocked); const invoke = await runLocalSystemInvokeWithPolicy("full", "on-miss", { preparedPlan: prepared.plan, approvalSource: "auto-review", autoReviewer, commitExecAuthorization: commitAuthorization, }); expect(autoReviewer).not.toHaveBeenCalled(); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "auto-review" }), }), ); expect(invoke.runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(invoke.sendInvokeResult); }); }); it("rejects forwarded auto-review when current ask policy tightens to always", async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "on-miss", "deny"), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.defaults = { ...current.defaults, ask: "always" }; }); const invoke = await runLocalSystemInvokeWithPolicy("full", "on-miss", { preparedPlan: prepared.plan, approvalSource: "auto-review", commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "auto-review" }), }), ); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }); }); it("rejects forwarded auto-review when persisted security tightens to allowlist", async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "on-miss", "deny"), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.defaults = { ...current.defaults, security: "allowlist" }; }); const invoke = await runLocalSystemInvokeWithPolicy("full", "on-miss", { preparedPlan: prepared.plan, approvalSource: "auto-review", commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "auto-review", policySnapshot: expect.any(Object), }), }), ); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }); }); it("rejects forwarded auto-review when persisted ask tightens from off to on-miss", async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "off", "deny"), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.defaults = { ...current.defaults, ask: "on-miss" }; }); const invoke = await runLocalSystemInvokeWithPolicy("full", "off", { preparedPlan: prepared.plan, approvalSource: "auto-review", commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "auto-review", policySnapshot: expect.any(Object), }), }), ); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }); }); it("rejects forwarded auto-review when current security policy tightens to deny", async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "on-miss", "deny"), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.defaults = { ...current.defaults, security: "deny" }; }); const invoke = await runLocalSystemInvokeWithPolicy("full", "on-miss", { preparedPlan: prepared.plan, approvalSource: "auto-review", commitExecAuthorization: commitAuthorization, }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }); }); it("does not let forwarded auto-review authorize security audit suppression edits", async () => { const tmp = createFixtureDir("openclaw-forwarded-auto-review-suppression-"); const executablePath = createTempExecutable(tmp, "openclaw"); const prepared = buildCwdSessionApprovalPlan( [executablePath, "config", "set", "security.audit.suppressions", "[]"], tmp, "agent:main:main", ); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "on-miss", "deny"), async () => { const invoke = await runLocalSystemInvokeWithPolicy("full", "on-miss", { preparedPlan: prepared.plan, cwd: tmp, approvalSource: "auto-review", }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectExecDeniedEvent(invoke.sendNodeEvent); expectInvokeErrorMessage( invoke.sendInvokeResult, "SYSTEM_RUN_DENIED: explicit approval required", true, ); }); }); it("preserves exact-plan forwarded auto-review for strict inline eval", async () => { const plan = createStrictInlineEvalApprovalPlan("openclaw-forwarded-inline-"); setRuntimeConfigSnapshot({ tools: { exec: { strictInlineEval: true } } }); try { await withTempApprovalsHome(createApprovals("full", "on-miss", "deny"), async () => { const commitAuthorization = vi.fn(commitExecAuthorizationLocked); const invoke = await runLocalSystemInvokeWithPolicy("full", "on-miss", { preparedPlan: plan, approvalSource: "auto-review", commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "auto-review" }), }), ); expect(invoke.runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(invoke.sendInvokeResult); }); } finally { clearRuntimeConfigSnapshot(); } }); it("does not commit allow-always state when local screen recording is unavailable", async () => { await withTempApprovalsHome(createApprovals("full", "always", "deny"), async () => { const commitAuthorization = vi.fn(commitExecAuthorizationLocked); const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { approvalDecision: "allow-always", approved: true, needsScreenRecording: true, commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).not.toHaveBeenCalled(); expect(invoke.runCommand).not.toHaveBeenCalled(); expect(loadExecApprovals().agents?.main?.allowlist ?? []).toStrictEqual([]); expect(invoke.sendNodeEvent).toHaveBeenCalledWith( expect.anything(), "exec.denied", expect.objectContaining({ reason: "permission:screenRecording" }), ); }); }); it("revalidates timeout fallback against the current askFallback policy", async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "always", "full", {}), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.defaults = { ...current.defaults, askFallback: "deny" }; }); const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? undefined, approvalSource: "ask-fallback", commitExecAuthorization: commitAuthorization, }); expect(invoke.runCommand).not.toHaveBeenCalled(); expect(invoke.sendExecFinishedEvent).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }); }); it("requires a canonical plan for timeout fallback provenance", async () => { const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { approvalSource: "ask-fallback", }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "approvalSource requires matching systemRunPlan", true, ); }); it("requires a canonical plan for forwarded auto-review provenance", async () => { const invoke = await runLocalSystemInvokeWithPolicy("full", "on-miss", { approvalSource: "auto-review", prepareDelayedApprovalPlan: false, }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "approvalSource requires matching systemRunPlan", true, ); }); it("requires a canonical plan for explicit approval provenance", async () => { const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { approvalDecision: "allow-once", approved: true, prepareDelayedApprovalPlan: false, }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "explicit approval requires matching systemRunPlan", true, ); }); it("requires a prepared policy snapshot for forwarded delayed approval", async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const invoke = await runLocalSystemInvokeWithPolicy("full", "on-miss", { preparedPlan: prepared.plan, approvalSource: "auto-review", prepareDelayedApprovalPlan: false, }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "delayed approval requires a prepared policy snapshot", true, ); }); it("rejects explicit approval when policy tightens after prepare", async () => { await withTempApprovalsHome(createApprovals("full", "always", "deny"), async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const policyBoundPlan = bindCurrentPolicyToPlan(prepared.plan); const current = loadExecApprovals(); current.defaults = { ...current.defaults, security: "allowlist" }; saveExecApprovals(current); const commitAuthorization = vi.fn(commitExecAuthorizationLocked); const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { preparedPlan: policyBoundPlan, approvalDecision: "allow-once", approved: true, prepareDelayedApprovalPlan: false, commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).not.toHaveBeenCalled(); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "exec approval policy changed; request approval again", ); }); }); it("rejects forwarded auto-review when ask tightens after prepare", async () => { await withTempApprovalsHome(createApprovals("full", "off", "deny"), async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const policyBoundPlan = bindCurrentPolicyToPlan(prepared.plan); const current = loadExecApprovals(); current.defaults = { ...current.defaults, ask: "on-miss" }; saveExecApprovals(current); const commitAuthorization = vi.fn(commitExecAuthorizationLocked); const invoke = await runLocalSystemInvokeWithPolicy("full", "off", { preparedPlan: policyBoundPlan, approvalSource: "auto-review", prepareDelayedApprovalPlan: false, commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).not.toHaveBeenCalled(); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "exec approval policy changed; request approval again", ); }); }); it("rejects explicit approval when an allowlist rule is revoked after prepare", async () => { await withTempApprovalsHome( createApprovals("allowlist", "always", "deny", { main: { allowlist: [{ id: "rule-1", pattern: "/usr/bin/echo" }], }, }), async () => { const prepared = buildSystemRunApprovalPlan({ command: ["echo", "ok"], agentId: "main", sessionKey: "agent:main:main", }); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const policyBoundPlan = bindCurrentPolicyToPlan(prepared.plan); const current = loadExecApprovals(); current.agents = { ...current.agents, main: { allowlist: [] } }; saveExecApprovals(current); const commitAuthorization = vi.fn(commitExecAuthorizationLocked); const invoke = await runLocalSystemInvokeWithPolicy("allowlist", "always", { preparedPlan: policyBoundPlan, agentId: "main", approvalDecision: "allow-once", approved: true, prepareDelayedApprovalPlan: false, commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).not.toHaveBeenCalled(); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "exec approval policy changed; request approval again", ); }, ); }); it("rejects timeout fallback provenance mixed with explicit approval", async () => { const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { approvalDecision: "allow-once", approvalSource: "ask-fallback", }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "approvalSource cannot be combined with explicit approval", true, ); }); it("rejects forwarded auto-review provenance mixed with explicit approval", async () => { const invoke = await runLocalSystemInvokeWithPolicy("full", "on-miss", { approved: true, approvalDecision: "allow-once", approvalSource: "auto-review", }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "approvalSource cannot be combined with explicit approval", true, ); }); it("applies marker-only full timeout fallback without another prompt", async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "always", "full", {}), async () => { const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { preparedPlan: prepared.plan, approvalSource: "ask-fallback", }); expect(invoke.runCommand).toHaveBeenCalledWith( prepared.plan.argv, prepared.plan.cwd, undefined, undefined, undefined, expect.any(Function), ); expectInvokeOk(invoke.sendInvokeResult); }); }); it.runIf(process.platform !== "win32")( "permits a durable exact-command approval under allowlist timeout fallback", async () => { const tempDir = createFixtureDir("openclaw-fallback-durable-"); const prepared = buildCwdSessionApprovalPlan( ["/bin/sh", "-c", "/bin/ls"], tempDir, "agent:main:main", ); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const commandPattern = createExactCommandPattern(prepared.plan.commandText); await withTempApprovalsHome( createApprovals("full", "always", "allowlist", { main: { allowlist: [{ pattern: commandPattern, source: "allow-always" }] }, }), async () => { const commitAuthorization = vi.fn(commitExecAuthorizationLocked); const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tempDir, approvalSource: "ask-fallback", commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "ask-fallback", requireExactCommandApproval: true, }), }), ); expect(invoke.runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(invoke.sendInvokeResult); }, ); }, ); it.runIf(process.platform !== "win32")( "rejects allowlist timeout fallback when its durable source is removed before commit", async () => { const tempDir = createFixtureDir("openclaw-fallback-durable-revoked-"); const prepared = buildCwdSessionApprovalPlan( ["/bin/sh", "-c", "/bin/ls"], tempDir, "agent:main:main", ); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const commandPattern = createExactCommandPattern(prepared.plan.commandText); await withTempApprovalsHome( createApprovals("full", "always", "allowlist", { main: { allowlist: [{ pattern: commandPattern, source: "allow-always" }] }, }), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.agents = { ...current.agents, main: { allowlist: [{ pattern: commandPattern }] }, }; }); const invoke = await runLocalSystemInvokeWithPolicy("full", "always", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tempDir, approvalSource: "ask-fallback", commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "ask-fallback", requireExactCommandApproval: true, }), }), ); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }, ); }, ); it("preserves source-only fallback across the authenticated Mac app bridge", async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "always", "full", {}), async () => { const invoke = await runMacSystemInvokeWithPolicy("full", "always", { runViaResponse: createMacExecHostSuccess(), preparedPlan: prepared.plan, approvalSource: "ask-fallback", }); const call = requireMacExecHostCall(invoke.runViaMacAppExecHost); expect(call.request?.approvalSource).toBe("ask-fallback"); expect(call.request?.approvalDecision).toBeNull(); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeOk(invoke.sendInvokeResult, "app-ok"); }); }); it("preserves marker-only auto-review across the authenticated Mac app bridge", async () => { const prepared = buildSessionApprovalPlan(["echo", "ok"], "agent:main:main"); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "on-miss", "deny"), async () => { const invoke = await runMacSystemInvokeWithPolicy("full", "on-miss", { runViaResponse: createMacExecHostSuccess(), preparedPlan: prepared.plan, approvalSource: "auto-review", }); const call = requireMacExecHostCall(invoke.runViaMacAppExecHost); expect(call.request?.approvalSource).toBe("auto-review"); expect(call.request?.approvalDecision).toBeNull(); expect(call.request?.policySnapshot).toEqual( createExecApprovalPolicySnapshot({ file: loadExecApprovals(), agentId: undefined }), ); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeOk(invoke.sendInvokeResult, "app-ok"); }); }); it("does not let timeout fallback satisfy strict inline review", async () => { const plan = createStrictInlineEvalApprovalPlan("openclaw-fallback-inline-"); setRuntimeConfigSnapshot({ tools: { exec: { strictInlineEval: true } } }); try { await withTempApprovalsHome(createApprovals("full", "always", "full", {}), async () => { const invoke = await runLocalSystemInvoke({ preparedPlan: plan, approvalSource: "ask-fallback", }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( invoke.sendInvokeResult, "requires explicit approval in strictInlineEval mode", ); }); } finally { clearRuntimeConfigSnapshot(); } }); it("does not let timeout fallback authorize security audit suppression edits", async () => { const tmp = createFixtureDir("openclaw-timeout-fallback-suppression-"); const executablePath = createTempExecutable(tmp, "openclaw"); const prepared = buildCwdSessionApprovalPlan( [executablePath, "config", "set", "security.audit.suppressions", "[]"], tmp, "agent:main:main", ); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome(createApprovals("full", "always", "full", {}), async () => { const invoke = await runLocalSystemInvoke({ preparedPlan: prepared.plan, cwd: tmp, approvalSource: "ask-fallback", }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalRequiredDenied(invoke.sendNodeEvent, invoke.sendInvokeResult); }); }); it("keeps audit suppression edits approval-gated under allowlist fallback from full/off", async () => { const tmp = createFixtureDir("openclaw-timeout-fallback-full-off-suppression-"); const executablePath = createTempExecutable(tmp, "openclaw"); const prepared = buildCwdSessionApprovalPlan( [executablePath, "config", "set", "security.audit.suppressions", "[]"], tmp, "agent:main:main", ); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome( createApprovals("full", "off", "allowlist", { main: { allowlist: [{ pattern: fs.realpathSync(executablePath) }] }, }), async () => { const invoke = await runLocalSystemInvokeWithPolicy("full", "off", { preparedPlan: prepared.plan, cwd: tmp, approvalSource: "ask-fallback", }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectApprovalRequiredDenied(invoke.sendNodeEvent, invoke.sendInvokeResult); }, ); }); it("rejects unknown approval provenance", async () => { const invoke = await runLocalSystemInvokeWithPolicy("full", "off", { approved: true, approvalDecision: "allow-once", approvalSource: "explicit", }); expect(invoke.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage(invoke.sendInvokeResult, "approvalSource invalid", true); }); it("rejects unbindable strict inline-eval carriers before delayed approval", async () => { setRuntimeConfigSnapshot({ tools: { exec: { strictInlineEval: true, }, }, }); try { await withTempApprovalsHome(createAllowlistOnMissApprovals(), async () => { const tempDir = createFixtureDir("openclaw-inline-eval-bin-"); const executablePath = createTempExecutable(tempDir, "python3.13"); const prepared = buildSystemRunApprovalPlan({ command: [executablePath, "-c", "print('hi')"], }); expect(prepared).toEqual({ ok: false, message: "SYSTEM_RUN_DENIED: approval cannot safely bind this interpreter/runtime command", }); expect(loadExecApprovals().agents?.main?.allowlist ?? []).toStrictEqual([]); }); } finally { clearRuntimeConfigSnapshot(); } }); it("persists benign awk allow-always approvals in strict inline-eval mode without reopening inline carriers", async () => { setRuntimeConfigSnapshot({ tools: { exec: { strictInlineEval: true, }, }, }); try { await withTempApprovalsHome(createAllowlistOnMissApprovals(), async () => { const tempDir = createFixtureDir("openclaw-inline-eval-awk-"); const executablePath = createTempExecutable(tempDir, "gawk"); fs.writeFileSync(path.join(tempDir, "script.awk"), "{ print }\n"); const benign = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command: [executablePath, "-F", ",", "-f", "script.awk"], cwd: tempDir, approvalDecision: "allow-always", approved: true, runCommand: vi.fn(async () => createLocalRunResult("awk-ok")), }); expect(benign.runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(benign.sendInvokeResult, "awk-ok"); const allowlist = loadExecApprovals().agents?.main?.allowlist ?? []; expect(allowlist).toHaveLength(2); expect(allowlist[0]?.pattern).toBe(fs.realpathSync(executablePath)); expect(allowlist[0]?.lastUsedCommand).toBeUndefined(); expect(allowlist[1]?.pattern).toMatch(/^=node-command:[0-9a-f]{16}$/); expect(allowlist[1]?.lastUsedCommand).toBeUndefined(); const malicious = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command: [executablePath, 'BEGIN{system("id")}', "/dev/null"], cwd: tempDir, }); expect(malicious.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( malicious.sendInvokeResult, "awk inline program requires explicit approval in strictInlineEval mode", ); const abbreviated = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command: [executablePath, '--s=BEGIN{system("id")}', "/dev/null"], cwd: tempDir, }); expect(abbreviated.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( abbreviated.sendInvokeResult, "gawk --source requires explicit approval in strictInlineEval mode", ); }); } finally { clearRuntimeConfigSnapshot(); } }); it("does not persist allow-always approvals for strict inline-eval make carriers", async () => { setRuntimeConfigSnapshot({ tools: { exec: { strictInlineEval: true, }, }, }); try { await withTempApprovalsHome(createAllowlistOnMissApprovals(), async () => { const tempDir = createFixtureDir("openclaw-inline-eval-make-"); const executablePath = createTempExecutable(tempDir, "make"); const makefilePath = path.join(tempDir, "Makefile"); fs.writeFileSync(makefilePath, "all:\n\t@echo inline-eval-ok\n"); const prepared = buildCwdApprovalPlan([executablePath, "-f", makefilePath], tempDir); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const { runCommand, sendInvokeResult } = await runLocalSystemInvokeWithPolicy( "allowlist", "on-miss", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tempDir, approvalDecision: "allow-always", approved: true, runCommand: vi.fn(async () => createLocalRunResult("inline-eval-ok")), }, ); expect(runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(sendInvokeResult, "inline-eval-ok"); expect(loadExecApprovals().agents?.main?.allowlist ?? []).toStrictEqual([]); }); } finally { clearRuntimeConfigSnapshot(); } }); it.runIf(process.platform !== "win32")( "auto-runs allowlisted inner scripts through transport shell wrappers", async () => { const tempDir = createFixtureDir("openclaw-shell-wrapper-inner-"); const scriptsDir = path.join(tempDir, "scripts"); fs.mkdirSync(scriptsDir, { recursive: true }); const scriptPath = path.join(scriptsDir, "check_mail.sh"); fs.writeFileSync(scriptPath, "#!/bin/sh\necho ok\n"); fs.chmodSync(scriptPath, 0o755); await withTempApprovalsHome( createAllowlistOnMissApprovals({ agents: { main: { allowlist: [{ pattern: fs.realpathSync(scriptPath) }], }, }, }), async () => { const invoke = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command: ["/bin/sh", "-lc", "./scripts/check_mail.sh --limit 5"], rawCommand: '/bin/sh -lc "./scripts/check_mail.sh --limit 5"', cwd: tempDir, runCommand: vi.fn(async () => createLocalRunResult("shell-wrapper-inner-ok")), }); expect(invoke.runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(invoke.sendInvokeResult, "shell-wrapper-inner-ok"); }, ); }, ); it("keeps cmd.exe transport wrappers approval-gated on Windows", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); try { for (const testCase of [ { name: "env-assignment cmd.exe", commandPrefix: ["env", "FOO=bar", "cmd.exe", "/d", "/s", "/c"], }, ]) { const tempDir = createFixtureDir("openclaw-cmd-wrapper-allow-"); const scriptPath = path.join(tempDir, "check_mail.cmd"); fs.writeFileSync(scriptPath, "@echo off\r\necho ok\r\n"); const command = [...testCase.commandPrefix, `${scriptPath} --limit 5`]; await withTempApprovalsHome( createAllowlistOnMissApprovals({ agents: { main: { allowlist: [{ pattern: scriptPath }], }, }, }), async () => { const seenArgv: string[][] = []; const invoke = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { command, cwd: tempDir, isCmdExeInvocation: (argv) => { seenArgv.push([...argv]); const token = argv[0]?.trim(); if (!token) { return false; } const base = path.win32.basename(token).toLowerCase(); return base === "cmd.exe" || base === "cmd"; }, }); expect(seenArgv, testCase.name).toEqual([ ["cmd.exe", "/d", "/s", "/c", `${scriptPath} --limit 5`], ]); expect(invoke.runCommand, testCase.name).not.toHaveBeenCalled(); expectApprovalRequiredDenied(invoke.sendNodeEvent, invoke.sendInvokeResult); }, ); } } finally { platformSpy.mockRestore(); } }); it("fails closed when cmd.exe wrapper trust is downgraded before execution", async () => { const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); try { const tempDir = createFixtureDir("openclaw-cmd-wrapper-downgraded-"); const commandName = "check_mail.cmd"; const command = ["env", "FOO=bar", "cmd.exe", "/d", "/s", "/c", `${commandName} --limit 5`]; const ordinaryPattern = "*"; const prepared = buildSystemRunApprovalPlan({ command, cwd: tempDir }); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const commandPattern = createExactCommandPattern(prepared.plan.commandText); await withTempApprovalsHome( createAllowlistOnMissApprovals({ agents: { main: { allowlist: [ { pattern: ordinaryPattern }, { pattern: commandPattern, source: "allow-always" }, ], }, }, }), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.agents = { ...current.agents, main: { allowlist: [{ pattern: ordinaryPattern }, { pattern: commandPattern }], }, }; }); const invoke = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tempDir, isCmdExeInvocation: (argv) => { const token = argv[0]?.trim(); if (!token) { return false; } const base = path.win32.basename(token).toLowerCase(); return base === "cmd.exe" || base === "cmd"; }, commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "current-policy", requireExactCommandApproval: true, }), }), ); expect(invoke.runCommand).not.toHaveBeenCalled(); expect(invoke.sendExecFinishedEvent).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(invoke); }, ); } finally { platformSpy.mockRestore(); } }); it("reuses exact-command durable trust for shell-wrapper reruns", async () => { if (process.platform === "win32") { return; } const tempDir = createFixtureDir("openclaw-shell-wrapper-allow-"); const prepared = buildCwdApprovalPlan(["/bin/sh", "-c", "/bin/ls"], tempDir); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); await withTempApprovalsHome( createApprovals("allowlist", "on-miss", "full", { main: { allowlist: [ { pattern: `=command:${crypto .createHash("sha256") .update(prepared.plan.commandText) .digest("hex") .slice(0, 16)}`, source: "allow-always", }, ], }, }), async () => { const rerun = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tempDir, runCommand: vi.fn(async () => createLocalRunResult("shell-wrapper-reused")), }); expect(rerun.runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(rerun.sendInvokeResult, "shell-wrapper-reused"); }, ); }); it.runIf(process.platform !== "win32")( "rejects durable trust when its approved directory is replaced before execution", async () => { const tempDir = createFixtureDir("openclaw-durable-cwd-drift-"); const movedDir = `${tempDir}-moved`; const prepared = buildCwdApprovalPlan(["/bin/sh", "-c", "/bin/ls"], tempDir); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const commandPattern = createExactCommandPattern(prepared.plan.commandText); await withTempApprovalsHome( createApprovals("allowlist", "on-miss", "full", { main: { allowlist: [{ pattern: commandPattern, source: "allow-always" }], }, }), async () => { const commitAuthorization: HandleSystemRunInvokeOptions["commitExecAuthorization"] = async (params) => { const assertCurrent = await commitExecAuthorizationLocked(params); fs.renameSync(tempDir, movedDir); fs.mkdirSync(tempDir); return assertCurrent; }; const rerun = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tempDir, commitExecAuthorization: commitAuthorization, }); expect(rerun.runCommand).not.toHaveBeenCalled(); expectInvokeErrorMessage( rerun.sendInvokeResult, "SYSTEM_RUN_DENIED: approval cwd changed before execution", true, ); }, ); }, ); it("does not bind safe builtin policy to a redundant exact-command grant", async () => { if (process.platform === "win32") { return; } const tempDir = createFixtureDir("openclaw-shell-wrapper-redundant-grant-"); const prepared = buildCwdApprovalPlan(["/bin/sh", "-c", "cd ."], tempDir); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const commandPattern = createExactCommandPattern(prepared.plan.commandText); await withTempApprovalsHome( createApprovals("allowlist", "on-miss", "full", { main: { allowlist: [{ pattern: commandPattern, source: "allow-always" }], }, }), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.agents = { ...current.agents, main: { allowlist: [] } }; }); const rerun = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tempDir, commitExecAuthorization: commitAuthorization, runCommand: vi.fn(async () => createLocalRunResult("safe-builtin-ok")), }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "current-policy", requireExactCommandApproval: false, requireDurableAllowlistApproval: false, }), }), ); expect(rerun.runCommand).toHaveBeenCalledTimes(1); expectInvokeOk(rerun.sendInvokeResult, "safe-builtin-ok"); }, ); }); it("fails closed when an exact-command grant is revoked before execution", async () => { if (process.platform === "win32") { return; } const tempDir = createFixtureDir("openclaw-shell-wrapper-revoked-"); const prepared = buildCwdApprovalPlan(["/bin/sh", "-c", "/bin/ls"], tempDir); expect(prepared.ok).toBe(true); requireApprovalPlan(prepared, "unreachable"); const commandPattern = createExactCommandPattern(prepared.plan.commandText); await withTempApprovalsHome( createApprovals("allowlist", "on-miss", "full", { main: { allowlist: [{ pattern: commandPattern, source: "allow-always" }], }, }), async () => { const commitAuthorization = createPolicyMutationCommit((current) => { current.agents = { ...current.agents, main: { allowlist: [] } }; }); const rerun = await runLocalSystemInvokeWithPolicy("allowlist", "on-miss", { preparedPlan: prepared.plan, cwd: prepared.plan.cwd ?? tempDir, commitExecAuthorization: commitAuthorization, }); expect(commitAuthorization).toHaveBeenCalledWith( expect.objectContaining({ authorization: expect.objectContaining({ source: "current-policy", requireExactCommandApproval: true, }), }), ); expect(rerun.runCommand).not.toHaveBeenCalled(); expect(rerun.sendExecFinishedEvent).not.toHaveBeenCalled(); expectApprovalStateWriteDenied(rerun); }, ); }); }); /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */