Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,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"] }
}
}
}
},
Expand All @@ -34,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 /<basePath>/<flowName>/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 /<basePath>/<flowName>/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)." }
}
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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": [
Expand Down
24 changes: 24 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:<message-id>`). 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 -----------------------------------------------------------
Expand Down
8 changes: 7 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
97 changes: 77 additions & 20 deletions src/plugin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,38 @@ 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?: (
name: string,
handler: (event: { tool?: string; params?: unknown; [k: string]: unknown }) =>
| { requireApproval?: boolean; prompt?: string; block?: boolean }
events: string | string[],
handler: (event: BeforeToolCallEvent) =>
| { requireApproval?: RequireApprovalDecision; block?: boolean; blockReason?: string }
| void
| Promise<{ requireApproval?: boolean; prompt?: string; block?: boolean } | void>,
opts?: { priority?: number },
| Promise<{ requireApproval?: RequireApprovalDecision; block?: boolean; blockReason?: string } | void>,
opts: { name: string; description?: string; priority?: number },
) => void;
config?: {
plugins?: { entries?: Record<string, { config?: PluginConfig }> };
Expand Down Expand Up @@ -75,21 +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) {
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}?`,
};
});
} else {
// 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) => {
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: {
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 (skippable via approval config).",
},
);
} else if (!api.registerHook) {
api.logger?.warn(
"clawflow: registerHook unavailable — flow_run will run without approval gate. Update OpenClaw to enable.",
);
Expand Down
29 changes: 0 additions & 29 deletions src/plugin/openclaw.plugin.json

This file was deleted.

Loading