diff --git a/CHANGELOG.md b/CHANGELOG.md index d8ad851898f..c8ec9f4cbac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Docs: https://docs.openclaw.ai - Cron/failover: classify structured OpenAI-compatible `server_error` payloads as `server_error`, expose that reason in cron state, and let one-shot cron retry policy honor `retryOn: ["server_error"]` without requiring raw `5xx` text. (#45594) Thanks @clovericbot. - Slack: wake the resolved thread session after interactive reply button/select clicks and carry Slack delivery context through the queued interaction event, so clicks continue the visible conversation. Fixes #79676 and #61502. (#79836) Thanks @velvet-shark, @tianxiaochannel-oss88, and @Saicheg. - WhatsApp/streaming: send only the new suffix when text-end block replies repeat prior preambles across tool-call cycles, preventing cumulative WhatsApp preamble messages. Fixes #78946. (#79120) Thanks @brokemac79 and @papawattu. +- Tests/security audit: sandbox `audit-exec-surface.test.ts` under a per-case OpenClaw home tempdir, redirecting `OPENCLAW_HOME` (which wins over `HOME`/`USERPROFILE` in `resolveRawHomeDir`) alongside `HOME` and `USERPROFILE`, so its `saveExecApprovals(...)` calls never touch the live `~/.openclaw/exec-approvals.json` on the host running the suite. Sibling exec-approvals tests already used the tempdir pattern; this file did not, so running `pnpm test` against a contributor's local checkout was silently truncating their real approvals to `{ "version": 1, "agents": {} }`. (#79885) Thanks @omarshahine. ## 2026.5.9 diff --git a/src/security/audit-exec-surface.test.ts b/src/security/audit-exec-surface.test.ts index a47ac7a95a2..48946b04ce9 100644 --- a/src/security/audit-exec-surface.test.ts +++ b/src/security/audit-exec-surface.test.ts @@ -1,4 +1,7 @@ -import { afterEach, describe, expect, it } from "vitest"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { saveExecApprovals } from "../infra/exec-approvals.js"; import { collectExecRuntimeFindings } from "./audit.js"; @@ -27,11 +30,61 @@ function requireFinding( return finding; } -afterEach(() => { - saveExecApprovals({ version: 1, agents: {} }); -}); - describe("security audit exec surface findings", () => { + // Redirect the OpenClaw home (OPENCLAW_HOME wins over HOME/USERPROFILE in + // `resolveRawHomeDir`) to a per-test tempdir so `saveExecApprovals` never + // touches the real `~/.openclaw/exec-approvals.json` on the host running + // the suite. + let previousOpenClawHome: string | undefined; + let previousHome: string | undefined; + let previousUserProfile: string | undefined; + let tempRoot = ""; + let tempCaseIndex = 0; + + beforeAll(async () => { + tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-exec-approvals-")); + }); + + beforeEach(async () => { + previousOpenClawHome = process.env.OPENCLAW_HOME; + previousHome = process.env.HOME; + previousUserProfile = process.env.USERPROFILE; + const tempDir = path.join(tempRoot, `case-${++tempCaseIndex}`); + await fs.mkdir(path.join(tempDir, ".openclaw"), { recursive: true }); + // OPENCLAW_HOME takes precedence over HOME/USERPROFILE in resolveRawHomeDir, + // so all three must point at the tempdir to neutralize whichever the host + // happens to have set. + process.env.OPENCLAW_HOME = tempDir; + process.env.HOME = tempDir; + // Windows uses USERPROFILE for os.homedir() + process.env.USERPROFILE = tempDir; + }); + + afterEach(() => { + saveExecApprovals({ version: 1, agents: {} }); + if (previousOpenClawHome === undefined) { + delete process.env.OPENCLAW_HOME; + } else { + process.env.OPENCLAW_HOME = previousOpenClawHome; + } + if (previousHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = previousHome; + } + if (previousUserProfile === undefined) { + delete process.env.USERPROFILE; + } else { + process.env.USERPROFILE = previousUserProfile; + } + }); + + afterAll(async () => { + if (tempRoot) { + await fs.rm(tempRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 }); + } + }); + it("warns when exec approvals enable autoAllowSkills", () => { saveExecApprovals({ version: 1,