From c46f1e4496efa5eca0cbb2d023958107903b44a2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 1 Aug 2026 21:36:05 -0700 Subject: [PATCH] fix(logbook): honor canonical gateway.nodes.commands.deny kill switch (#117803) --- extensions/logbook/index.test.ts | 51 ++++++++++++++++++++++++++++++++ extensions/logbook/index.ts | 4 +-- 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 extensions/logbook/index.test.ts diff --git a/extensions/logbook/index.test.ts b/extensions/logbook/index.test.ts new file mode 100644 index 000000000000..1540d615445e --- /dev/null +++ b/extensions/logbook/index.test.ts @@ -0,0 +1,51 @@ +import type { + OpenClawPluginApi, + OpenClawPluginNodeInvokePolicy, +} from "openclaw/plugin-sdk/plugin-entry"; +import { describe, expect, it, vi } from "vitest"; +import plugin from "./index.js"; + +type PolicyContext = Parameters[0]; + +function registerLogbookPolicies(): OpenClawPluginNodeInvokePolicy[] { + const policies: OpenClawPluginNodeInvokePolicy[] = []; + plugin.register({ + pluginConfig: {}, + session: { controls: { registerControlUiDescriptor: () => {} } }, + registerNodeInvokePolicy: (policy: OpenClawPluginNodeInvokePolicy) => policies.push(policy), + registerService: () => {}, + registerGatewayMethod: () => {}, + } as unknown as OpenClawPluginApi); + return policies; +} + +describe("logbook snapshot invoke policy", () => { + it("blocks logbook.snapshot when gateway.nodes.commands.deny lists screen.snapshot", async () => { + const [policy] = registerLogbookPolicies(); + expect(policy?.commands).toEqual(["logbook.snapshot"]); + const invokeNode = vi.fn(); + const result = await policy!.handle({ + nodeId: "node-1", + command: "logbook.snapshot", + params: undefined, + config: { gateway: { nodes: { commands: { deny: ["screen.snapshot"] } } } }, + invokeNode, + } as unknown as PolicyContext); + expect(result).toMatchObject({ ok: false, code: "SCREEN_CAPTURE_DENIED" }); + expect(invokeNode).not.toHaveBeenCalled(); + }); + + it("invokes the node when screen.snapshot is not denied", async () => { + const [policy] = registerLogbookPolicies(); + const invokeNode = vi.fn().mockResolvedValue({ ok: true, payloadJSON: null }); + const result = await policy!.handle({ + nodeId: "node-1", + command: "logbook.snapshot", + params: undefined, + config: { gateway: { nodes: { commands: { deny: ["camera.snap"] } } } }, + invokeNode, + } as unknown as PolicyContext); + expect(result).toMatchObject({ ok: true }); + expect(invokeNode).toHaveBeenCalledTimes(1); + }); +}); diff --git a/extensions/logbook/index.ts b/extensions/logbook/index.ts index 4e6800d33165..b95b214dc526 100644 --- a/extensions/logbook/index.ts +++ b/extensions/logbook/index.ts @@ -113,13 +113,13 @@ export default definePluginEntry({ handle: async (ctx) => { // Honor the operator's screen-capture kill switch: a screen.snapshot // deny must block this capture command too, not just the app node's. - const denied = ctx.config.gateway?.nodes?.denyCommands ?? []; + const denied = ctx.config.gateway?.nodes?.commands?.deny ?? []; if (denied.includes("screen.snapshot")) { return { ok: false, code: "SCREEN_CAPTURE_DENIED", message: - "screen capture is denied by gateway.nodes.denyCommands (screen.snapshot); Logbook capture stays blocked until it is removed", + "screen capture is denied by gateway.nodes.commands.deny (screen.snapshot); Logbook capture stays blocked until it is removed", }; } return await ctx.invokeNode();