From cb3833a6737c5352c667cedd76e1089274bc3665 Mon Sep 17 00:00:00 2001 From: pallaoro Date: Sun, 6 Sep 2026 21:48:21 +0200 Subject: [PATCH] Retire the flow-authoring gate; @clawnify/agent-permissions owns it (1.5.2) flow_create / flow_edit / flow_publish / flow_delete are gated by @clawnify/agent-permissions 0.6.0, the one permission authority on the box. This plugin's own gate for them was registered through registerHook, which OpenClaw 2026.9.1 no longer dispatches for before_tool_call, so it prompted on 2026.7.1 and silently did not on 2026.9.1; and with both gates live a 2026.7.1 box prompted twice per write. Only the flow_run gate stays: it is the operator's opt-in, skippable for unattended sessions. approval.gateMutations is accepted and ignored so existing configs still validate. --- openclaw.plugin.json | 2 +- package-lock.json | 4 +-- package.json | 2 +- src/plugin/index.ts | 46 +++++++++-------------------- tests/approval-gate.test.ts | 58 +++++++++++++++++-------------------- 5 files changed, 43 insertions(+), 69 deletions(-) diff --git a/openclaw.plugin.json b/openclaw.plugin.json index fdbc553..2c9cd24 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -129,7 +129,7 @@ "gateMutations": { "type": "boolean", "default": true, - "description": "Gate flow authoring/publishing (flow_create/edit/publish/delete) behind approval on every call, independently of `enabled` (which governs flow_run). Set false to disable." + "description": "Retired in 1.5.2 and ignored: flow authoring (flow_create/edit/publish/delete) is gated by @clawnify/agent-permissions, the box's permission authority. Kept so existing configs still validate." } } } diff --git a/package-lock.json b/package-lock.json index 27cfb74..9fc4608 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@clawnify/clawflow", - "version": "1.3.1", + "version": "1.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@clawnify/clawflow", - "version": "1.3.1", + "version": "1.5.2", "license": "MIT", "devDependencies": { "@types/node": "^25.5.0", diff --git a/package.json b/package.json index a335ae0..7724c5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@clawnify/clawflow", - "version": "1.5.1", + "version": "1.5.2", "description": "The n8n for agents. A declarative, AI-native workflow format that agents can read, write, and run.", "type": "module", "main": "./dist/index.js", diff --git a/src/plugin/index.ts b/src/plugin/index.ts index ff0a34e..98e30ec 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -127,42 +127,22 @@ function register(api: PluginApi) { : 5 * 60_000; const approvalTimeoutBehavior: "allow" | "deny" = approvalCfg.timeoutBehavior === "allow" ? "allow" : "deny"; - // Intrinsic mutation gate: authoring/publishing/deleting a flow is a write - // action that must never run without a human OK, so it is gated on every call - // independently of `enabled` (which governs flow_run) and does NOT honor - // skipSessionPatterns. Kill-switch: approval.gateMutations=false. - const gateMutations = approvalCfg.gateMutations !== false; - const MUTATION_VERBS: Record = { - flow_create: "Create", - flow_edit: "Edit", - flow_publish: "Publish", - flow_delete: "Delete", - }; - - if (api.registerHook && (approvalEnabled || gateMutations)) { + // Flow authoring (flow_create / flow_edit / flow_publish / flow_delete) is + // gated by @clawnify/agent-permissions ≥ 0.6.0, the one permission authority + // on the box, not here. This plugin's own gate for it (1.3 – 1.5.1) was + // registered through registerHook, which OpenClaw 2026.9.1 no longer + // dispatches for before_tool_call, so it would have prompted on 2026.7.1 and + // silently not on 2026.9.1; and with both gates live a 2026.7.1 box prompted + // twice per write. `approval.gateMutations` is accepted for old configs and + // ignored. Only the flow_run gate stays here: it is the operator's opt-in, + // skippable for unattended sessions. + + if (api.registerHook && approvalEnabled) { api.registerHook( "before_tool_call", (event) => { const toolName = event.toolName ?? event.tool; - - // Flow-authoring tools — always gate (no skipSessionPatterns). No - // allow-always persist path, so every call re-prompts. - const mutationVerb = toolName ? MUTATION_VERBS[toolName] : undefined; - if (gateMutations && mutationVerb) { - const mp = (event.params ?? {}) as { file?: string; flow?: string }; - const name = mp.flow ?? mp.file ?? "inline flow"; - return { - requireApproval: { - title: `${mutationVerb} clawflow "${name}"?`.slice(0, 80), - description: "Creates, edits, publishes, or deletes a flow definition.", - severity: "warning", - timeoutMs: approvalTimeoutMs, - timeoutBehavior: approvalTimeoutBehavior, - }, - }; - } - - if (!approvalEnabled || toolName !== "flow_run") return; + if (toolName !== "flow_run") return; const sessionKey = event.context?.sessionKey ?? ""; if (skipPatterns.some((pattern) => sessionKey.includes(pattern))) { @@ -185,7 +165,7 @@ function register(api: PluginApi) { }, { name: "clawflow-approval-gate", - description: "Gate flow_run (skippable) and flow authoring/publishing/deletion (always) behind user approval.", + description: "Gate flow_run behind user approval (skippable for unattended sessions). Flow authoring is gated by agent-permissions.", }, ); } else if (!api.registerHook) { diff --git a/tests/approval-gate.test.ts b/tests/approval-gate.test.ts index 7a8dd02..150ba3e 100644 --- a/tests/approval-gate.test.ts +++ b/tests/approval-gate.test.ts @@ -36,41 +36,35 @@ function captureHook(clawflowConfig: Record): Hook { return hook!; } -describe("clawflow approval gate — flow mutation tools", () => { - it("gates create/edit/publish/delete by default", async () => { - const hook = captureHook({}); - for (const [tool, verb] of [ - ["flow_create", "Create"], - ["flow_edit", "Edit"], - ["flow_publish", "Publish"], - ["flow_delete", "Delete"], - ] as const) { - const res = await hook({ toolName: tool, params: { flow: "my-flow" } }); - assert.ok(res && res.requireApproval, `${tool} should require approval`); - assert.match(res.requireApproval!.title, new RegExp(`^${verb} clawflow "my-flow"`)); +describe("clawflow approval gate", () => { + // Since 1.5.2 flow_create/edit/publish/delete are gated by + // @clawnify/agent-permissions (always-ask, no rule can pre-approve). This + // plugin must not gate them too: OpenClaw 2026.9.1 stopped dispatching + // registerHook for before_tool_call, so a gate here prompts on 2026.7.1 and + // silently does not on 2026.9.1, and with both live a 2026.7.1 box prompts + // twice per write. + it("does not gate create/edit/publish/delete, whatever the config says", async () => { + for (const approval of [{ enabled: true }, { enabled: true, gateMutations: true }, { enabled: true, skipSessionPatterns: ["email"] }]) { + const hook = captureHook({ approval }); + for (const tool of ["flow_create", "flow_edit", "flow_publish", "flow_delete"]) { + const res = await hook({ toolName: tool, params: { flow: "my-flow" } }); + assert.equal(res, undefined, `${tool} must be left to agent-permissions`); + } } }); - it("gates mutations even when the flow_run gate is disabled (independent of enabled)", async () => { - const hook = captureHook({ approval: { enabled: false } }); - const res = await hook({ toolName: "flow_delete", params: { file: "x" } }); - assert.ok(res && res.requireApproval, "mutation must still gate when enabled=false"); - }); - - it("does NOT honor skipSessionPatterns for mutations (always require)", async () => { - const hook = captureHook({ approval: { skipSessionPatterns: ["email"] } }); - const res = await hook({ - toolName: "flow_publish", - params: { flow: "f" }, - context: { sessionKey: "agent:main:main:email:123" }, - }); - assert.ok(res && res.requireApproval, "mutation must gate even in a skipped session"); - }); - - it("kill-switch: gateMutations=false disables mutation gating", async () => { - const hook = captureHook({ approval: { gateMutations: false } }); - const res = await hook({ toolName: "flow_create", params: { flow: "f" } }); - assert.equal(res, undefined, "gateMutations=false should not gate"); + it("registers no hook at all when the flow_run gate is off", () => { + let registered = false; + const api = { + registerTool: () => {}, + registerHook: () => { + registered = true; + }, + config: { workspace: os.tmpdir(), plugins: { entries: { clawflow: { config: { approval: { enabled: false } } } } } }, + logger: { info: () => {}, warn: () => {}, error: () => {} }, + }; + plugin.register(api as never); + assert.equal(registered, false, "nothing to gate here when flow_run gating is off"); }); it("keeps flow_run behavior: gated when enabled, skipped by pattern", async () => {