From afef7dc4ced3e4fb31fd7bf0684886a420caffa5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 05:20:30 +0100 Subject: [PATCH] test: remove exec approval file fixtures --- docs/gateway/security/index.md | 2 +- docs/nodes/troubleshooting.md | 2 +- .../bash-tools.exec-host-shared.test.ts | 2 +- .../bash-tools.exec.approval-id.test.ts | 25 ++++++------------- src/agents/bash-tools.exec.path.test.ts | 2 +- src/agents/pi-tools.safe-bins.test.ts | 2 +- src/cli/exec-approvals-cli.test.ts | 21 ++++++++-------- src/cli/exec-policy-cli.test.ts | 20 +++++++-------- src/cli/exec-policy-cli.ts | 12 ++++----- src/gateway/server-methods/exec-approvals.ts | 2 +- src/infra/exec-approvals-policy.test.ts | 12 ++++++--- src/node-host/invoke.ts | 2 +- 12 files changed, 50 insertions(+), 54 deletions(-) diff --git a/docs/gateway/security/index.md b/docs/gateway/security/index.md index e099e521e6a..440ac3a4473 100644 --- a/docs/gateway/security/index.md +++ b/docs/gateway/security/index.md @@ -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 diff --git a/docs/nodes/troubleshooting.md b/docs/nodes/troubleshooting.md index e2f6a172194..86587978b13 100644 --- a/docs/nodes/troubleshooting.md +++ b/docs/nodes/troubleshooting.md @@ -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 diff --git a/src/agents/bash-tools.exec-host-shared.test.ts b/src/agents/bash-tools.exec-host-shared.test.ts index 2d2ae8d6b89..15873995282 100644 --- a/src/agents/bash-tools.exec-host-shared.test.ts +++ b/src/agents/bash-tools.exec-host-shared.test.ts @@ -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", diff --git a/src/agents/bash-tools.exec.approval-id.test.ts b/src/agents/bash-tools.exec.approval-id.test.ts index c89394dbc5b..da6fd3369db 100644 --- a/src/agents/bash-tools.exec.approval-id.test.ts +++ b/src/agents/bash-tools.exec.approval-id.test.ts @@ -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) { - 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[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 }, ) diff --git a/src/agents/bash-tools.exec.path.test.ts b/src/agents/bash-tools.exec.path.test.ts index 456b80f1537..64fc17ebbe5 100644 --- a/src/agents/bash-tools.exec.path.test.ts +++ b/src/agents/bash-tools.exec.path.test.ts @@ -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: { diff --git a/src/agents/pi-tools.safe-bins.test.ts b/src/agents/pi-tools.safe-bins.test.ts index 675c86842b0..b69408e3a11 100644 --- a/src/agents/pi-tools.safe-bins.test.ts +++ b/src/agents/pi-tools.safe-bins.test.ts @@ -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: { diff --git a/src/cli/exec-approvals-cli.test.ts b/src/cli/exec-approvals-cli.test.ts index 4a3784efb08..08b2bfa8084 100644 --- a/src/cli/exec-approvals-cli.test.ts +++ b/src/cli/exec-approvals-cli.test.ts @@ -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: {} }, diff --git a/src/cli/exec-policy-cli.test.ts b/src/cli/exec-policy-cli.test.ts index 9022e527d36..6f03dee39ba 100644 --- a/src/cli/exec-policy-cli.test.ts +++ b/src/cli/exec-policy-cli.test.ts @@ -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", diff --git a/src/cli/exec-policy-cli.ts b/src/cli/exec-policy-cli.ts index 4acc960a195..7d5c439bc2f 100644 --- a/src/cli/exec-policy-cli.ts +++ b/src/cli/exec-policy-cli.ts @@ -57,7 +57,7 @@ const EXEC_POLICY_PRESETS: Record & { - runtimeApprovalsSource: "local-file" | "node-runtime"; + runtimeApprovalsSource: "local-state" | "node-runtime"; security: { requested: ExecSecurity; requestedSource: string; @@ -234,7 +234,7 @@ async function buildLocalExecPolicyShowPayload(): Promise ); 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"), }, ], diff --git a/src/gateway/server-methods/exec-approvals.ts b/src/gateway/server-methods/exec-approvals.ts index 0fbed7b4be4..40429fc4327 100644 --- a/src/gateway/server-methods/exec-approvals.ts +++ b/src/gateway/server-methods/exec-approvals.ts @@ -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; } diff --git a/src/infra/exec-approvals-policy.test.ts b/src/infra/exec-approvals-policy.test.ts index 5f7390bce1c..20dd58d5a38 100644 --- a/src/infra/exec-approvals-policy.test.ts +++ b/src/infra/exec-approvals-policy.test.ts @@ -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", }); }); diff --git a/src/node-host/invoke.ts b/src/node-host/invoke.ts index 7e1597f0aa2..b179b5c9e9c 100644 --- a/src/node-host/invoke.ts +++ b/src/node-host/invoke.ts @@ -390,7 +390,7 @@ export async function handleInvoke( try { const params = decodeParams(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();