From d8163de918e51a2eee0a8e0087e1dfbd52a59ae4 Mon Sep 17 00:00:00 2001 From: pallaoro Date: Sun, 12 Apr 2026 23:24:49 +0200 Subject: [PATCH] Validate unknown fields on flow nodes, bump to 0.9.5 Reject unrecognized keys on any node type during flow validation. Catches misplaced fields like branch paths placed as siblings of "paths" instead of inside it. --- package-lock.json | 4 ++-- package.json | 2 +- src/core/validate.ts | 29 +++++++++++++++++++++++++++++ tests/core.test.ts | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0742559..a0a2aed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@clawnify/clawflow", - "version": "0.9.2", + "version": "0.9.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@clawnify/clawflow", - "version": "0.9.2", + "version": "0.9.4", "license": "MIT", "devDependencies": { "@types/node": "^25.5.0", diff --git a/package.json b/package.json index 6576411..aa034b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@clawnify/clawflow", - "version": "0.9.4", + "version": "0.9.5", "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/core/validate.ts b/src/core/validate.ts index 4e09133..b2dd1db 100644 --- a/src/core/validate.ts +++ b/src/core/validate.ts @@ -153,6 +153,25 @@ function validateNodes( } } +// ---- Allowed keys per node type (BaseNode keys are always allowed) ---------------- + +const BASE_KEYS = new Set(["name", "do", "output", "retry", "timeout"]); + +const ALLOWED_KEYS: Record> = { + ai: new Set([...BASE_KEYS, "prompt", "input", "schema", "model", "temperature", "maxTokens", "attachments"]), + agent: new Set([...BASE_KEYS, "task", "input", "tools", "agentId"]), + branch: new Set([...BASE_KEYS, "on", "paths", "default"]), + condition: new Set([...BASE_KEYS, "if", "then", "else"]), + loop: new Set([...BASE_KEYS, "over", "as", "nodes"]), + parallel: new Set([...BASE_KEYS, "nodes", "mode"]), + http: new Set([...BASE_KEYS, "url", "method", "body", "headers"]), + memory: new Set([...BASE_KEYS, "action", "key", "value"]), + wait: new Set([...BASE_KEYS, "for", "prompt", "preview", "event"]), + sleep: new Set([...BASE_KEYS, "duration"]), + code: new Set([...BASE_KEYS, "run", "input"]), + exec: new Set([...BASE_KEYS, "command", "cwd"]), +}; + /** Validate required fields per node type */ function validateNodeFields(node: FlowNode, errors: ValidationError[]): void { const e = (field: string, msg: string) => @@ -164,6 +183,16 @@ function validateNodeFields(node: FlowNode, errors: ValidationError[]): void { return; } + // Check for unknown keys + const allowed = ALLOWED_KEYS[nodeType]; + if (allowed) { + for (const key of Object.keys(node)) { + if (!allowed.has(key)) { + e(key, `Unknown field "${key}" on ${nodeType} node "${node.name}"`); + } + } + } + switch (nodeType) { case "ai": { const n = node as AiNode; diff --git a/tests/core.test.ts b/tests/core.test.ts index 7345588..614cedc 100644 --- a/tests/core.test.ts +++ b/tests/core.test.ts @@ -1166,6 +1166,38 @@ describe("validateFlow", () => { assert.equal(result.ok, true); }); + it("catches branch path placed as sibling of paths", () => { + const flow: FlowDefinition = { + flow: "misplaced-branch-path", + nodes: [ + { name: "classify", do: "code" as const, run: "'densita'", output: "order_type" }, + { + name: "route", do: "branch" as const, on: "order_type", + paths: { + densita: [{ name: "d1", do: "code" as const, run: "'ok'", output: "x" }], + }, + // This is the bug: diametri is a sibling of paths, not inside it + diametri: [{ name: "d2", do: "code" as const, run: "'ok'", output: "y" }], + } as any, + ], + }; + const result = validateFlow(flow); + assert.equal(result.ok, false); + assert.ok(result.errors.some((e) => e.message.includes("Unknown field \"diametri\""))); + }); + + it("catches unknown fields on any node type", () => { + const flow: FlowDefinition = { + flow: "unknown-field", + nodes: [ + { name: "bad", do: "ai" as const, prompt: "hello", bogus: true } as any, + ], + }; + const result = validateFlow(flow); + assert.equal(result.ok, false); + assert.ok(result.errors.some((e) => e.message.includes("Unknown field \"bogus\""))); + }); + it("catches unknown node type", () => { const flow: FlowDefinition = { flow: "unknown-type",