test: remove exec approval file fixtures

This commit is contained in:
Peter Steinberger
2026-05-10 05:20:30 +01:00
parent af368c4434
commit afef7dc4ce
12 changed files with 50 additions and 54 deletions

View File

@@ -425,7 +425,7 @@ If a macOS node is paired, the Gateway can invoke `system.run` on that node. Thi
- Gateway node pairing is not a per-command approval surface. It establishes node identity/trust and token issuance.
- The Gateway applies a coarse global node command policy via `gateway.nodes.allowCommands` / `denyCommands`.
- Controlled on the Mac via **Settings → Exec approvals** (security + ask + allowlist).
- The per-node `system.run` policy is the node's own exec approvals file (`exec.approvals.node.*`), which can be stricter or looser than the gateway's global command-ID policy.
- The per-node `system.run` policy is the node's own SQLite exec approvals state (`exec.approvals.node.*`), which can be stricter or looser than the gateway's global command-ID policy.
- A node running with `security="full"` and `ask="off"` is following the default trusted-operator model. Treat that as expected behavior unless your deployment explicitly requires a tighter approval or allowlist stance.
- Approval mode binds exact request context and, when possible, one concrete local script/file operand. If OpenClaw cannot identify exactly one direct local file for an interpreter/runtime command, approval-backed execution is denied rather than promising full semantic coverage.
- For `host=node`, approval-backed runs also store a canonical prepared

View File

@@ -76,7 +76,7 @@ If pairing is missing, approve the node device first.
If `nodes describe` is missing a command, check the gateway node command policy and whether the node actually declared that command on connect.
If pairing is fine but `system.run` fails, fix exec approvals/allowlist on that node.
Node pairing is an identity/trust gate, not a per-command approval surface. For `system.run`, the per-node policy lives in that node's exec approvals file (`openclaw approvals get --node ...`), not in the gateway pairing record.
Node pairing is an identity/trust gate, not a per-command approval surface. For `system.run`, the per-node policy lives in that node's SQLite exec approvals state (`openclaw approvals get --node ...`), not in the gateway pairing record.
For approval-backed `host=node` runs, the gateway also binds execution to the
prepared canonical `systemRunPlan`. If a later caller mutates command/cwd or

View File

