test: sandbox audit-exec-surface under OpenClaw home tempdir

audit-exec-surface.test.ts called saveExecApprovals({ version: 1, agents: {} })
in a top-level afterEach and inside several it() blocks without redirecting
the OpenClaw home env vars. Sibling exec-approvals tests (the store and
agent ones) already wrap their cases in beforeAll(mkdtemp) +
beforeEach(set OPENCLAW_HOME/HOME/USERPROFILE = tempDir) for exactly this
reason.

Without that wrapping, every saveExecApprovals call resolved through
expandHomePrefix → resolveRawHomeDir, which prefers OPENCLAW_HOME and falls
back through HOME and USERPROFILE — so on a developer machine running an
OpenClaw gateway, pnpm test silently truncated the live
~/.openclaw/exec-approvals.json to { "version": 1, "agents": {} }.

Move the cleanup hook inside the describe block, add the standard
beforeAll / beforeEach / afterAll setup, and save/restore all three env
vars (OPENCLAW_HOME, HOME, USERPROFILE) so every case runs against a
per-case tempdir regardless of what the host has set.

Verified on macOS Darwin 25.4.0 (M1) running an active OpenClaw gateway
with the live ~/.openclaw/exec-approvals.json (11722 bytes, 7 configured
agents): SHA-256 unchanged before vs after running
  pnpm exec vitest run src/security/audit-exec-surface.test.ts
all 7 cases pass.
This commit is contained in:
Omar Shahine
2026-05-09 11:29:56 -07:00
parent 9a2eba684e
commit de039cde81
2 changed files with 59 additions and 5 deletions

View File

@@ -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

View File

@@ -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,