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();