@@ -109,7 +109,7 @@ describe("sendExecApprovalFollowupResult", () => {
});
describe("resolveExecHostApprovalContext", () => {
it("does not let exec-approvals.json broaden security beyond the requested policy", () => {
it("does not let host exec approvals broaden security beyond the requested policy", () => {
mocks.resolveExecApprovals.mockReturnValue({
defaults: {
security: "allowlist",

View File

@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { readExecApprovalsSnapshot, saveExecApprovals } from "../infra/exec-approvals.js";
import { sendMessage } from "../infra/outbound/message.js";
import { buildSystemRunPreparePayload } from "../test-utils/system-run-prepare-payload.js";
import { createExecTool } from "./bash-tools.exec.js";
@@ -183,10 +184,8 @@ function buildPreparedSystemRunPayload(rawInvokeParams: unknown) {
return buildSystemRunPreparePayload(params);
}
async function writeExecApprovalsConfig(config: Record<string, unknown>) {
const approvalsPath = path.join(process.env.HOME ?? "", ".openclaw", "exec-approvals.json");
await fs.mkdir(path.dirname(approvalsPath), { recursive: true });
await fs.writeFile(approvalsPath, JSON.stringify(config, null, 2));
async function writeExecApprovalsConfig(config: Parameters<typeof saveExecApprovals>[0]) {
saveExecApprovals(config);
}
function acceptedApprovalResponse(params: unknown) {
@@ -774,22 +773,14 @@ describe("exec approvals", () => {
expect(calls).toContain("exec.approval.request");
expect(calls).toContain("exec.approval.waitDecision");
const approvalsPath = path.join(process.env.HOME ?? "", ".openclaw", "exec-approvals.json");
await expect
.poll(
async () => {
try {
const raw = await fs.readFile(approvalsPath, "utf8");
const parsed = JSON.parse(raw) as {
agents?: { main?: { allowlist?: Array<{ source?: string }> } };
};
return (
parsed.agents?.main?.allowlist?.some((entry) => entry.source === "allow-always") ===
true
);
} catch {
return false;
}
const parsed = readExecApprovalsSnapshot().file;
return (
parsed.agents?.main?.allowlist?.some((entry) => entry.source === "allow-always") ===
true
);
},
{ timeout: 2000, interval: 1 },
)

View File

@@ -77,7 +77,7 @@ let createExecTool: typeof import("./bash-tools.exec.js").createExecTool;
function createExecApprovals(): ExecApprovalsResolved {
return {
path: "/tmp/exec-approvals.json",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
socketPath: "/tmp/exec-approvals.sock",
token: "token",
defaults: {

View File

@@ -12,7 +12,7 @@ let createOpenClawCodingTools: typeof import("./pi-tools.js").createOpenClawCodi
const { mockExecApprovals, supervisorSpawnMock } = vi.hoisted(() => {
const execApprovals = {
path: "/tmp/exec-approvals.json",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
socketPath: "/tmp/exec-approvals.sock",
token: "token",
defaults: {

View File

@@ -39,7 +39,7 @@ const mocks = vi.hoisted(() => {
};
}
return {
path: "/tmp/exec-approvals.json",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
exists: true,
hash: "hash-1",
file: { version: 1, agents: {} },
@@ -56,7 +56,7 @@ const mocks = vi.hoisted(() => {
const { callGatewayFromCli, defaultRuntime, readBestEffortConfig, runtimeErrors } = mocks;
const localSnapshot = {
path: "/tmp/local-exec-approvals.json",
path: "/tmp/local-openclaw.sqlite#kv/exec.approvals/current",
exists: true,
raw: "{}",
hash: "hash-local",
@@ -249,13 +249,14 @@ describe("exec approvals CLI", () => {
expect.objectContaining({
scopeLabel: "agent:runner",
security: expect.objectContaining({
hostSource: "/tmp/local-exec-approvals.json agents.*.security",
hostSource:
"/tmp/local-openclaw.sqlite#kv/exec.approvals/current agents.*.security",
}),
ask: expect.objectContaining({
hostSource: "/tmp/local-exec-approvals.json agents.*.ask",
hostSource: "/tmp/local-openclaw.sqlite#kv/exec.approvals/current agents.*.ask",
}),
askFallback: expect.objectContaining({
source: "/tmp/local-exec-approvals.json agents.*.askFallback",
source: "/tmp/local-openclaw.sqlite#kv/exec.approvals/current agents.*.askFallback",
}),
}),
]),
@@ -282,7 +283,7 @@ describe("exec approvals CLI", () => {
}
if (method === "exec.approvals.node.get") {
return {
path: "/tmp/node-exec-approvals.json",
path: "/tmp/node-openclaw.sqlite#kv/exec.approvals/current",
exists: true,
hash: "hash-node-1",
file: {
@@ -317,7 +318,7 @@ describe("exec approvals CLI", () => {
}),
askFallback: expect.objectContaining({
effective: "deny",
source: "/tmp/node-exec-approvals.json defaults.askFallback",
source: "/tmp/node-openclaw.sqlite#kv/exec.approvals/current defaults.askFallback",
}),
}),
],
@@ -335,7 +336,7 @@ describe("exec approvals CLI", () => {
}
if (method === "exec.approvals.get") {
return {
path: "/tmp/exec-approvals.json",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
exists: true,
hash: "hash-1",
file: { version: 1, agents: {} },
@@ -367,7 +368,7 @@ describe("exec approvals CLI", () => {
}
if (method === "exec.approvals.get") {
return {
path: "/tmp/exec-approvals.json",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
exists: true,
hash: "hash-1",
file: { version: 1, agents: {} },
@@ -399,7 +400,7 @@ describe("exec approvals CLI", () => {
}
if (method === "exec.approvals.node.get") {
return {
path: "/tmp/node-exec-approvals.json",
path: "/tmp/node-openclaw.sqlite#kv/exec.approvals/current",
exists: true,
hash: "hash-node-1",
file: { version: 1, agents: {} },

View File

@@ -104,7 +104,7 @@ const mocks = vi.hoisted(() => {
config: configState,
})),
readExecApprovalsSnapshot: vi.fn<() => ExecApprovalsSnapshot>(() => ({
path: "/tmp/exec-approvals.json",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
exists: true,
raw: "{}",
hash: "approvals-hash",
@@ -218,7 +218,7 @@ describe("exec-policy CLI", () => {
}));
mocks.readExecApprovalsSnapshot.mockReset();
mocks.readExecApprovalsSnapshot.mockImplementation(() => ({
path: "/tmp/exec-approvals.json",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
exists: true,
raw: "{}",
hash: "approvals-hash",
@@ -238,7 +238,7 @@ describe("exec-policy CLI", () => {
expect(mocks.defaultRuntime.writeJson).toHaveBeenCalledWith(
expect.objectContaining({
configPath: "/tmp/openclaw.json",
approvalsPath: "/tmp/exec-approvals.json",
approvalsStore: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
effectivePolicy: expect.objectContaining({
scopes: [
expect.objectContaining({
@@ -378,7 +378,7 @@ describe("exec-policy CLI", () => {
config: mocks.getConfig(),
}));
mocks.readExecApprovalsSnapshot.mockImplementationOnce(() => ({
path: "/tmp/exec-approvals.json\u0007\nforged",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current\u0007\nforged",
exists: true,
raw: "{}",
hash: "approvals-hash",
@@ -405,7 +405,7 @@ describe("exec-policy CLI", () => {
mocks.defaultRuntime.log.mock.calls.map((call) => String(call[0] ?? "")).join("\n"),
);
expect(output).toContain("/tmp/openclaw.json");
expect(output).toContain("/tmp/exec-approvals.json");
expect(output).toContain("/tmp/openclaw.sqlite#kv/exec.approvals/current");
expect(output).toContain("scope\\u{200B}name");
expect(output).toContain("host=auto");
expect(output).toContain("tools.exec.");
@@ -464,7 +464,7 @@ describe("exec-policy CLI", () => {
const originalApprovals = structuredClone(mocks.getApprovals());
const originalRaw = JSON.stringify(originalApprovals, null, 2);
const originalSnapshot: ExecApprovalsSnapshot = {
path: "/tmp/exec-approvals.json",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
exists: true,
raw: originalRaw,
hash: "approvals-hash",
@@ -484,9 +484,9 @@ describe("exec-policy CLI", () => {
expect(mocks.runtimeErrors).toEqual(["config write failed"]);
});
it("removes a newly-written approvals file when config replacement fails and the original file was missing", async () => {
it("removes newly-written approvals state when config replacement fails and the original state was missing", async () => {
const missingSnapshot: ExecApprovalsSnapshot = {
path: "/tmp/missing-exec-approvals.json",
path: "/tmp/missing-openclaw.sqlite#kv/exec.approvals/current",
exists: false,
raw: null,
hash: "approvals-hash",
@@ -508,7 +508,7 @@ describe("exec-policy CLI", () => {
const originalApprovals = structuredClone(mocks.getApprovals());
const originalRaw = JSON.stringify(originalApprovals, null, 2);
const originalSnapshot = {
path: "/tmp/exec-approvals.json",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
exists: true,
raw: originalRaw,
hash: "original-hash",
@@ -524,7 +524,7 @@ describe("exec-policy CLI", () => {
agents: {},
};
const concurrentSnapshot: ExecApprovalsSnapshot = {
path: "/tmp/exec-approvals.json",
path: "/tmp/openclaw.sqlite#kv/exec.approvals/current",
exists: true,
raw: JSON.stringify(concurrentFile, null, 2),
hash: "concurrent-write-hash",

View File

@@ -57,7 +57,7 @@ const EXEC_POLICY_PRESETS: Record<ExecPolicyPresetName, Required<ExecPolicyResol
type ExecPolicyShowPayload = {
configPath: string;
approvalsPath: string;
approvalsStore: string;
approvalsExists: boolean;
effectivePolicy: {
note: string;
@@ -72,7 +72,7 @@ type ExecPolicyShowScope = Omit<
ExecPolicyScopeSnapshot,
"security" | "ask" | "askFallback" | "allowedDecisions"
> & {
runtimeApprovalsSource: "local-file" | "node-runtime";
runtimeApprovalsSource: "local-state" | "node-runtime";
security: {
requested: ExecSecurity;
requestedSource: string;
@@ -234,7 +234,7 @@ async function buildLocalExecPolicyShowPayload(): Promise<ExecPolicyShowPayload>
);
return {
configPath: configSnapshot.path,
approvalsPath: approvalsSnapshot.path,
approvalsStore: approvalsSnapshot.path,
approvalsExists: approvalsSnapshot.exists,
effectivePolicy: {
note: hasNodeRuntimeScope
@@ -250,7 +250,7 @@ function buildExecPolicyShowScope(snapshot: ExecPolicyScopeSnapshot): ExecPolicy
if (snapshot.host.requested !== "node") {
return {
...baseScope,
runtimeApprovalsSource: "local-file",
runtimeApprovalsSource: "local-state",
};
}
return {
@@ -293,9 +293,9 @@ function renderExecPolicyShow(payload: ExecPolicyShowPayload): void {
],
rows: [
{ Field: "Config", Value: sanitizeExecPolicyTableCell(payload.configPath) },
{ Field: "Approvals", Value: sanitizeExecPolicyTableCell(payload.approvalsPath) },
{ Field: "Approvals", Value: sanitizeExecPolicyTableCell(payload.approvalsStore) },
{
Field: "Approvals File",
Field: "Approvals State",
Value: sanitizeExecPolicyTableCell(payload.approvalsExists ? "present" : "missing"),
},
],

View File

@@ -118,7 +118,7 @@ export const execApprovalsHandlers: GatewayRequestHandlers = {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, "exec approvals file is required"),
errorShape(ErrorCodes.INVALID_REQUEST, "exec approvals document is required"),
);
return;
}

View File

@@ -308,14 +308,18 @@ describe("exec approvals policy helpers", () => {
},
configPath: "tools.exec",
scopeLabel: "tools.exec",
hostPath: "/tmp/node-exec-approvals.json",
hostPath: "/tmp/node-openclaw.sqlite#kv/exec.approvals/current",
});
expect(summary.security.hostSource).toBe("/tmp/node-exec-approvals.json defaults.security");
expect(summary.ask.hostSource).toBe("/tmp/node-exec-approvals.json defaults.ask");
expect(summary.security.hostSource).toBe(
"/tmp/node-openclaw.sqlite#kv/exec.approvals/current defaults.security",
);
expect(summary.ask.hostSource).toBe(
"/tmp/node-openclaw.sqlite#kv/exec.approvals/current defaults.ask",
);
expect(summary.askFallback).toEqual({
effective: "deny",
source: "/tmp/node-exec-approvals.json defaults.askFallback",
source: "/tmp/node-openclaw.sqlite#kv/exec.approvals/current defaults.askFallback",
});
});

View File

@@ -390,7 +390,7 @@ export async function handleInvoke(
try {
const params = decodeParams<SystemExecApprovalsSetParams>(frame.paramsJSON);
if (!params.file || typeof params.file !== "object") {
throw new Error("INVALID_REQUEST: exec approvals file required");
throw new Error("INVALID_REQUEST: exec approvals document required");
}
ensureExecApprovals();
const snapshot = readExecApprovalsSnapshot();