workflow: meta rides the seam as data — the engine never evaluates it
P1 review finding: extractMeta timed only the literal's vm evaluation;
materializing the RESULT then read properties ordinarily on the HOST
stack, so a meta literal smuggling a getter (get name() { while(true){} })
could wedge the host outside any timeout — defeating the exact spin
isolation the worker thread exists for.
Rather than harden the evaluator (descriptor walks, AST validation),
delete the mechanism: the workflow's identity now reaches the seam as a
plain JSON field (WorkflowStartRequest.meta), carried by the tool as a
schema-validated `meta` object parameter the model fills directly. The
engine only shape-validates data (validateMeta, every violation named)
and pre-parses the body; the scanner, the vm evaluation, and the
host-side materialization are gone, and with them the hole. A body
still opening with a Claude Code-style `export const meta` statement
gets a pointed SCRIPT_PARSE message (the likeliest authoring slip; a
CC script's body stays drop-in, only its meta header moves into the
parameter). syncTimeoutMs now governs exactly one thing: the initial
synchronous slice inside the worker.
The RFC's decision section is rewritten in place (implemented-RFC
rule); the embedded-meta format moves to alternatives-considered with
the hole as the reason. Tool description, presentation (title now reads
meta.name directly — the textual sniff is gone), seam vocabulary docs,
and catalogs follow.
This commit is contained in:
@@ -13,9 +13,8 @@
|
||||
* collection is deferred to the cross-tool background redesign.
|
||||
*
|
||||
* Render intent (decided up front, per the render-intent RFC): a `generic`
|
||||
* card whose title carries the script's `meta.name`, sniffed textually from
|
||||
* the args — presentation must be a pure function of `args`, so it cannot ask
|
||||
* the engine to parse.
|
||||
* card whose title carries the workflow's `meta.name`, read directly from the
|
||||
* call's `meta` parameter — presentation is a pure function of `args`.
|
||||
*
|
||||
* Usage policy ships with the tool as a `tool:<toolName>` system-prompt
|
||||
* section (explicit-ask-only guidance) — tool guidance lives in tool plugins,
|
||||
@@ -58,7 +57,7 @@ type ResolvedConfig = Required<Config>
|
||||
*/
|
||||
const DESCRIPTION = `Run a JavaScript workflow script that orchestrates subagents at scale. Use this for work that fans out across many independent pieces — an audit over many files, a migration, multi-angle research, adversarial verification of findings — where you write the orchestration as a script instead of delegating turn by turn.
|
||||
|
||||
The script MUST begin with \`export const meta = {...}\` — a PURE object literal (no variables, calls, or template interpolation) with required \`name\` (short kebab-case) and \`description\` strings, optional \`whenToUse\` string and \`phases\` array (\`{title, detail?, model?}\`). The body after it is plain JavaScript (NOT TypeScript) running with top-level await; end with \`return <value>\` — the value must be JSON-serializable and is this tool's result.
|
||||
The workflow's identity rides the \`meta\` parameter as JSON: required \`name\` (short kebab-case) and \`description\` strings, optional \`whenToUse\` string and \`phases\` array (\`{title, detail?, model?}\`). The \`script\` parameter is the plain JavaScript body ONLY (NOT TypeScript, and NO \`export const meta\` statement — meta is a parameter, not code), running with top-level await; end with \`return <value>\` — the value must be JSON-serializable and is this tool's result.
|
||||
|
||||
Script-body hooks:
|
||||
- \`agent(prompt, opts?): Promise<any>\` — run one subagent to completion. Without \`opts.schema\` it resolves to the child's final text; with \`opts.schema\` (an object-rooted JSON Schema using ONLY type/properties/required/additionalProperties/items/enum/const — no oneOf/pattern/format/numeric bounds) it resolves to the validated object. Resolves \`null\` when the child fails (filter with \`.filter(Boolean)\`). Other opts: \`label\` (display), \`phase\` (progress group), \`model\` (override). Anything else (\`effort\`/\`isolation\`/\`agentType\`) is rejected loudly.
|
||||
@@ -70,20 +69,17 @@ Misused hooks (bad arguments, unknown options, unsupported schemas, tripped caps
|
||||
|
||||
Constraints: concurrency and total-agent caps apply; no filesystem, network, timers, or Node.js APIs are provided — the agents do the work, the script only coordinates them. The run executes in the foreground: this call returns when the whole script finishes.`
|
||||
|
||||
type WorkflowCallArgs = { script: string; args?: Record<string, unknown> }
|
||||
|
||||
/** Best-effort meta.name sniff for presentation (pure textual; no evaluation). */
|
||||
function sniffMetaName(script: string): string | undefined {
|
||||
const match = /export\s+const\s+meta\s*=\s*\{[^{}]*?name\s*:\s*(['"`])([^'"`\n]{1,64})\1/.exec(script)
|
||||
return match?.[2]
|
||||
type WorkflowCallArgs = {
|
||||
script: string
|
||||
meta: { name: string; description: string; whenToUse?: string; phases?: { title: string; detail?: string; model?: string }[] }
|
||||
args?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/** The pending-state card: a generic card titled by the script's meta name. */
|
||||
/** The pending-state card: a generic card titled by the workflow's meta name. */
|
||||
function presentWorkflowCall(args: WorkflowCallArgs): ToolCallView {
|
||||
const name = sniffMetaName(args.script)
|
||||
return {
|
||||
card: 'generic',
|
||||
title: name !== undefined ? `workflow: ${name}` : 'workflow',
|
||||
title: `workflow: ${args.meta.name}`,
|
||||
rawInput: args.script,
|
||||
}
|
||||
}
|
||||
@@ -139,7 +135,29 @@ export function apply(ctx: Context, config: Config): void {
|
||||
script: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'The complete workflow script: `export const meta = {...}` followed by the plain-JS body (top-level await allowed; end with `return <json-value>`).',
|
||||
description: 'The plain-JS workflow script body (top-level await allowed; NO `export const meta` statement; end with `return <json-value>`).',
|
||||
},
|
||||
meta: {
|
||||
type: 'object',
|
||||
required: true,
|
||||
description: 'The workflow identity block (plain JSON — never code).',
|
||||
properties: {
|
||||
name: { type: 'string', required: true, description: 'Short kebab-case workflow name.' },
|
||||
description: { type: 'string', required: true, description: 'One-line description of what the workflow does.' },
|
||||
whenToUse: { type: 'string', description: 'Optional guidance on when this workflow applies.' },
|
||||
phases: {
|
||||
type: 'array',
|
||||
description: 'Optional phase declarations matched by phase() calls.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string', required: true, description: 'The phase title phase() calls match by exact string.' },
|
||||
detail: { type: 'string', description: 'Optional one-line description of the phase.' },
|
||||
model: { type: 'string', description: 'Optional model override this phase is expected to use.' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
type: 'object',
|
||||
@@ -155,11 +173,12 @@ export function apply(ctx: Context, config: Config): void {
|
||||
throw new Error('workflow tool requires a calling agent (exec.agent was undefined)')
|
||||
}
|
||||
|
||||
// Parse failures (SCRIPT_PARSE/META_INVALID) throw synchronously here
|
||||
// and become isError results via the registry — the model sees the
|
||||
// violation list and can correct the script.
|
||||
// Meta/body validation failures (META_INVALID/SCRIPT_PARSE) throw
|
||||
// synchronously here and become isError results via the registry — the
|
||||
// model sees the violation list and can correct the call.
|
||||
const run: WorkflowRun = ctx.workflows.start({
|
||||
script: args.script,
|
||||
meta: args.meta,
|
||||
...args.args !== undefined ? { args: args.args } : {},
|
||||
parent,
|
||||
...exec.signal ? { signal: exec.signal } : {},
|
||||
|
||||
Reference in New Issue
Block a user