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.
86 lines
4.2 KiB
TypeScript
86 lines
4.2 KiB
TypeScript
/**
|
|
* Meta validation: check the caller-provided {@link WorkflowMeta} DATA against
|
|
* the shape contract and reject everything else loud, every violation named.
|
|
* Meta arrives as plain JSON through the seam (the model-facing tool carries
|
|
* it as a schema-validated object parameter) — the engine never evaluates
|
|
* script text to obtain it, so no script-controlled code can run on the host
|
|
* here (an evaluated meta literal could smuggle getters that spin the host
|
|
* outside any vm timeout, the exact escape the worker thread exists to
|
|
* prevent).
|
|
*
|
|
* @module @deepseek-ai/dsh-workflow-workerthread/meta
|
|
*/
|
|
|
|
import { WorkflowError } from '@deepseek-ai/dsh-workflow'
|
|
import type { WorkflowMeta, WorkflowPhase } from '@deepseek-ai/dsh-workflow'
|
|
|
|
/** Collect shape violations for a meta value (plain JSON data by the seam contract). */
|
|
function validateMetaShape(meta: unknown): { meta?: WorkflowMeta; violations: string[] } {
|
|
const violations: string[] = []
|
|
if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) {
|
|
return { violations: ['meta must be an object'] }
|
|
}
|
|
const record = meta as Record<string, unknown>
|
|
const known = new Set(['name', 'description', 'whenToUse', 'phases'])
|
|
for (const key of Object.keys(record)) {
|
|
if (!known.has(key)) violations.push(`meta.${key} is not a recognized field (name/description/whenToUse/phases)`)
|
|
}
|
|
if (typeof record.name !== 'string' || record.name.length === 0) violations.push('meta.name must be a non-empty string')
|
|
if (typeof record.description !== 'string' || record.description.length === 0) violations.push('meta.description must be a non-empty string')
|
|
if (record.whenToUse !== undefined && typeof record.whenToUse !== 'string') violations.push('meta.whenToUse must be a string')
|
|
const phases: WorkflowPhase[] = []
|
|
if (record.phases !== undefined) {
|
|
if (!Array.isArray(record.phases)) {
|
|
violations.push('meta.phases must be an array')
|
|
} else {
|
|
record.phases.forEach((phase, index) => {
|
|
if (typeof phase !== 'object' || phase === null || Array.isArray(phase)) {
|
|
violations.push(`meta.phases[${index}] must be an object`)
|
|
return
|
|
}
|
|
const entry = phase as Record<string, unknown>
|
|
for (const key of Object.keys(entry)) {
|
|
if (!['title', 'detail', 'model'].includes(key)) violations.push(`meta.phases[${index}].${key} is not a recognized field`)
|
|
}
|
|
if (typeof entry.title !== 'string' || entry.title.length === 0) violations.push(`meta.phases[${index}].title must be a non-empty string`)
|
|
if (entry.detail !== undefined && typeof entry.detail !== 'string') violations.push(`meta.phases[${index}].detail must be a string`)
|
|
if (entry.model !== undefined && typeof entry.model !== 'string') violations.push(`meta.phases[${index}].model must be a string`)
|
|
if (violations.length === 0) {
|
|
phases.push({
|
|
title: entry.title as string,
|
|
...entry.detail !== undefined ? { detail: entry.detail as string } : {},
|
|
...entry.model !== undefined ? { model: entry.model as string } : {},
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
if (violations.length > 0) return { violations }
|
|
return {
|
|
violations,
|
|
meta: {
|
|
name: record.name as string,
|
|
description: record.description as string,
|
|
...record.whenToUse !== undefined ? { whenToUse: record.whenToUse as string } : {},
|
|
...record.phases !== undefined ? { phases } : {},
|
|
},
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate a caller-provided meta value against the {@link WorkflowMeta}
|
|
* contract. Throws `META_INVALID` naming every violation (unknown fields,
|
|
* missing/mistyped `name`/`description`, malformed `phases`); the returned
|
|
* meta is a NORMALIZED copy built from the validated fields, so the engine
|
|
* never aliases the caller's object.
|
|
* @param value - the meta data from the start request (plain JSON by the seam contract).
|
|
* @returns the validated, normalized meta block.
|
|
*/
|
|
export function validateMeta(value: unknown): WorkflowMeta {
|
|
const { meta, violations } = validateMetaShape(value)
|
|
if (meta === undefined) {
|
|
throw new WorkflowError(`invalid meta: ${violations.join('; ')}`, 'META_INVALID')
|
|
}
|
|
return meta
|
|
}
|