@@ -48,14 +48,6 @@ export function parseWorkflowScript(
4848 // Strip only the leading `export ` so line numbers stay aligned (7 spaces).
4949 const body = normalized . replace ( META_EXPORT , " const meta =" ) ;
5050
51- // Reject further imports / exports after transform
52- if ( / \b i m p o r t \s + / . test ( body ) || / \b e x p o r t \s + / . test ( body ) ) {
53- throw new WorkflowScriptError (
54- "syntax" ,
55- "Workflow scripts may not use import or additional export statements" ,
56- ) ;
57- }
58-
5951 // Workflow APIs are installed as context-realm globals by the sandbox child.
6052 // Keeping the factory argument-free avoids handing host-realm functions or
6153 // constructors directly to model-authored workflow code.
@@ -114,27 +106,31 @@ function extractMetaLiteral(source: string): { metaLiteral: string; metaEndIndex
114106 const end = scanBalancedObject ( source , objectStart ) ;
115107 const metaLiteral = source . slice ( objectStart , end + 1 ) ;
116108
117- // Purity: no calls, spreads, templates inside meta (rough static checks)
118- if ( / [ ` $ ] / . test ( metaLiteral ) && / \$ \{ / . test ( metaLiteral ) ) {
119- throw new WorkflowScriptError ( "meta" , "meta must be a pure literal (no template interpolation)" ) ;
120- }
121- if ( / \. \. \. / . test ( metaLiteral ) ) {
122- throw new WorkflowScriptError ( "meta" , "meta must be a pure literal (no spreads)" ) ;
123- }
124- // Disallow identifier references that look like calls: word(
125- if ( / \b [ A - Z a - z _ $ ] [ \w $ ] * \s * \( / . test ( metaLiteral ) ) {
126- throw new WorkflowScriptError ( "meta" , "meta must be a pure literal (no function calls)" ) ;
127- }
109+ assertPureMetaLiteral ( metaLiteral ) ;
128110
129111 return { metaLiteral, metaEndIndex : end + 1 } ;
130112}
131113
132114function scanBalancedObject ( source : string , start : number ) : number {
133115 let depth = 0 ;
134116 let inString : '"' | "'" | null = null ;
117+ let inLineComment = false ;
118+ let inBlockComment = false ;
135119 let escape = false ;
136120 for ( let i = start ; i < source . length ; i += 1 ) {
137121 const ch = source [ i ] ! ;
122+ const next = source [ i + 1 ] ;
123+ if ( inLineComment ) {
124+ if ( ch === "\n" ) inLineComment = false ;
125+ continue ;
126+ }
127+ if ( inBlockComment ) {
128+ if ( ch === "*" && next === "/" ) {
129+ inBlockComment = false ;
130+ i += 1 ;
131+ }
132+ continue ;
133+ }
138134 if ( inString ) {
139135 if ( escape ) {
140136 escape = false ;
@@ -147,6 +143,16 @@ function scanBalancedObject(source: string, start: number): number {
147143 if ( ch === inString ) inString = null ;
148144 continue ;
149145 }
146+ if ( ch === "/" && next === "/" ) {
147+ inLineComment = true ;
148+ i += 1 ;
149+ continue ;
150+ }
151+ if ( ch === "/" && next === "*" ) {
152+ inBlockComment = true ;
153+ i += 1 ;
154+ continue ;
155+ }
150156 if ( ch === '"' || ch === "'" ) {
151157 inString = ch ;
152158 continue ;
@@ -160,6 +166,94 @@ function scanBalancedObject(source: string, start: number): number {
160166 throw new WorkflowScriptError ( "meta" , "Unclosed meta object literal" ) ;
161167}
162168
169+ function assertPureMetaLiteral ( literal : string ) : void {
170+ for ( let i = 0 ; i < literal . length ; i += 1 ) {
171+ const ch = literal [ i ] ! ;
172+ const next = literal [ i + 1 ] ;
173+ if ( ch === '"' || ch === "'" ) {
174+ i = skipQuoted ( literal , i , ch ) ;
175+ continue ;
176+ }
177+ if ( ch === "/" && next === "/" ) {
178+ i = skipLineComment ( literal , i + 2 ) ;
179+ continue ;
180+ }
181+ if ( ch === "/" && next === "*" ) {
182+ i = skipBlockComment ( literal , i + 2 ) ;
183+ continue ;
184+ }
185+ if ( ch === "`" ) {
186+ throw new WorkflowScriptError ( "meta" , "meta must be a pure literal (no templates)" ) ;
187+ }
188+ if ( literal . startsWith ( "..." , i ) ) {
189+ throw new WorkflowScriptError ( "meta" , "meta must be a pure literal (no spreads)" ) ;
190+ }
191+ if ( literal . startsWith ( "=>" , i ) ) {
192+ throw new WorkflowScriptError ( "meta" , "meta must be a pure literal (no functions)" ) ;
193+ }
194+ if ( ! / [ A - Z a - z _ $ ] / . test ( ch ) ) continue ;
195+
196+ const identifierStart = i ;
197+ i += 1 ;
198+ while ( i < literal . length && / [ \w $ ] / . test ( literal [ i ] ! ) ) i += 1 ;
199+ const identifier = literal . slice ( identifierStart , i ) ;
200+ if ( identifier === "function" || identifier === "class" || identifier === "new" ) {
201+ throw new WorkflowScriptError ( "meta" , "meta must be a pure literal (no executable values)" ) ;
202+ }
203+ i = skipTrivia ( literal , i ) - 1 ;
204+ if ( literal [ i + 1 ] === "(" ) {
205+ throw new WorkflowScriptError ( "meta" , "meta must be a pure literal (no function calls)" ) ;
206+ }
207+ }
208+ }
209+
210+ function skipQuoted ( source : string , start : number , quote : '"' | "'" ) : number {
211+ let escape = false ;
212+ for ( let i = start + 1 ; i < source . length ; i += 1 ) {
213+ const ch = source [ i ] ! ;
214+ if ( escape ) {
215+ escape = false ;
216+ continue ;
217+ }
218+ if ( ch === "\\" ) {
219+ escape = true ;
220+ continue ;
221+ }
222+ if ( ch === quote ) return i ;
223+ }
224+ return source . length - 1 ;
225+ }
226+
227+ function skipLineComment ( source : string , start : number ) : number {
228+ const newline = source . indexOf ( "\n" , start ) ;
229+ return newline < 0 ? source . length - 1 : newline ;
230+ }
231+
232+ function skipBlockComment ( source : string , start : number ) : number {
233+ const end = source . indexOf ( "*/" , start ) ;
234+ return end < 0 ? source . length - 1 : end + 1 ;
235+ }
236+
237+ function skipTrivia ( source : string , start : number ) : number {
238+ let i = start ;
239+ while ( i < source . length ) {
240+ if ( / \s / . test ( source [ i ] ! ) ) {
241+ i += 1 ;
242+ continue ;
243+ }
244+ if ( source . startsWith ( "//" , i ) ) {
245+ i = skipLineComment ( source , i + 2 ) + 1 ;
246+ continue ;
247+ }
248+ if ( source . startsWith ( "/*" , i ) ) {
249+ i = skipBlockComment ( source , i + 2 ) + 1 ;
250+ continue ;
251+ }
252+ break ;
253+ }
254+ return i ;
255+ }
256+
163257function isOnlyPreamble ( text : string ) : boolean {
164258 // strip block comments, line comments, whitespace
165259 const stripped = text
@@ -185,7 +279,7 @@ function evaluateMetaLiteral(literal: string, filename: string): unknown {
185279}
186280
187281function validateMeta ( value : unknown ) : WorkflowMeta {
188- const parsed = workflowMetaSchema . safeParse ( value ) ;
282+ const parsed = workflowMetaSchema . safeParse ( value , { reportInput : true } ) ;
189283 if ( parsed . success ) return parsed . data ;
190284
191285 const issue = parsed . error . issues [ 0 ] ;
0 commit comments