@@ -8,7 +8,6 @@ import { sha256Hex } from '@sim/security/hash'
88import { getErrorMessage } from '@sim/utils/errors'
99import { generateShortId } from '@sim/utils/id'
1010import { toRecord } from '@sim/utils/object'
11- import { escapeRegExp } from '@sim/utils/string'
1211import { NextResponse } from 'next/server'
1312import type { ParsedFunctionExecuteBody } from '@/lib/api/contracts'
1413import { isMothershipSandboxEnabled , isRemoteSandboxEnabled } from '@/lib/core/config/env-flags'
@@ -87,6 +86,7 @@ import {
8786} from '@/lib/execution/remote-sandbox/sandbox-paths'
8887import type { SandboxCollectedFile , SandboxFile } from '@/lib/execution/remote-sandbox/types'
8988import { isExecutionResourceLimitError } from '@/lib/execution/resource-errors'
89+ import { MAX_FUNCTION_REFERENCES } from '@/lib/function-execution/limits'
9090import type { SandboxExportedFile } from '@/lib/function-execution/output'
9191import { planUserFileMounts , resolveUserFileMounts } from '@/lib/function-execution/sandbox-mounts'
9292import {
@@ -750,38 +750,33 @@ function scrubInternalIdentifiers(message: string, identifiers: readonly string[
750750
751751function resolveWorkflowVariables (
752752 code : string ,
753- workflowVariables : Record < string , any > ,
754- contextVariables : Record < string , any >
753+ workflowVariables : Record < string , unknown > ,
754+ contextVariables : Record < string , unknown >
755755) : string {
756- let resolvedCode = code
757-
758- const regex = createWorkflowVariablePattern ( )
759- let match : RegExpExecArray | null
760- const replacements : Array < {
761- match : string
762- index : number
763- variableName : string
764- variableValue : unknown
765- } > = [ ]
766-
767- while ( ( match = regex . exec ( code ) ) !== null ) {
768- const variableName = match [ 1 ] . trim ( )
769-
770- const foundVariable = Object . entries ( workflowVariables ) . find (
771- ( [ _ , variable ] ) => normalizeName ( variable . name || '' ) === variableName
772- )
773-
774- if ( ! foundVariable ) {
775- const availableVars = Object . values ( workflowVariables )
776- . map ( ( v ) => v . name )
777- . filter ( Boolean )
756+ const variablesByName = new Map < string , Record < string , unknown > > ( )
757+ for ( const value of Object . values ( workflowVariables ) ) {
758+ const variable = toRecord ( value )
759+ if ( typeof variable . name !== 'string' ) continue
760+ const name = normalizeName ( variable . name )
761+ if ( ! variablesByName . has ( name ) ) variablesByName . set ( name , variable )
762+ }
763+ const replacements = new Map < string , string > ( )
764+ const boundNames = new Set < string > ( )
765+
766+ return code . replace ( createWorkflowVariablePattern ( ) , ( _match , name : string ) => {
767+ const variableName = name . trim ( )
768+ const cached = replacements . get ( variableName )
769+ if ( cached !== undefined ) return cached
770+
771+ const variable = variablesByName . get ( variableName )
772+ if ( ! variable ) {
773+ const availableVars = [ ...variablesByName . values ( ) ] . map ( ( value ) => value . name ) . filter ( Boolean )
778774 throw new Error (
779775 `Variable "${ variableName } " doesn't exist.` +
780776 ( availableVars . length > 0 ? ` Available: ${ availableVars . join ( ', ' ) } ` : '' )
781777 )
782778 }
783779
784- const variable = foundVariable [ 1 ]
785780 let variableValue : unknown = variable . value
786781
787782 if ( variable . value !== undefined && variable . value !== null ) {
@@ -805,24 +800,15 @@ function resolveWorkflowVariables(
805800 }
806801 }
807802
808- replacements . push ( {
809- match : match [ 0 ] ,
810- index : match . index ,
811- variableName,
812- variableValue,
813- } )
814- }
815-
816- for ( let i = replacements . length - 1 ; i >= 0 ; i -- ) {
817- const { match : matchStr , index, variableName, variableValue } = replacements [ i ]
818-
819803 const safeVarName = `__variable_${ variableName . replace ( / [ ^ a - z A - Z 0 - 9 _ ] / g, '_' ) } `
820- contextVariables [ safeVarName ] = variableValue
821- resolvedCode =
822- resolvedCode . slice ( 0 , index ) + safeVarName + resolvedCode . slice ( index + matchStr . length )
823- }
824-
825- return resolvedCode
804+ // The original reverse rewrite gave the first reference precedence on binding-name collisions.
805+ if ( ! boundNames . has ( safeVarName ) ) {
806+ contextVariables [ safeVarName ] = variableValue
807+ boundNames . add ( safeVarName )
808+ }
809+ replacements . set ( variableName , safeVarName )
810+ return safeVarName
811+ } )
826812}
827813
828814/**
@@ -869,13 +855,12 @@ function resolveTagVariables(
869855 contextVariables : Record < string , unknown > ,
870856 language = 'javascript'
871857) : string {
872- let resolvedCode = code
873858 const undefinedLiteral = language === 'python' ? 'None' : 'undefined'
859+ const replacements = new Map < string , string | undefined > ( )
874860
875- const tagMatches = resolvedCode . match ( TAG_PATTERN ) || [ ]
876-
877- for ( const match of tagMatches ) {
861+ return code . replace ( TAG_PATTERN , ( match ) => {
878862 const tagName = match . slice ( REFERENCE . START . length , - REFERENCE . END . length ) . trim ( )
863+ if ( replacements . has ( tagName ) ) return replacements . get ( tagName ) ?? match
879864 const pathParts = tagName . split ( REFERENCE . PATH_DELIMITER )
880865 const blockName = pathParts [ 0 ]
881866 const fieldPath = pathParts . slice ( 1 )
@@ -887,14 +872,15 @@ function resolveTagVariables(
887872 } )
888873
889874 if ( ! result ) {
890- continue
875+ replacements . set ( tagName , undefined )
876+ return match
891877 }
892878
893879 let tagValue = result . value
894880
895881 if ( tagValue === undefined ) {
896- resolvedCode = resolvedCode . replace ( new RegExp ( escapeRegExp ( match ) , 'g' ) , undefinedLiteral )
897- continue
882+ replacements . set ( tagName , undefinedLiteral )
883+ return undefinedLiteral
898884 }
899885
900886 if ( typeof tagValue === 'string' ) {
@@ -910,10 +896,9 @@ function resolveTagVariables(
910896
911897 const safeVarName = `__tag_${ tagName . replace ( / _ / g, '_1' ) . replace ( / \. / g, '_0' ) } `
912898 contextVariables [ safeVarName ] = tagValue
913- resolvedCode = resolvedCode . replace ( new RegExp ( escapeRegExp ( match ) , 'g' ) , safeVarName )
914- }
915-
916- return resolvedCode
899+ replacements . set ( tagName , safeVarName )
900+ return safeVarName
901+ } )
917902}
918903
919904/**
@@ -2281,6 +2266,23 @@ export async function executeFunctionRequest(
22812266 )
22822267 includePrivateResolvedSecretNames = privateResolvedSecretNamesMetadataType !== undefined
22832268
2269+ let referenceCount = 0
2270+ for ( const _match of body . code . matchAll ( TAG_PATTERN ) ) {
2271+ if ( ++ referenceCount > MAX_FUNCTION_REFERENCES ) {
2272+ return appendPrivateResolvedSecretNames (
2273+ NextResponse . json (
2274+ {
2275+ success : false ,
2276+ error : `Function code exceeds the maximum of ${ MAX_FUNCTION_REFERENCES } references` ,
2277+ } ,
2278+ { status : 400 }
2279+ ) ,
2280+ includePrivateResolvedSecretNames ? [ ] : null ,
2281+ privateResolvedSecretNamesMetadataType
2282+ )
2283+ }
2284+ }
2285+
22842286 const mountedWorkspaceFileProvenance = inspectMountedWorkspaceFileProvenance ( req . headers , body )
22852287 if ( mountedWorkspaceFileProvenance . status === 'invalid' ) {
22862288 return appendPrivateResolvedSecretNames (
0 commit comments