From 52967728811314f25a7356df05607f0f3a5b6866 Mon Sep 17 00:00:00 2001 From: pallaoro Date: Tue, 19 May 2026 11:59:11 +0200 Subject: [PATCH 1/4] manifest: declare contracts.tools, bump to 1.2.0 OpenClaw 2026.5.2 began enforcing `contracts.tools` as the manifest ownership contract for plugin tool registration: any runtime `registerTool()` call whose name isn't declared in the manifest is rejected, so the gateway reports `toolNames: []` and tools like `flow_run` are invisible to agents. Declares all 11 tools registered in src/plugin/index.ts: flow_create, flow_delete, flow_restore_from_bin, flow_run, flow_resume, flow_send_event, flow_status, flow_list, flow_read, flow_publish, flow_edit. Also: - Bumps compat.pluginApi / minGatewayVersion to >=2026.5.2 (the release that enforced the contract) and build.openclawVersion to 2026.5.12. - Syncs openclaw.plugin.json version (was stale at 0.2.2) with package.json (now both 1.2.0). - Drops the unreferenced duplicate manifest at src/plugin/openclaw.plugin.json. --- openclaw.plugin.json | 17 ++++++++++++++++- package.json | 8 ++++---- src/plugin/openclaw.plugin.json | 29 ----------------------------- 3 files changed, 20 insertions(+), 34 deletions(-) delete mode 100644 src/plugin/openclaw.plugin.json diff --git a/openclaw.plugin.json b/openclaw.plugin.json index f548ddf..d1b6954 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -2,8 +2,23 @@ "id": "clawflow", "name": "ClawFlow", "description": "The n8n for agents. Declarative, AI-native workflow engine — LLM-writable, Cloudflare-portable.", - "version": "0.2.2", + "version": "1.2.0", "skills": ["./skills/flow"], + "contracts": { + "tools": [ + "flow_create", + "flow_delete", + "flow_restore_from_bin", + "flow_run", + "flow_resume", + "flow_send_event", + "flow_status", + "flow_list", + "flow_read", + "flow_publish", + "flow_edit" + ] + }, "configSchema": { "type": "object", "additionalProperties": false, diff --git a/package.json b/package.json index ce66ad9..71e14bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@clawnify/clawflow", - "version": "1.1.0", + "version": "1.2.0", "description": "The n8n for agents. A declarative, AI-native workflow format that agents can read, write, and run.", "type": "module", "main": "./dist/index.js", @@ -28,11 +28,11 @@ "./index.ts" ], "compat": { - "pluginApi": ">=2026.3.22", - "minGatewayVersion": ">=2026.3.22" + "pluginApi": ">=2026.5.2", + "minGatewayVersion": ">=2026.5.2" }, "build": { - "openclawVersion": "2026.3.24" + "openclawVersion": "2026.5.12" } }, "files": [ diff --git a/src/plugin/openclaw.plugin.json b/src/plugin/openclaw.plugin.json deleted file mode 100644 index 8ffc8f5..0000000 --- a/src/plugin/openclaw.plugin.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "id": "clawflow", - "name": "ClawFlow", - "description": "The n8n for agents. Declarative, AI-native workflow engine — LLM-writable, Cloudflare-portable.", - "version": "0.2.2", - "skills": ["./skills/flow"], - "configSchema": { - "type": "object", - "additionalProperties": false, - "properties": { - "apiKey": { "type": "string" }, - "defaultModel": { "type": "string" }, - "baseUrl": { "type": "string" }, - "defaultAgent": { "type": "string" }, - "memoryDir": { "type": "string" }, - "stateDir": { "type": "string" }, - "maxNodeDurationMs": { "type": "number" } - } - }, - "uiHints": { - "apiKey": { "label": "Anthropic API Key", "sensitive": true, "help": "Used when ANTHROPIC_API_KEY env var is not set" }, - "defaultModel": { "label": "Default AI Model", "placeholder": "smart" }, - "baseUrl": { "label": "Direct API Base URL", "placeholder": "https://api.anthropic.com" }, - "defaultAgent": { "label": "Agent ID for do:agent nodes", "placeholder": "clawflow", "help": "OpenClaw agent ID to delegate agent tasks to. Uses --local (embedded) if unset." }, - "memoryDir": { "label": "Memory Directory" }, - "stateDir": { "label": "Flow State Directory" }, - "maxNodeDurationMs": { "label": "Node Timeout (ms)", "placeholder": "30000" } - } -} From f95c075dd512b4902be942c34ba25ddb2c816572 Mon Sep 17 00:00:00 2001 From: pallaoro Date: Tue, 19 May 2026 12:52:03 +0200 Subject: [PATCH 2/4] src: re-export plugin default from src/index.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OpenClaw's plugin loader uses `package.json.main` (./dist/index.js) as the entry point, so dist/index.js must expose `default.register` for the plugin to register tools. Before this change, src/index.ts contained only library re-exports (FlowRunner, validateFlow, etc.) — compiled dist/index.js had no `default` export at all, and the gateway logged `[plugins] clawflow missing register/activate export`. After: dist/index.js exports BOTH the plugin default (id + register) AND the public library surface, so OpenClaw can register the 11 flow tools while standalone consumers can still `import { FlowRunner }`. --- src/index.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index e1dc62f..e685d54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,10 @@ -// clawflow — core exports +// clawflow — core exports + OpenClaw plugin entry +// +// The default export is the OpenClaw plugin definition (id, register). Named +// exports are the public library surface (FlowRunner, validateFlow, etc.) for +// standalone consumers (e.g. `@clawnify/clawflow` as a Cloudflare runtime). +export { default } from "./plugin/index.js"; + export { FlowRunner, sendEvent } from "./core/runner.js"; export { StateStore } from "./core/store.js"; export { transpileToCloudflare } from "./core/transpile.js"; From 07ca7c89d78977309f359be014542ee12c370516 Mon Sep 17 00:00:00 2001 From: pallaoro Date: Tue, 19 May 2026 12:57:05 +0200 Subject: [PATCH 3/4] plugin: pass required name to registerHook OpenClaw 2026.4.26 began requiring an explicit hook name in the third argument of `api.registerHook()`. Without it the loader rejects the plugin entirely with: plugin load failed: clawflow: Error: hook registration missing name This prevents the plugin's register() from completing, so none of the 11 flow_* tools end up registered either. Passes { name: "clawflow-flow-run-approval" } as the opts argument and updates the local PluginApi typing to match the current SDK shape (events: string|string[], opts.name required). --- src/plugin/index.ts | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/plugin/index.ts b/src/plugin/index.ts index 762a172..a125574 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -22,12 +22,12 @@ import type { FlowDefinition, FlowNode, PluginConfig, BranchNode, ConditionNode, interface PluginApi { registerTool: (def: object, opts?: { optional?: boolean }) => void; registerHook?: ( - name: string, + events: string | string[], handler: (event: { tool?: string; params?: unknown; [k: string]: unknown }) => | { requireApproval?: boolean; prompt?: string; block?: boolean } | void | Promise<{ requireApproval?: boolean; prompt?: string; block?: boolean } | void>, - opts?: { priority?: number }, + opts: { name: string; description?: string; priority?: number }, ) => void; config?: { plugins?: { entries?: Record }; @@ -78,17 +78,24 @@ function register(api: PluginApi) { // Pause and prompt the user before any flow_run invocation. Flows can have // side effects (HTTP, exec, agent delegation) so we require explicit consent. if (api.registerHook) { - api.registerHook("before_tool_call", (event) => { - if (event.tool !== "flow_run") return; - const p = (event.params ?? {}) as { file?: string; flow?: { flow?: string }; version?: number; draft?: boolean }; - const target = p.file ?? p.flow?.flow ?? "inline flow"; - const variant = - p.version != null ? ` v${p.version}` : p.draft ? " (draft)" : ""; - return { - requireApproval: true, - prompt: `Run clawflow "${target}"${variant}?`, - }; - }); + api.registerHook( + "before_tool_call", + (event) => { + if (event.tool !== "flow_run") return; + const p = (event.params ?? {}) as { file?: string; flow?: { flow?: string }; version?: number; draft?: boolean }; + const target = p.file ?? p.flow?.flow ?? "inline flow"; + const variant = + p.version != null ? ` v${p.version}` : p.draft ? " (draft)" : ""; + return { + requireApproval: true, + prompt: `Run clawflow "${target}"${variant}?`, + }; + }, + { + name: "clawflow-flow-run-approval", + description: "Request user approval before executing flow_run (flows can call HTTP, exec, and agent tools).", + }, + ); } else { api.logger?.warn( "clawflow: registerHook unavailable — flow_run will run without approval gate. Update OpenClaw to enable.", From c5a6d8fbd4160d03864e11ee1003107a3180b831 Mon Sep 17 00:00:00 2001 From: pallaoro Date: Tue, 19 May 2026 13:08:29 +0200 Subject: [PATCH 4/4] plugin: make flow_run approval gate configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The approval hook previously prompted unconditionally on every flow_run and returned a boolean `requireApproval: true` with a plain prompt string — inflexible and incompatible with hook-driven, unattended automation (e.g. inbound-email workflows whose session has no interactive channel to surface the prompt in). Adds `approval` to PluginConfig with four knobs, all backward-compatible (the default behavior is still "ask on every run"): approval: enabled: boolean # default true; false disables entirely skipSessionPatterns: string[] # substrings; if any appears in # ctx.sessionKey the gate is skipped # (e.g. ["email"] for inbound-email # automation: session keys look like # `agent:main:main:email:`) timeoutMs: number # default 300000 (5 min) timeoutBehavior: "allow"|"deny" # default "deny" Also modernizes the hook: - Reads `event.toolName` (keeps `event.tool` fallback for older runtimes). - Reads `event.context.sessionKey` per current OpenClaw event shape. - Returns the rich `requireApproval: { title, description, severity, timeoutMs, timeoutBehavior }` object the gateway and dashboard render natively, instead of the legacy boolean+prompt pair. Hosts that want unattended automation (e.g. Clawnify) can configure `plugins.entries.clawflow.config.approval.skipSessionPatterns = ["email"]`. Standalone users keep the safe default. --- openclaw.plugin.json | 13 +++++++- src/core/types.ts | 24 +++++++++++++++ src/plugin/index.ts | 72 +++++++++++++++++++++++++++++++++++++------- 3 files changed, 97 insertions(+), 12 deletions(-) diff --git a/openclaw.plugin.json b/openclaw.plugin.json index d1b6954..f080b71 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -38,6 +38,16 @@ "flowsDir": { "type": "string" } }, "required": ["port"] + }, + "approval": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { "type": "boolean" }, + "skipSessionPatterns": { "type": "array", "items": { "type": "string" } }, + "timeoutMs": { "type": "number" }, + "timeoutBehavior": { "type": "string", "enum": ["allow", "deny"] } + } } } }, @@ -49,6 +59,7 @@ "memoryDir": { "label": "Memory Directory" }, "stateDir": { "label": "Flow State Directory" }, "maxNodeDurationMs": { "label": "Node Timeout (ms)", "placeholder": "30000" }, - "serve": { "label": "Flow HTTP Server", "help": "Start an HTTP server that runs flows on POST. Requires at least port. Endpoint: POST ///run with the JSON body as the flow's inputs." } + "serve": { "label": "Flow HTTP Server", "help": "Start an HTTP server that runs flows on POST. Requires at least port. Endpoint: POST ///run with the JSON body as the flow's inputs." }, + "approval": { "label": "Approval Gate", "help": "Prompt the user before flow_run executes. Set enabled=false to disable entirely. Add session-key substrings to skipSessionPatterns to bypass for unattended hooks (e.g. \"email\" for inbound-email automation)." } } } diff --git a/src/core/types.ts b/src/core/types.ts index 747693b..7929710 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -393,6 +393,30 @@ export interface PluginConfig { * test isolation or to run multiple FlowRunners with different step sets. */ customSteps?: import("./custom-steps.js").StepRegistry; + /** + * Approval gate for the `flow_run` tool. Flows can call HTTP, exec, and + * agent tools, so by default the plugin prompts the user before each run. + * Hosts that need unattended automation can disable the gate entirely or + * skip it for specific session contexts. + */ + approval?: ApprovalConfig; +} + +export interface ApprovalConfig { + /** Disable the approval gate entirely. Default: `true` (gate enabled). */ + enabled?: boolean; + /** + * Substrings matched against the current session key. If any substring + * appears in the session key, the approval gate is skipped for that run. + * Useful for hook-driven sessions where no interactive channel is bound + * (e.g. inbound email automation, where the session key looks like + * `agent:main:main:email:`). Default: `[]`. + */ + skipSessionPatterns?: string[]; + /** Override the prompt timeout (ms). Default: `300000` (5 min). */ + timeoutMs?: number; + /** Action on prompt timeout: `"allow"` or `"deny"`. Default: `"deny"`. */ + timeoutBehavior?: "allow" | "deny"; } // ---- Model Shorthands ----------------------------------------------------------- diff --git a/src/plugin/index.ts b/src/plugin/index.ts index a125574..e057eba 100644 --- a/src/plugin/index.ts +++ b/src/plugin/index.ts @@ -19,14 +19,37 @@ import type { FlowDefinition, FlowNode, PluginConfig, BranchNode, ConditionNode, // flow_publish — publish a draft flow as a numbered version // flow_edit — edit nodes in a flow definition (file or inline) +interface BeforeToolCallEvent { + toolName?: string; + /** Older OpenClaw releases used `tool`; kept for back-compat. */ + tool?: string; + params?: unknown; + context?: { + sessionKey?: string; + sessionId?: string; + agentId?: string; + runId?: string; + [k: string]: unknown; + }; + [k: string]: unknown; +} + +type RequireApprovalDecision = { + title: string; + description: string; + severity?: "info" | "warning" | "critical"; + timeoutMs?: number; + timeoutBehavior?: "allow" | "deny"; +}; + interface PluginApi { registerTool: (def: object, opts?: { optional?: boolean }) => void; registerHook?: ( events: string | string[], - handler: (event: { tool?: string; params?: unknown; [k: string]: unknown }) => - | { requireApproval?: boolean; prompt?: string; block?: boolean } + handler: (event: BeforeToolCallEvent) => + | { requireApproval?: RequireApprovalDecision; block?: boolean; blockReason?: string } | void - | Promise<{ requireApproval?: boolean; prompt?: string; block?: boolean } | void>, + | Promise<{ requireApproval?: RequireApprovalDecision; block?: boolean; blockReason?: string } | void>, opts: { name: string; description?: string; priority?: number }, ) => void; config?: { @@ -75,28 +98,55 @@ function register(api: PluginApi) { } // ---- Approval gate for flow_run ----------------------------------------------- - // Pause and prompt the user before any flow_run invocation. Flows can have - // side effects (HTTP, exec, agent delegation) so we require explicit consent. - if (api.registerHook) { + // Flows can call HTTP, exec, and agent tools, so by default we prompt the user + // before each run. Disable entirely (`approval.enabled: false`) or skip for + // specific session contexts (`approval.skipSessionPatterns`) — useful for + // hook-driven, unattended automation that has no interactive channel to + // approve in. + const approvalCfg = pluginCfg.approval ?? {}; + const approvalEnabled = approvalCfg.enabled !== false; + const skipPatterns = Array.isArray(approvalCfg.skipSessionPatterns) + ? approvalCfg.skipSessionPatterns.filter((p): p is string => typeof p === "string" && p.length > 0) + : []; + const approvalTimeoutMs = + typeof approvalCfg.timeoutMs === "number" && approvalCfg.timeoutMs > 0 + ? approvalCfg.timeoutMs + : 5 * 60_000; + const approvalTimeoutBehavior: "allow" | "deny" = + approvalCfg.timeoutBehavior === "allow" ? "allow" : "deny"; + + if (api.registerHook && approvalEnabled) { api.registerHook( "before_tool_call", (event) => { - if (event.tool !== "flow_run") return; + const toolName = event.toolName ?? event.tool; + if (toolName !== "flow_run") return; + + const sessionKey = event.context?.sessionKey ?? ""; + if (skipPatterns.some((pattern) => sessionKey.includes(pattern))) { + return; + } + const p = (event.params ?? {}) as { file?: string; flow?: { flow?: string }; version?: number; draft?: boolean }; const target = p.file ?? p.flow?.flow ?? "inline flow"; const variant = p.version != null ? ` v${p.version}` : p.draft ? " (draft)" : ""; return { - requireApproval: true, - prompt: `Run clawflow "${target}"${variant}?`, + requireApproval: { + title: `Run clawflow "${target}"${variant}?`, + description: "Flows may call HTTP, exec, and agent tools.", + severity: "warning", + timeoutMs: approvalTimeoutMs, + timeoutBehavior: approvalTimeoutBehavior, + }, }; }, { name: "clawflow-flow-run-approval", - description: "Request user approval before executing flow_run (flows can call HTTP, exec, and agent tools).", + description: "Request user approval before executing flow_run (skippable via approval config).", }, ); - } else { + } else if (!api.registerHook) { api.logger?.warn( "clawflow: registerHook unavailable — flow_run will run without approval gate. Update OpenClaw to enable.", );