import * as vm from "node:vm"; /** * Parse + validate a workflow script the way Claude Code's Workflow tool does: * - the FIRST statement must be `export const meta = { ... }`, a PURE LITERAL; * - meta.name and meta.description are required non-empty strings; * - the script may not use Date.now()/Math.random()/argless new Date() * (they break resume — the determinism contract). * * Clean-room (no acorn dependency): the meta object literal is located by * brace-matching and evaluated in an *empty* vm context, so any reference to a * variable, function call, or interpolation throws — which is exactly the * "pure literal" rule. The matched span is stripped to yield the runnable body. */ /** Mirrors AgentRunOptions['effort'] without importing the SDK-coupled module. */ export type WorkflowEffort = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; const EFFORT_VALUES: readonly WorkflowEffort[] = ["off", "minimal", "low", "medium", "high", "xhigh"]; export interface WorkflowMetaPhase { title: string; detail?: string; model?: string; effort?: WorkflowEffort; } export interface WorkflowMeta { name: string; description: string; whenToUse?: string; phases?: WorkflowMetaPhase[]; model?: string; effort?: WorkflowEffort; [key: string]: unknown; } // Author-facing fast feedback. The real runtime enforcement is the prelude in runtime.ts. const DETERMINISM_BLOCKLIST = /\bDate\s*\.\s*now\b|\bMath\s*\.\s*random\b|\bnew\s+Date\s*\(\s*\)/; const META_PREFIX = /^export\s+const\s+meta\s*=/; export function parseWorkflowScript(script: string): { meta: WorkflowMeta; body: string } { const text = script.trimStart(); const lead = script.length - text.length; // chars trimmed, to map indices back if (DETERMINISM_BLOCKLIST.test(text)) { throw new Error( "Workflow scripts must be deterministic: Date.now() / Math.random() / argless new Date() are unavailable (they break resume). Pass timestamps via args and vary randomness by index.", ); } const m = META_PREFIX.exec(text); if (!m) { throw new Error("`export const meta = { name, description, phases? }` must be the first statement in the script."); } // Locate the object literal that follows `=`. let i = m[0].length; while (i < text.length && /\s/.test(text[i])) i++; if (text[i] !== "{") { throw new Error("meta must be assigned a literal object: `export const meta = { ... }`."); } const objEnd = matchBrace(text, i); if (objEnd === -1) throw new Error("meta object literal is not closed (unbalanced braces)."); const objText = text.slice(i, objEnd + 1); // Evaluate as a literal in an EMPTY realm: a pure literal succeeds; anything // referencing a variable / calling a function / interpolating throws. let meta: WorkflowMeta; try { meta = vm.runInNewContext(`(${objText})`, Object.create(null), { timeout: 1000, displayErrors: false, }) as WorkflowMeta; } catch (err) { throw new Error( `meta must be a PURE LITERAL — no variables, function calls, spreads, or template interpolation. (${(err as Error).message})`, ); } validateMeta(meta); // Strip the whole `export const meta = {...}` statement (plus an optional // trailing semicolon) from the original script to get the runnable body. let after = objEnd + 1; while (after < text.length && /[\s;]/.test(text[after]) && text[after] !== "\n") after++; if (text[after] === ";") after++; const body = script.slice(0, lead) + text.slice(after); return { meta, body }; } /** Index of the `}` matching the `{` at `open`, or -1. Skips strings/comments. */ function matchBrace(s: string, open: number): number { let depth = 0; let inStr: string | null = null; for (let i = open; i < s.length; i++) { const c = s[i]; if (inStr) { if (c === "\\") { i++; } else if (c === inStr) { inStr = null; } continue; } if (c === '"' || c === "'" || c === "`") { inStr = c; } else if (c === "/" && s[i + 1] === "/") { const nl = s.indexOf("\n", i); if (nl === -1) return -1; i = nl; } else if (c === "/" && s[i + 1] === "*") { const end = s.indexOf("*/", i + 2); if (end === -1) return -1; i = end + 1; } else if (c === "{") { depth++; } else if (c === "}") { depth--; if (depth === 0) return i; } } return -1; } export function validateMeta(meta: unknown): asserts meta is WorkflowMeta { if (!meta || typeof meta !== "object" || Array.isArray(meta)) { throw new Error("meta must be an object literal."); } const m = meta as Record; if (typeof m.name !== "string" || !m.name.trim()) { throw new Error("meta.name is required and must be a non-empty string."); } if (typeof m.description !== "string" || !m.description.trim()) { throw new Error("meta.description is required and must be a non-empty string."); } if (m.phases !== undefined) { if (!Array.isArray(m.phases)) throw new Error("meta.phases must be an array when present."); for (const p of m.phases) { if (!p || typeof p !== "object" || typeof (p as { title?: unknown }).title !== "string") { throw new Error("each meta.phases entry must be an object with a string `title`."); } const pe = (p as { effort?: unknown }).effort; if (pe !== undefined && !EFFORT_VALUES.includes(pe as WorkflowEffort)) { throw new Error( `meta.phases[].effort must be one of ${EFFORT_VALUES.join("|")} when present (got ${JSON.stringify(pe)}).`, ); } } } if (m.effort !== undefined && !EFFORT_VALUES.includes(m.effort as WorkflowEffort)) { throw new Error( `meta.effort must be one of ${EFFORT_VALUES.join("|")} when present (got ${JSON.stringify(m.effort)}).`, ); } }