jsfe 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1214,6 +1214,7 @@ function evaluateCondition(data, condition) {
1214
1214
  if (!condition || typeof condition !== 'object')
1215
1215
  return false;
1216
1216
  const { field, operator, value } = condition;
1217
+ logger.debug(`evaluateCondition called with field: ${field}, operator: ${operator}, value: ${value}`);
1217
1218
  try {
1218
1219
  let fieldValue = undefined;
1219
1220
  if (isPathTraversableObject(data)) {
@@ -1222,44 +1223,61 @@ function evaluateCondition(data, condition) {
1222
1223
  switch (operator) {
1223
1224
  case 'equals':
1224
1225
  case 'eq':
1226
+ logger.debug(`evaluateCondition: checking equality for field ${field} with value ${value}`);
1225
1227
  return fieldValue === value;
1226
1228
  case 'notEquals':
1227
1229
  case 'ne':
1230
+ logger.debug(`evaluateCondition: checking inequality for field ${field} with value ${value}`);
1228
1231
  return fieldValue !== value;
1229
1232
  case 'contains':
1233
+ logger.debug(`evaluateCondition: checking containment for field ${field} with value ${value}`);
1230
1234
  return String(fieldValue).includes(String(value));
1231
1235
  case 'exists':
1236
+ logger.debug(`evaluateCondition: checking existence for field ${field}`);
1232
1237
  return fieldValue !== null && fieldValue !== undefined;
1233
1238
  case 'notExists':
1239
+ logger.debug(`evaluateCondition: checking non-existence for field ${field}`);
1234
1240
  return fieldValue === null || fieldValue === undefined;
1235
1241
  case 'greaterThan':
1236
1242
  case 'gt':
1243
+ logger.debug(`evaluateCondition: checking greaterThan for field ${field} with value ${value}`);
1237
1244
  return Number(fieldValue) > Number(value);
1238
1245
  case 'lessThan':
1239
1246
  case 'lt':
1247
+ logger.debug(`evaluateCondition: checking lessThan for field ${field} with value ${value}`);
1240
1248
  return Number(fieldValue) < Number(value);
1241
1249
  case 'greaterThanOrEqual':
1242
1250
  case 'gte':
1251
+ logger.debug(`evaluateCondition: checking greaterThanOrEqual for field ${field} with value ${value}`);
1243
1252
  return Number(fieldValue) >= Number(value);
1244
1253
  case 'lessThanOrEqual':
1245
1254
  case 'lte':
1255
+ logger.debug(`evaluateCondition: checking lessThanOrEqual for field ${field} with value ${value}`);
1246
1256
  return Number(fieldValue) <= Number(value);
1247
1257
  case 'startsWith':
1258
+ logger.debug(`evaluateCondition: checking startsWith for field ${field} with value ${value}`);
1248
1259
  return String(fieldValue).startsWith(String(value));
1249
1260
  case 'endsWith':
1261
+ logger.debug(`evaluateCondition: checking endsWith for field ${field} with value ${value}`);
1250
1262
  return String(fieldValue).endsWith(String(value));
1251
1263
  case 'matches':
1264
+ logger.debug(`evaluateCondition: checking regex match for field ${field} with value ${value}`);
1252
1265
  return new RegExp(String(value)).test(String(fieldValue));
1253
1266
  case 'in':
1267
+ logger.debug(`evaluateCondition: checking inclusion for field ${field} with value ${value}`);
1254
1268
  return Array.isArray(value) && value.includes(fieldValue);
1255
1269
  case 'hasLength':
1256
1270
  const length = Array.isArray(fieldValue) ? fieldValue.length : String(fieldValue).length;
1271
+ logger.debug(`evaluateCondition: checking length for field ${field} with value ${value}, actual length: ${length}`);
1257
1272
  return length === Number(value);
1258
1273
  case 'isArray':
1274
+ logger.debug(`evaluateCondition: checking if field ${field} is an array`);
1259
1275
  return Array.isArray(fieldValue);
1260
1276
  case 'isObject':
1277
+ logger.debug(`evaluateCondition: checking if field ${field} is an object`);
1261
1278
  return typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue);
1262
1279
  case 'isString':
1280
+ logger.debug(`evaluateCondition: checking if field ${field} is a string`);
1263
1281
  return typeof fieldValue === 'string';
1264
1282
  case 'isNumber':
1265
1283
  return typeof fieldValue === 'number' && !isNaN(fieldValue);
@@ -1284,14 +1302,19 @@ function interpolateObject(obj, data, args = {}, engine) {
1284
1302
  let value = undefined;
1285
1303
  if (isPathTraversableObject(data)) {
1286
1304
  value = extractByPath(data, path);
1305
+ // Handle user input wrapper objects
1306
+ if (isUserInputVariable(value)) {
1307
+ value = value.value;
1308
+ }
1287
1309
  }
1288
- // If not found in data and engine is available, check engine session variables
1310
+ // If not found in data and engine is available, use the new unified resolver
1289
1311
  if ((value === undefined || value === null)) {
1290
1312
  try {
1291
- value = resolveEngineSessionVariable(path, engine);
1313
+ // Use the new resolveVariablePath function which handles all variable types
1314
+ value = resolveVariablePath(path, args, [], engine);
1292
1315
  }
1293
1316
  catch (error) {
1294
- // Ignore engine session variable resolution errors
1317
+ // Ignore variable resolution errors
1295
1318
  }
1296
1319
  }
1297
1320
  return value !== undefined && value !== null ? value : '';
@@ -1306,14 +1329,19 @@ function interpolateObject(obj, data, args = {}, engine) {
1306
1329
  let value = undefined;
1307
1330
  if (isPathTraversableObject(data)) {
1308
1331
  value = extractByPath(data, path.trim());
1332
+ // Handle user input wrapper objects
1333
+ if (isUserInputVariable(value)) {
1334
+ value = value.value;
1335
+ }
1309
1336
  }
1310
- // If not found in data and engine is available, check engine session variables
1337
+ // If not found in data and engine is available, use the new unified resolver
1311
1338
  if ((value === undefined || value === null)) {
1312
1339
  try {
1313
- value = resolveEngineSessionVariable(path.trim(), engine);
1340
+ // Use the new resolveVariablePath function which handles all variable types
1341
+ value = resolveVariablePath(path.trim(), args, [], engine);
1314
1342
  }
1315
1343
  catch (error) {
1316
- // Ignore engine session variable resolution errors
1344
+ // Ignore variable resolution errors
1317
1345
  }
1318
1346
  }
1319
1347
  return value !== null && value !== undefined ? String(value) : '';
@@ -1501,11 +1529,8 @@ function checkRateLimit(engine, userId, toolId) {
1501
1529
  function sanitizeInput(input) {
1502
1530
  if (typeof input !== 'string')
1503
1531
  return input;
1504
- // Basic HTML escape and trim
1505
- return input.trim().replace(/[<>'"&]/g, (char) => {
1506
- const escapeMap = { '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '&': '&amp;' };
1507
- return escapeMap[char];
1508
- });
1532
+ // Simple trim - no HTML encoding since user input is treated as literal data
1533
+ return input.trim();
1509
1534
  }
1510
1535
  function validateToolArgs(tool, args) {
1511
1536
  if (!tool.parameters)
@@ -1807,10 +1832,11 @@ async function playFlowFrame(engine) {
1807
1832
  delete currentFlowFrame.pendingVariable;
1808
1833
  }
1809
1834
  else {
1810
- // Store user input as variable value
1835
+ // Store user input as variable value with proper sanitization
1811
1836
  // (System commands like 'cancel' are already handled before this point)
1812
- currentFlowFrame.variables[currentFlowFrame.pendingVariable] = userInput;
1813
- logger.info(`Stored user input in variable '${currentFlowFrame.pendingVariable}': "${userInput}"`);
1837
+ setUserInputVariable(currentFlowFrame.variables, currentFlowFrame.pendingVariable, userInput, true // Sanitize user input
1838
+ );
1839
+ logger.info(`Stored sanitized user input in variable '${currentFlowFrame.pendingVariable}': "${userInput}"`);
1814
1840
  delete currentFlowFrame.pendingVariable;
1815
1841
  // Pop the SAY-GET step now that variable assignment is complete
1816
1842
  currentFlowFrame.flowStepsStack.pop();
@@ -2124,7 +2150,7 @@ async function getFlowForInput(input, engine) {
2124
2150
  // === SMART DEFAULT ONFAIL GENERATOR ===
2125
2151
  function generateSmartRetryDefaultOnFail(step, error, currentFlowFrame) {
2126
2152
  const toolName = step.tool || 'unknown';
2127
- const errorMessage = error.message || 'Unknown error';
2153
+ const errorMessage = error.message.toLocaleLowerCase() || 'unknown error';
2128
2154
  const flowName = currentFlowFrame.flowName;
2129
2155
  logger.info(`Generating smart default onFail for tool ${toolName}, in flow ${flowName} for error: ${errorMessage}`);
2130
2156
  // Categorize error types for intelligent handling
@@ -2152,7 +2178,8 @@ function generateSmartRetryDefaultOnFail(step, error, currentFlowFrame) {
2152
2178
  errorMessage.includes('bad request') ||
2153
2179
  errorMessage.includes('invalid request') ||
2154
2180
  errorMessage.includes('malformed request') ||
2155
- errorMessage.includes('syntax error');
2181
+ errorMessage.includes('syntax error') ||
2182
+ errorMessage.includes('rate limit');
2156
2183
  // Provoke cancelation of the current flow if unrecoverable
2157
2184
  const isAuthError = errorMessage.includes('401') || // Unauthorized
2158
2185
  errorMessage.includes('402') || // Payment Required
@@ -2178,7 +2205,7 @@ function generateSmartRetryDefaultOnFail(step, error, currentFlowFrame) {
2178
2205
  doCancel = true;
2179
2206
  }
2180
2207
  else {
2181
- logger.warn(`Unrecognized error type for tool ${toolName} in flow ${flowName}: ${errorMessage}`);
2208
+ logger.debug(`Unrecognized error type for tool ${toolName} in flow ${flowName}: ${errorMessage}`);
2182
2209
  // Default to Cancel for unexpected errors
2183
2210
  doCancel = true;
2184
2211
  }
@@ -2304,7 +2331,7 @@ async function performStepInputValidation(step, currentFlowFrame, engine) {
2304
2331
  // Custom validation function
2305
2332
  if (step.inputValidation.customValidator && engine.APPROVED_FUNCTIONS) {
2306
2333
  try {
2307
- const validator = engine.APPROVED_FUNCTIONS.get(step.inputValidation.customValidator);
2334
+ const validator = engine.APPROVED_FUNCTIONS[step.inputValidation.customValidator];
2308
2335
  if (typeof validator === 'function') {
2309
2336
  const customResult = await validator(currentFlowFrame.variables, currentFlowFrame);
2310
2337
  if (customResult && typeof customResult === 'object') {
@@ -2822,8 +2849,13 @@ function handleSetStep(currentFlowFrame, engine) {
2822
2849
  throw new Error(`SET step requires both 'variable' and 'value' attributes`);
2823
2850
  }
2824
2851
  // Support interpolation in SET values
2852
+ // Use JavaScript evaluation context for security - strings will be properly quoted
2825
2853
  const interpolatedValue = typeof step.value === 'string'
2826
- ? interpolateMessage(step.value, [], currentFlowFrame?.variables, engine)
2854
+ ? evaluateExpression(step.value, currentFlowFrame?.variables || {}, [], {
2855
+ securityLevel: 'basic',
2856
+ context: 'javascript-evaluation', // Force JavaScript context for SET values
2857
+ returnType: 'auto'
2858
+ }, engine)
2827
2859
  : step.value;
2828
2860
  if (currentFlowFrame && currentFlowFrame.variables !== undefined) {
2829
2861
  currentFlowFrame.variables[step.variable] = interpolatedValue;
@@ -2838,7 +2870,11 @@ async function handleSwitchStep(currentFlowFrame, engine) {
2838
2870
  throw new Error(`SWITCH step requires both 'variable' and 'branches' attributes`);
2839
2871
  }
2840
2872
  // Get the variable value to switch on
2841
- const switchValue = currentFlowFrame.variables ? currentFlowFrame.variables[step.variable] : undefined;
2873
+ let switchValue = currentFlowFrame.variables ? currentFlowFrame.variables[step.variable] : undefined;
2874
+ // Handle user input wrapper objects
2875
+ if (isUserInputVariable(switchValue)) {
2876
+ switchValue = switchValue.value;
2877
+ }
2842
2878
  logger.info(`SWITCH step: evaluating variable '${step.variable}' with value '${switchValue}'`);
2843
2879
  // Find the matching branch (exact value matching only)
2844
2880
  let selectedStep = null;
@@ -2938,7 +2974,7 @@ async function handleSubFlowStep(currentFlowFrame, engine) {
2938
2974
  const input = currentFlowFrame.inputStack[currentFlowFrame.inputStack.length - 1];
2939
2975
  const flowsMenu = engine.flowsMenu; // Access the global flows menu
2940
2976
  const subFlowName = step.value || step.name || step.nextFlow;
2941
- const subFlow = flowsMenu?.find(f => f.name === subFlowName);
2977
+ const subFlow = flowsMenu?.find(f => f.name === subFlowName || f.id === subFlowName);
2942
2978
  if (!subFlow) {
2943
2979
  return getSystemMessage(engine, 'subflow_not_found', { subFlowName });
2944
2980
  }
@@ -3212,6 +3248,11 @@ function generateEnhancedFallbackArgs(schema, input, flowFrame) {
3212
3248
  // Direct variable match
3213
3249
  if (flowFrame.variables[key] !== undefined) {
3214
3250
  let value = flowFrame.variables[key];
3251
+ // Handle user input wrapper objects
3252
+ if (isUserInputVariable(value)) {
3253
+ const userInputObj = value;
3254
+ value = userInputObj.value;
3255
+ }
3215
3256
  // Type conversion if needed
3216
3257
  if (prop.type === 'number' && typeof value === 'string') {
3217
3258
  const num = parseFloat(value);
@@ -3222,37 +3263,6 @@ function generateEnhancedFallbackArgs(schema, input, flowFrame) {
3222
3263
  args[key] = value;
3223
3264
  continue;
3224
3265
  }
3225
- // Smart matching for account numbers
3226
- if (key.includes('account')) {
3227
- const accountVar = Object.keys(flowFrame.variables).find(varName => varName.includes('account') || (typeof flowFrame.variables[varName] === 'object' &&
3228
- flowFrame.variables[varName] !== null &&
3229
- flowFrame.variables[varName]?.accountId));
3230
- if (accountVar) {
3231
- const accountData = flowFrame.variables[accountVar];
3232
- if (typeof accountData === 'object' && accountData !== null && 'accountId' in accountData) {
3233
- args[key] = accountData.accountId;
3234
- }
3235
- else if (typeof accountData === 'string') {
3236
- args[key] = accountData;
3237
- }
3238
- }
3239
- }
3240
- // Smart matching for amounts
3241
- if (key.includes('amount')) {
3242
- const amountVar = Object.keys(flowFrame.variables).find(varName => varName.includes('amount') || varName.includes('payment'));
3243
- if (amountVar) {
3244
- const amountData = flowFrame.variables[amountVar];
3245
- if (typeof amountData === 'number') {
3246
- args[key] = amountData;
3247
- }
3248
- else if (typeof amountData === 'string') {
3249
- const num = parseFloat(amountData);
3250
- if (!isNaN(num)) {
3251
- args[key] = num;
3252
- }
3253
- }
3254
- }
3255
- }
3256
3266
  }
3257
3267
  }
3258
3268
  // If enhanced fallback didn't find anything useful, try simple pattern matching
@@ -3315,7 +3325,7 @@ async function callTool(engine, tool, args, userId = 'anonymous', transactionId
3315
3325
  if (!APPROVED_FUNCTIONS) {
3316
3326
  throw new Error(`Approved functions registry not available`);
3317
3327
  }
3318
- const fn = APPROVED_FUNCTIONS.get(tool.implementation.function);
3328
+ const fn = APPROVED_FUNCTIONS[tool.implementation.function];
3319
3329
  if (!fn) {
3320
3330
  throw new Error(`Function "${tool.implementation.function}" not found in approved functions registry`);
3321
3331
  }
@@ -3885,10 +3895,115 @@ export async function portableHash(data, secret, algorithm = 'SHA-256', encoding
3885
3895
  return encoding === 'base64' ? arrayBufferToBase64(hash) : Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
3886
3896
  }
3887
3897
  }
3888
- // Unified expression evaluator - replaces both evaluateSafeExpression and evaluateSafeCondition
3898
+ /**
3899
+ * Resolve variable path like "cargo.callerId" or "user.profile.name"
3900
+ * Supports variables, context stack, engine session variables, and global variables
3901
+ */
3902
+ function resolveVariablePath(path, variables, contextStack, engine) {
3903
+ const parts = path.split('.');
3904
+ const rootVar = parts[0];
3905
+ // 1. Try to find in flow variables first
3906
+ let rootValue = variables[rootVar];
3907
+ // Handle user input variables as pure literals
3908
+ if (rootValue && typeof rootValue === 'object' && rootValue !== null) {
3909
+ const userInputObj = rootValue;
3910
+ if (userInputObj.__userInput === true && userInputObj.__literal === true) {
3911
+ // Extract the literal value and treat it as a normal variable
3912
+ // This allows normal property access like userInput.length
3913
+ rootValue = userInputObj.value;
3914
+ }
3915
+ else {
3916
+ logger.debug(`resolveVariablePath - Found user input object, but not a literal: ${JSON.stringify(userInputObj)}`);
3917
+ }
3918
+ }
3919
+ // 2. If not found in variables, try context stack
3920
+ if (rootValue === undefined && contextStack.length > 0) {
3921
+ for (const context of contextStack) {
3922
+ if (context && typeof context === 'object' && rootVar in context) {
3923
+ rootValue = context[rootVar];
3924
+ break;
3925
+ }
3926
+ }
3927
+ }
3928
+ // 3. If not found and engine available, try engine session variables
3929
+ if (rootValue === undefined && engine) {
3930
+ // Use the centralized session variable logic
3931
+ const sessionVars = getEngineSessionVariables(engine, contextStack);
3932
+ // Handle property access (e.g., cargo.someVar)
3933
+ if (path.includes('.')) {
3934
+ const parts = path.split('.');
3935
+ const baseVariable = parts[0];
3936
+ const propertyPath = parts.slice(1).join('.');
3937
+ // Get the base session variable
3938
+ const baseValue = sessionVars[baseVariable];
3939
+ // If we found a base value, navigate the remaining path
3940
+ if (baseValue !== undefined) {
3941
+ return resolveVariablePath(propertyPath, { root: baseValue }, [], engine);
3942
+ }
3943
+ }
3944
+ else {
3945
+ // Handle direct variable access (no dots)
3946
+ rootValue = sessionVars[path];
3947
+ }
3948
+ }
3949
+ // 4. If not found and engine available, try global variables
3950
+ if (rootValue === undefined && engine?.globalVariables) {
3951
+ rootValue = engine.globalVariables[rootVar];
3952
+ }
3953
+ // If still not found, return undefined
3954
+ if (rootValue === undefined) {
3955
+ return undefined;
3956
+ }
3957
+ // Navigate the remaining path safely
3958
+ let current = rootValue;
3959
+ for (let i = 1; i < parts.length; i++) {
3960
+ if (current === null || current === undefined) {
3961
+ return undefined;
3962
+ }
3963
+ if (typeof current === 'object' && current !== null) {
3964
+ current = current[parts[i]];
3965
+ }
3966
+ else {
3967
+ return undefined;
3968
+ }
3969
+ }
3970
+ return current;
3971
+ }
3972
+ /**
3973
+ * Check if a variable value is marked as user input (literal data)
3974
+ * User input should never be evaluated as code for security
3975
+ */
3976
+ function isUserInputVariable(value) {
3977
+ return (value !== null &&
3978
+ typeof value === 'object' &&
3979
+ value.__userInput === true &&
3980
+ value.__literal === true);
3981
+ }
3982
+ /**
3983
+ * Input sanitization for user-provided values
3984
+ * Escapes special characters that could break expression syntax
3985
+ * NOTE: This is secondary defense - primary security is treating user input as literal data
3986
+ */
3987
+ function sanitizeUserInput(input) {
3988
+ if (typeof input !== 'string') {
3989
+ return String(input);
3990
+ }
3991
+ // Escape special characters that could break expression syntax
3992
+ return input
3993
+ .replace(/\\/g, '\\\\') // Escape backslashes
3994
+ .replace(/'/g, "\\'") // Escape single quotes
3995
+ .replace(/"/g, '\\"') // Escape double quotes
3996
+ .replace(/\n/g, '\\n') // Escape newlines
3997
+ .replace(/\r/g, '\\r') // Escape carriage returns
3998
+ .replace(/\t/g, '\\t'); // Escape tabs
3999
+ }
4000
+ /**
4001
+ * Simplified Expression Evaluator
4002
+ * Replaced complex two-phase parser with regex + JavaScript evaluation for 100% reliability
4003
+ */
3889
4004
  function evaluateExpression(expression, variables = {}, contextStack = [], options = {}, engine) {
3890
4005
  const opts = {
3891
- securityLevel: 'standard',
4006
+ securityLevel: 'basic',
3892
4007
  allowLogicalOperators: true,
3893
4008
  allowMathOperators: true,
3894
4009
  allowComparisons: true,
@@ -3898,335 +4013,56 @@ function evaluateExpression(expression, variables = {}, contextStack = [], optio
3898
4013
  ...options
3899
4014
  };
3900
4015
  try {
3901
- logger.debug(`Evaluating expression: ${expression} with options: ${JSON.stringify(opts)}`);
3902
- // Security check with configurable level
3903
- if (containsUnsafePatterns(expression, opts, engine)) {
3904
- logger.warn(`Blocked unsafe expression in ${opts.context}: ${expression}`);
3905
- return opts.returnType === 'boolean' ? false : `[blocked: ${expression}]`;
3906
- }
3907
- // Special handling for boolean return type with template expressions
3908
- // If the entire expression is wrapped in {{ }} and we want a boolean,
3909
- // evaluate the inner expression directly without string conversion
3910
- if (opts.returnType === 'boolean' && expression.startsWith('{{') && expression.endsWith('}}')) {
3911
- const innerExpression = expression.slice(2, -2).trim();
3912
- const result = evaluateExpression(innerExpression, variables, contextStack, {
3913
- ...opts,
3914
- returnType: 'boolean'
3915
- }, engine);
3916
- return result;
3917
- }
3918
- // First, check if this is a comparison or logical expression with template variables
3919
- let processedExpression = expression;
3920
- // If it has template variables, interpolate them first (for non-boolean contexts)
3921
- while (processedExpression.includes('{{') && processedExpression.includes('}}')) {
3922
- processedExpression = interpolateTemplateVariables(processedExpression, variables, contextStack, opts, engine);
3923
- }
3924
- // Now check the processed expression for operators (after template interpolation)
3925
- // Handle logical AND/OR expressions (if allowed)
3926
- if (opts.allowLogicalOperators && (processedExpression.includes('&&') || processedExpression.includes('||'))) {
3927
- const result = evaluateLogicalExpression(processedExpression, variables, contextStack, opts, engine);
3928
- return convertReturnType(result, opts.returnType);
3929
- }
3930
- // Handle comparison expressions (if allowed)
3931
- if (opts.allowComparisons && containsComparisonOperators(processedExpression)) {
3932
- const result = evaluateComparisonExpression(processedExpression, variables, contextStack, opts, engine);
3933
- return convertReturnType(result, opts.returnType);
3934
- }
3935
- // Handle ternary expressions (if allowed)
3936
- if (opts.allowTernary && processedExpression.includes('?') && processedExpression.includes(':')) {
3937
- const result = evaluateSafeTernaryExpression(processedExpression, variables, contextStack, engine);
3938
- return convertReturnType(result, opts.returnType);
3939
- }
3940
- // Handle mathematical expressions (if allowed)
3941
- if (opts.allowMathOperators && isMathematicalExpression(processedExpression)) {
3942
- const result = evaluateSafeMathematicalExpression(processedExpression, variables, contextStack, engine);
3943
- return convertReturnType(result, opts.returnType);
3944
- }
3945
- // Handle typeof operator (e.g., "typeof variable", "typeof container")
3946
- if (processedExpression.trim().startsWith('typeof ')) {
3947
- const result = evaluateTypeofExpression(processedExpression, variables, contextStack, engine);
3948
- return convertReturnType(result, opts.returnType);
3949
- }
3950
- // Handle simple variable paths (e.g., "user.name", "data.items.length")
3951
- if (isSimpleVariablePath(processedExpression)) {
3952
- const result = resolveSimpleVariable(processedExpression, variables, contextStack, engine);
3953
- return convertReturnType(result, opts.returnType);
3954
- }
3955
- // Handle function calls (e.g., "currentTime()", "extractCryptoFromInput(...)")
3956
- if (processedExpression.includes('(') && processedExpression.includes(')')) {
3957
- const result = evaluateFunctionCall(processedExpression, variables, contextStack, engine);
3958
- if (result !== undefined) {
3959
- return convertReturnType(result, opts.returnType);
3960
- }
3961
- }
3962
- // If no pattern matches and we had template variables, return the interpolated result
3963
- if (processedExpression !== expression) {
3964
- return convertReturnType(processedExpression, opts.returnType);
3965
- }
3966
- // Fallback: treat as literal
3967
- const result = expression;
3968
- return convertReturnType(result, opts.returnType);
3969
- }
3970
- catch (error) {
3971
- logger.warn(`Expression evaluation error in ${opts.context}: ${error.message}`);
3972
- return opts.returnType === 'boolean' ? false : `[error: ${expression}]`;
3973
- }
3974
- }
3975
- // Unified security pattern checking with configurable levels
3976
- function containsUnsafePatterns(expression, options, engine) {
3977
- const { securityLevel = 'standard', allowLogicalOperators = true, allowComparisons = true } = options;
3978
- // Safe string methods that are allowed in expressions
3979
- const safeStringMethods = [
3980
- 'toLowerCase', 'toUpperCase', 'trim', 'charAt', 'charCodeAt', 'indexOf', 'lastIndexOf',
3981
- 'substring', 'substr', 'slice', 'split', 'replace', 'match', 'search', 'includes',
3982
- 'startsWith', 'endsWith', 'padStart', 'padEnd', 'repeat', 'toString', 'valueOf',
3983
- 'length', 'concat', 'localeCompare', 'normalize'
3984
- ];
3985
- // Safe array methods that are allowed in expressions
3986
- const safeArrayMethods = [
3987
- 'length', 'join', 'indexOf', 'lastIndexOf', 'includes', 'slice', 'toString', 'valueOf'
3988
- ];
3989
- // Safe math methods that are allowed in expressions
3990
- const safeMathMethods = [
3991
- 'abs', 'ceil', 'floor', 'round', 'max', 'min', 'pow', 'sqrt', 'random'
3992
- ];
3993
- // Safe standalone functions that are allowed
3994
- const safeStandaloneFunctions = [
3995
- 'isNaN', 'isFinite', 'parseInt', 'parseFloat',
3996
- 'encodeURIComponent', 'decodeURIComponent', 'encodeURI', 'decodeURI',
3997
- 'String', 'Number', 'Boolean' // Type conversion functions (not new constructors)
3998
- ];
3999
- // Check if expression contains only safe function calls
4000
- const functionCallPattern = /(\w+)\.(\w+)\s*\(/g;
4001
- const standaloneFunctionPattern = /(?:^|[^.\w])(\w+)\s*\(/g;
4002
- let match;
4003
- const foundFunctionCalls = [];
4004
- // First, check for any standalone function calls (not methods)
4005
- standaloneFunctionPattern.lastIndex = 0; // Reset regex
4006
- while ((match = standaloneFunctionPattern.exec(expression)) !== null) {
4007
- const functionName = match[1];
4008
- // Skip if this is actually a method call (preceded by a dot)
4009
- const beforeFunction = expression.substring(0, match.index + match[0].indexOf(functionName));
4010
- if (!beforeFunction.endsWith('.')) {
4011
- // Check if this standalone function is in our safe list or approved functions
4012
- const isApprovedFunction = engine.APPROVED_FUNCTIONS?.get &&
4013
- typeof engine.APPROVED_FUNCTIONS.get(functionName) === 'function';
4014
- if (!safeStandaloneFunctions.includes(functionName) && !isApprovedFunction) {
4015
- logger.debug(`Blocking unsafe standalone function call: ${functionName}`);
4016
- return true; // Unsafe standalone function calls are not allowed
4017
- }
4018
- logger.debug(`Allowing safe standalone function call: ${functionName}${isApprovedFunction ? ' (approved)' : ' (built-in)'}`);
4019
- }
4020
- }
4021
- // Then, check method calls and verify they're safe
4022
- functionCallPattern.lastIndex = 0; // Reset regex
4023
- while ((match = functionCallPattern.exec(expression)) !== null) {
4024
- const methodName = match[2];
4025
- foundFunctionCalls.push(methodName);
4026
- // If this method is not in our safe lists, it's potentially unsafe
4027
- if (!safeStringMethods.includes(methodName) &&
4028
- !safeArrayMethods.includes(methodName) &&
4029
- !safeMathMethods.includes(methodName)) {
4030
- logger.debug(`Blocking unsafe method call: ${methodName}`);
4031
- return true; // Unsafe method found
4032
- }
4033
- }
4034
- // Core dangerous patterns (blocked at all levels) - removed the problematic function call pattern
4035
- const coreDangerousPatterns = [
4036
- /eval\s*\(/, // eval() calls
4037
- /Function\s*\(/, // Function constructor
4038
- /constructor/, // Constructor access
4039
- /prototype/, // Prototype manipulation
4040
- /__proto__/, // Prototype access
4041
- /import\s*\(/, // Dynamic imports
4042
- /require\s*\(/, // CommonJS requires
4043
- /process\./, // Process access
4044
- /global\./, // Global access
4045
- /window\./, // Window access (browser)
4046
- /document\./, // Document access (browser)
4047
- /console\./, // Console access
4048
- /setTimeout/, // Timer functions
4049
- /setInterval/, // Timer functions
4050
- /fetch\s*\(/, // Network requests
4051
- /XMLHttpRequest/, // Network requests
4052
- /localStorage/, // Storage access
4053
- /sessionStorage/, // Storage access
4054
- /\+\s*\+/, // Increment operators
4055
- /--/, // Decrement operators
4056
- /(?<![=!<>])=(?!=)/, // Assignment operators = (but not ==, !=, <=, >=)
4057
- /delete\s+/, // Delete operator
4058
- /new\s+/, // Constructor calls
4059
- /throw\s+/, // Throw statements
4060
- /try\s*\{/, // Try blocks
4061
- /catch\s*\(/, // Catch blocks
4062
- /finally\s*\{/, // Finally blocks
4063
- /for\s*\(/, // For loops
4064
- /while\s*\(/, // While loops
4065
- /do\s*\{/, // Do-while loops
4066
- /switch\s*\(/, // Switch statements
4067
- /return\s+/, // Return statements
4068
- ];
4069
- // Additional strict patterns (only blocked in strict mode)
4070
- const strictOnlyPatterns = [
4071
- /\[.*\]/, // Array/object bracket notation
4072
- /this\./, // This access
4073
- /arguments\./, // Arguments access
4074
- ];
4075
- // Check core patterns (function call validation is handled separately above)
4076
- if (coreDangerousPatterns.some(pattern => pattern.test(expression))) {
4077
- return true;
4078
- }
4079
- // Check strict-only patterns if in strict mode
4080
- if (securityLevel === 'strict' && strictOnlyPatterns.some(pattern => pattern.test(expression))) {
4081
- return true;
4082
- }
4083
- return false;
4084
- }
4085
- // Template variable interpolation ({{variable}} format) with nested support
4086
- // Inside-out approach: process innermost {{}} expressions first, then repeat until no more changes
4087
- function interpolateTemplateVariables(template, variables, contextStack, options, engine) {
4088
- logger.debug(`Starting template interpolation: ${template}`);
4089
- let result = template;
4090
- let iterations = 0;
4091
- const maxIterations = 10; // Prevent infinite loops
4092
- while (result.includes('{{') && result.includes('}}') && iterations < maxIterations) {
4093
- iterations++;
4094
- logger.debug(`Template interpolation iteration ${iterations}: ${result}`);
4095
- // Find the LAST (rightmost) {{ - this is the innermost opening
4096
- const lastOpenIndex = result.lastIndexOf('{{');
4097
- if (lastOpenIndex === -1)
4098
- break;
4099
- // From that position, find the FIRST }} - this is the matching closing
4100
- const closeIndex = result.indexOf('}}', lastOpenIndex);
4101
- if (closeIndex === -1) {
4102
- logger.error(`Found {{ at ${lastOpenIndex} but no matching }} in: ${result}`);
4103
- return 'Invalid template!';
4104
- }
4105
- // Extract the innermost expression
4106
- const expression = result.substring(lastOpenIndex + 2, closeIndex).trim();
4107
- logger.debug(`Found innermost template expression: {{${expression}}}`);
4108
- // Evaluate the innermost expression
4109
- const evaluatedContent = evaluateExpression(expression, variables, contextStack, {
4110
- ...options,
4111
- returnType: 'string'
4112
- }, engine);
4113
- const replacement = typeof evaluatedContent === 'string' ? evaluatedContent : String(evaluatedContent);
4114
- logger.debug(`Evaluated to: ${replacement}`);
4115
- // Replace the template expression with its evaluated result
4116
- result = result.substring(0, lastOpenIndex) + replacement + result.substring(closeIndex + 2);
4117
- logger.debug(`After replacement: ${result}`);
4118
- }
4119
- if (iterations >= maxIterations) {
4120
- logger.warn(`Template interpolation stopped after ${maxIterations} iterations to prevent infinite loop`);
4121
- }
4122
- logger.debug(`Final template result: ${result}`);
4123
- return result;
4124
- }
4125
- // Logical expression evaluator (&&, ||)
4126
- function evaluateLogicalExpression(expression, variables, contextStack, options, engine) {
4127
- // Handle OR expressions
4128
- if (expression.includes('||')) {
4129
- return evaluateSafeOrExpression(expression, variables, contextStack, engine);
4130
- }
4131
- // Handle AND expressions
4132
- if (expression.includes('&&')) {
4133
- const parts = expression.split('&&').map(part => part.trim());
4134
- for (const part of parts) {
4135
- const partResult = evaluateExpression(part, variables, contextStack, {
4136
- ...options,
4137
- returnType: 'boolean'
4138
- }, engine);
4139
- if (!partResult) {
4140
- return false;
4016
+ logger.debug(`Simplified evaluation starting: ${expression}`);
4017
+ // If not a string, return as-is
4018
+ if (typeof expression !== 'string') {
4019
+ return expression;
4020
+ }
4021
+ // Simple regex to find {{expression}} patterns
4022
+ const expressionRegex = /\{\{([^}]+)\}\}/g;
4023
+ // Create evaluation context by merging all available variables
4024
+ const context = createSimplifiedEvaluationContext(variables, contextStack, engine);
4025
+ // Check if the entire expression is a single interpolation
4026
+ const singleExpressionMatch = expression.match(/^\{\{([^}]+)\}\}$/);
4027
+ if (singleExpressionMatch) {
4028
+ // Single expression - return the evaluated result directly to preserve type
4029
+ try {
4030
+ const evaluationResult = evaluateJavaScriptExpression(singleExpressionMatch[1].trim(), context);
4031
+ logger.debug(`Simplified evaluation complete (single): ${expression} -> ${evaluationResult}`);
4032
+ return convertReturnType(evaluationResult, opts.returnType);
4033
+ }
4034
+ catch (error) {
4035
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
4036
+ logger.warn(`Single expression evaluation failed: ${singleExpressionMatch[1]} - ${errorMessage}`);
4037
+ return opts.returnType === 'boolean' ? false : `[error: ${expression}]`;
4141
4038
  }
4142
4039
  }
4143
- return true;
4144
- }
4145
- return false;
4146
- }
4147
- // Comparison expression evaluator (==, !=, <, >, <=, >=)
4148
- function evaluateComparisonExpression(expression, variables, contextStack, options, engine) {
4149
- // For comparison expressions, we need to handle variable substitution properly
4150
- let processedExpression = expression;
4151
- // Find all variable references (not in template format) and replace them
4152
- // This handles cases like "input.toLowerCase()" where input is a variable
4153
- const variableNames = Object.keys(variables);
4154
- for (const varName of variableNames) {
4155
- const varValue = variables[varName];
4156
- // Create a regex to match the variable name as a whole word
4157
- const varRegex = new RegExp(`\\b${varName}\\b`, 'g');
4158
- // Replace variable references with their values, properly quoted for strings
4159
- processedExpression = processedExpression.replace(varRegex, (match) => {
4160
- if (typeof varValue === 'string') {
4161
- logger.debug(`Replacing variable '${varName}' with string value: ${varValue}`);
4162
- return `"${varValue}"`;
4163
- }
4164
- else if (typeof varValue === 'boolean' || typeof varValue === 'number') {
4165
- logger.debug(`Replacing variable '${varName}' with value: ${varValue}`);
4166
- return varValue.toString();
4167
- }
4168
- else if (varValue === null || varValue === undefined) {
4169
- logger.debug(`Replacing variable '${varName}' with null value`);
4170
- return 'null';
4040
+ // Multiple expressions or mixed content - convert to string
4041
+ const result = expression.replace(expressionRegex, (match, expr) => {
4042
+ try {
4043
+ // Direct JavaScript evaluation with injected variables
4044
+ const evaluationResult = evaluateJavaScriptExpression(expr.trim(), context);
4045
+ return String(evaluationResult);
4171
4046
  }
4172
- else {
4173
- logger.debug(`Replacing variable '${varName}' with JSON value:`, varValue);
4174
- return JSON.stringify(varValue);
4047
+ catch (error) {
4048
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
4049
+ logger.warn(`Expression evaluation failed: ${expr} - ${errorMessage}`);
4050
+ return match; // Return original if evaluation fails
4175
4051
  }
4176
4052
  });
4177
- }
4178
- // Handle any remaining {{variable}} template expressions
4179
- const variableMatches = processedExpression.match(/\{\{([^}]+)\}\}/g);
4180
- if (variableMatches) {
4181
- for (const match of variableMatches) {
4182
- const varName = match.slice(2, -2).trim();
4183
- let varValue = getNestedValue(variables, varName);
4184
- logger.debug(`Resolving template variable '${varName}' with value:`, varValue);
4185
- // Check engine session variables if not found
4186
- if (varValue === undefined && engine) {
4187
- varValue = resolveEngineSessionVariable(varName, engine);
4188
- }
4189
- // Convert value to appropriate type for comparison
4190
- let replacementValue;
4191
- if (varValue === undefined || varValue === null) {
4192
- replacementValue = 'null';
4193
- }
4194
- else if (typeof varValue === 'string') {
4195
- replacementValue = `"${varValue}"`;
4196
- }
4197
- else if (typeof varValue === 'boolean') {
4198
- replacementValue = varValue.toString();
4199
- }
4200
- else {
4201
- replacementValue = varValue.toString();
4202
- }
4203
- processedExpression = processedExpression.replace(match, replacementValue);
4204
- }
4205
- }
4206
- logger.debug(`Comparison expression after variable substitution: ${processedExpression}`);
4207
- try {
4208
- // Use Function constructor for safe evaluation
4209
- const result = new Function('return ' + processedExpression)();
4210
- logger.debug(`Comparison evaluation result: ${result}`);
4211
- return !!result; // Convert to boolean
4053
+ logger.debug(`Simplified evaluation complete: ${expression} -> ${result}`);
4054
+ return convertReturnType(result, opts.returnType);
4212
4055
  }
4213
4056
  catch (error) {
4214
- logger.info(`Comparison evaluation failed for '${expression}':`, error.message);
4215
- return false;
4057
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
4058
+ logger.warn(`Simplified evaluation error: ${errorMessage}`);
4059
+ return opts.returnType === 'boolean' ? false : `[error: ${expression}]`;
4216
4060
  }
4217
4061
  }
4218
- // Helper function to check for comparison operators
4219
- function containsComparisonOperators(expression) {
4220
- return /[<>=!]+/.test(expression) &&
4221
- !(expression.includes('&&') || expression.includes('||')); // Not a logical expression
4222
- }
4223
- // Helper function to check for mathematical expressions
4224
- function isMathematicalExpression(expression) {
4225
- return /[\+\-\*\/\%]/.test(expression) &&
4226
- !expression.includes('++') &&
4227
- !expression.includes('--'); // Exclude increment/decrement
4228
- }
4229
- // Convert result to requested return type
4062
+ /**
4063
+ * Convert result to requested return type
4064
+ * Enhanced version with better type handling
4065
+ */
4230
4066
  function convertReturnType(value, returnType) {
4231
4067
  switch (returnType) {
4232
4068
  case 'boolean':
@@ -4238,478 +4074,158 @@ function convertReturnType(value, returnType) {
4238
4074
  return value;
4239
4075
  }
4240
4076
  }
4241
- // Clean unified expression interface functions
4077
+ /**
4078
+ * Get engine session variables for simplified evaluator
4079
+ * This is the SINGLE SOURCE OF TRUTH for session variable logic
4080
+ */
4081
+ function getEngineSessionVariables(engine, contextStack) {
4082
+ const sessionVars = {};
4083
+ try {
4084
+ const currentFlowFrame = getCurrentFlowFrame(engine);
4085
+ // Get userInput from the most recent user entry in context stack
4086
+ const userEntries = currentFlowFrame.contextStack.filter(entry => entry.role === 'user');
4087
+ if (userEntries.length > 0) {
4088
+ const lastUserEntry = userEntries[userEntries.length - 1];
4089
+ const userInputValue = typeof lastUserEntry.content === 'string' ? lastUserEntry.content : String(lastUserEntry.content);
4090
+ sessionVars.userInput = userInputValue;
4091
+ sessionVars.lastUserInput = userInputValue; // alias
4092
+ }
4093
+ // Add other engine session variables
4094
+ sessionVars.cargo = engine.cargo || {};
4095
+ sessionVars.sessionId = engine.sessionId;
4096
+ sessionVars.userId = currentFlowFrame.userId;
4097
+ sessionVars.flowName = currentFlowFrame.flowName;
4098
+ // Special function-like variables
4099
+ sessionVars['currentTime()'] = new Date().toISOString();
4100
+ // Add global variables if available
4101
+ if (engine.globalVariables) {
4102
+ Object.assign(sessionVars, engine.globalVariables);
4103
+ }
4104
+ }
4105
+ catch (error) {
4106
+ logger.debug(`Error getting engine session variables: ${error}`);
4107
+ }
4108
+ return sessionVars;
4109
+ }
4110
+ /**
4111
+ * Create evaluation context with all available variables for simplified evaluator
4112
+ */
4113
+ function createSimplifiedEvaluationContext(variables, contextStack, engine) {
4114
+ // Just use the provided variables - contextStack is primarily for conversation history
4115
+ const allVars = variables || {};
4116
+ // Get engine session variables
4117
+ const engineSessionVars = getEngineSessionVariables(engine, contextStack);
4118
+ const context = {
4119
+ // All variables as top-level properties
4120
+ ...allVars,
4121
+ // Engine session variables (userInput, cargo, sessionId, etc.)
4122
+ ...engineSessionVars,
4123
+ // Special variables that should always be available
4124
+ $args: variables?.$args || {},
4125
+ xml_result: variables?.xml_result,
4126
+ json_result: variables?.json_result,
4127
+ // Add context object for Object.keys() patterns (avoid 'this' keyword issues)
4128
+ currentContext: { ...allVars, ...engineSessionVars },
4129
+ // Include approved functions so they can be called in expressions
4130
+ ...engine.APPROVED_FUNCTIONS,
4131
+ // Utility objects available in expressions
4132
+ Object,
4133
+ Date,
4134
+ Math,
4135
+ String,
4136
+ Number,
4137
+ Boolean,
4138
+ Array,
4139
+ JSON
4140
+ };
4141
+ return context;
4142
+ }
4143
+ /**
4144
+ * Safely evaluate JavaScript expression with injected context
4145
+ */
4146
+ function evaluateJavaScriptExpression(expression, context) {
4147
+ // Filter to only valid JavaScript identifiers (root variables only)
4148
+ const validParamNames = Object.keys(context).filter(key => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key));
4149
+ // Unwrap user input variables before passing to function
4150
+ const paramValues = validParamNames.map(name => {
4151
+ const value = context[name];
4152
+ // Unwrap user input wrapper objects
4153
+ if (value && typeof value === 'object' &&
4154
+ value.__userInput === true &&
4155
+ value.__literal === true) {
4156
+ return value.value;
4157
+ }
4158
+ return value;
4159
+ });
4160
+ // Build function body that evaluates the expression
4161
+ const functionBody = `
4162
+ "use strict";
4163
+ try {
4164
+ const result = (${expression});
4165
+ // Auto-stringify complex data structures when used in string contexts
4166
+ if (typeof result === 'object' && result !== null &&
4167
+ !(result.__userInput === true && result.__literal === true)) {
4168
+ return JSON.stringify(result);
4169
+ }
4170
+ return result;
4171
+ } catch (e) {
4172
+ throw new Error('Expression evaluation failed: ' + e.message);
4173
+ }
4174
+ `;
4175
+ // Create and execute function with only valid parameter names
4176
+ const evaluatorFunction = new Function(...validParamNames, functionBody);
4177
+ return evaluatorFunction(...paramValues);
4178
+ }
4179
+ /**
4180
+ * Security-aware variable setter for user input
4181
+ * Integrates with the variable resolution system
4182
+ */
4183
+ function setUserInputVariable(variables, key, value, sanitize = true) {
4184
+ // Mark user input as static literal data that should never be evaluated as code
4185
+ variables[key] = {
4186
+ __userInput: true,
4187
+ __literal: true,
4188
+ value: sanitize && typeof value === 'string' ? sanitizeUserInput(value) : value
4189
+ };
4190
+ }
4191
+ // ===============================================
4192
+ // LEGACY COMPATIBILITY FUNCTIONS
4193
+ // ===============================================
4194
+ /**
4195
+ * Clean unified expression interface functions
4196
+ * Maintains backward compatibility with existing code
4197
+ */
4242
4198
  function interpolateMessage(template, contextStack, variables = {}, engine) {
4243
4199
  logger.debug(`Interpolating message template: ${template} with variables: ${JSON.stringify(variables)}`);
4244
4200
  if (!template)
4245
4201
  return template;
4246
- // Use our enhanced template interpolation that supports nested {{ }} expressions
4247
- return interpolateTemplateVariables(template, variables, contextStack, {
4248
- securityLevel: 'standard',
4202
+ // For template interpolation, we should NOT apply security restrictions
4203
+ // Templates are developer-controlled, not user input
4204
+ const result = evaluateExpression(template, variables, contextStack, {
4205
+ securityLevel: 'none', // No security restrictions for templates
4249
4206
  allowLogicalOperators: true,
4250
4207
  allowMathOperators: true,
4251
4208
  allowComparisons: true,
4252
4209
  allowTernary: true,
4253
4210
  context: 'template-interpolation',
4254
- returnType: 'auto'
4211
+ returnType: 'string'
4255
4212
  }, engine);
4213
+ return String(result);
4256
4214
  }
4215
+ /**
4216
+ * Safe condition evaluation for boolean contexts
4217
+ * Maintains backward compatibility
4218
+ */
4257
4219
  function evaluateSafeCondition(condition, variables, engine) {
4258
4220
  logger.debug(`Evaluating safe condition: ${condition} with variables: ${JSON.stringify(variables)}`);
4259
- return evaluateExpression(condition, variables, [], {
4260
- securityLevel: 'standard',
4221
+ const result = evaluateExpression(condition, variables, [], {
4222
+ securityLevel: 'basic',
4261
4223
  context: 'condition-evaluation',
4262
4224
  returnType: 'boolean',
4263
4225
  allowComparisons: true,
4264
4226
  allowLogicalOperators: true
4265
4227
  }, engine);
4266
- }
4267
- // Essential helper functions for the unified evaluator
4268
- function isSimpleVariablePath(expression) {
4269
- logger.debug(`Checking if expression is a simple variable path: ${expression}`);
4270
- return /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(expression);
4271
- }
4272
- function resolveSimpleVariable(expression, variables, contextStack, engine) {
4273
- // Debug logging to track variable resolution issues
4274
- logger.debug(`Resolving variable '${expression}' - Available variables: ${JSON.stringify(Object.keys(variables || {}))}`);
4275
- // Try variables first
4276
- if (variables && Object.keys(variables).length > 0) {
4277
- const val = expression.split('.').reduce((obj, part) => obj?.[part], variables);
4278
- if (val !== undefined) {
4279
- logger.debug(`Variable '${expression}' resolved to: ${val}`);
4280
- return typeof val === 'object' ? JSON.stringify(val) : String(val);
4281
- }
4282
- }
4283
- // If not found in variables, check for engine session variables
4284
- if (engine) {
4285
- const sessionValue = resolveEngineSessionVariable(expression, engine);
4286
- if (sessionValue !== undefined) {
4287
- logger.debug(`Variable '${expression}' resolved from engine session: ${sessionValue}`);
4288
- return typeof sessionValue === 'object' ? JSON.stringify(sessionValue) : String(sessionValue);
4289
- }
4290
- }
4291
- logger.debug(`Variable '${expression}' not found in variables or engine session`);
4292
- return `[undefined: ${expression}]`;
4293
- }
4294
- // Helper function to resolve engine session variables
4295
- function resolveEngineSessionVariable(expression, engine) {
4296
- try {
4297
- const currentFlowFrame = getCurrentFlowFrame(engine);
4298
- logger.debug(`Resolving engine session variable: ${expression} in flow frame: ${currentFlowFrame.flowName}`);
4299
- // Handle property access (e.g., cargo.someVar)
4300
- if (expression.includes('.')) {
4301
- const parts = expression.split('.');
4302
- const baseVariable = parts[0];
4303
- const propertyPath = parts.slice(1).join('.');
4304
- let baseValue;
4305
- // Get the base engine session variable
4306
- switch (baseVariable) {
4307
- case 'cargo':
4308
- baseValue = engine.cargo || {};
4309
- break;
4310
- case 'userInput':
4311
- case 'lastUserInput':
4312
- const userEntries = currentFlowFrame.contextStack.filter(entry => entry.role === 'user');
4313
- if (userEntries.length > 0) {
4314
- const lastUserEntry = userEntries[userEntries.length - 1];
4315
- baseValue = typeof lastUserEntry.content === 'string' ? lastUserEntry.content : String(lastUserEntry.content);
4316
- }
4317
- else {
4318
- baseValue = undefined;
4319
- }
4320
- break;
4321
- case 'sessionId':
4322
- baseValue = engine.sessionId;
4323
- break;
4324
- case 'userId':
4325
- baseValue = currentFlowFrame.userId;
4326
- break;
4327
- case 'flowName':
4328
- baseValue = currentFlowFrame.flowName;
4329
- break;
4330
- default:
4331
- logger.debug(`Unknown base engine session variable: ${baseVariable}`);
4332
- return undefined;
4333
- }
4334
- // If base value is undefined, return undefined
4335
- if (baseValue === undefined || baseValue === null) {
4336
- logger.debug(`Base engine session variable '${baseVariable}' is undefined/null`);
4337
- return undefined;
4338
- }
4339
- // Navigate through the property path using getNestedValue
4340
- const result = getNestedValue(baseValue, propertyPath);
4341
- logger.debug(`Engine session variable '${expression}' resolved to:`, result);
4342
- return result;
4343
- }
4344
- // Handle direct variable access (no dots)
4345
- switch (expression) {
4346
- case 'cargo':
4347
- logger.debug(`Returning cargo from engine session variable: ${expression}`);
4348
- return engine.cargo || {};
4349
- case 'userInput':
4350
- case 'lastUserInput':
4351
- // Get the most recent user input from context stack
4352
- const userEntries = currentFlowFrame.contextStack.filter(entry => entry.role === 'user');
4353
- if (userEntries.length > 0) {
4354
- logger.debug(`Returning last user input from engine session variable: ${expression}`);
4355
- const lastUserEntry = userEntries[userEntries.length - 1];
4356
- return typeof lastUserEntry.content === 'string' ? lastUserEntry.content : String(lastUserEntry.content);
4357
- }
4358
- logger.debug(`No user input found in context stack for engine session variable: ${expression}`);
4359
- return undefined;
4360
- case 'currentTime()':
4361
- logger.debug(`Returning current time for engine session variable: ${expression}`);
4362
- return new Date().toISOString();
4363
- case 'sessionId':
4364
- logger.debug(`Returning session ID for engine session variable: ${expression}`);
4365
- return engine.sessionId;
4366
- case 'userId':
4367
- logger.debug(`Returning user ID for engine session variable: ${expression}`);
4368
- return currentFlowFrame.userId;
4369
- case 'flowName':
4370
- logger.debug(`Returning flow name for engine session variable: ${expression}`);
4371
- return currentFlowFrame.flowName;
4372
- default:
4373
- logger.debug(`No specific engine session variable found for: ${expression}`);
4374
- return undefined;
4375
- }
4376
- }
4377
- catch (error) {
4378
- logger.debug(`Failed to resolve engine session variable '${expression}': ${error}`);
4379
- return undefined;
4380
- }
4381
- }
4382
- // Helper function to evaluate function calls
4383
- function evaluateFunctionCall(expression, variables, contextStack, engine) {
4384
- try {
4385
- logger.debug(`Evaluating function call: ${expression}`);
4386
- // Handle currentTime() function call
4387
- if (expression === 'currentTime()') {
4388
- logger.debug(`Returning current time for function call: ${expression}`);
4389
- return new Date().toISOString();
4390
- }
4391
- // Handle safe standalone functions
4392
- const safeStandaloneFunctions = ['isNaN', 'parseInt', 'parseFloat', 'Boolean', 'Number', 'String'];
4393
- // Parse function call with arguments
4394
- const functionMatch = expression.match(/^(\w+)\((.*)\)$/);
4395
- if (functionMatch) {
4396
- const funcName = functionMatch[1];
4397
- const argsString = functionMatch[2];
4398
- if (safeStandaloneFunctions.includes(funcName)) {
4399
- logger.debug(`Evaluating safe standalone function: ${funcName}`);
4400
- // Parse and evaluate arguments
4401
- const args = [];
4402
- if (argsString.trim()) {
4403
- // Simple argument parsing (supports literals and variables)
4404
- const argParts = argsString.split(',').map(arg => arg.trim());
4405
- for (const argPart of argParts) {
4406
- let argValue;
4407
- // Handle string literals
4408
- if ((argPart.startsWith('"') && argPart.endsWith('"')) ||
4409
- (argPart.startsWith("'") && argPart.endsWith("'"))) {
4410
- argValue = argPart.slice(1, -1);
4411
- }
4412
- // Handle number literals
4413
- else if (!isNaN(Number(argPart))) {
4414
- argValue = Number(argPart);
4415
- }
4416
- // Handle boolean literals
4417
- else if (argPart === 'true' || argPart === 'false') {
4418
- argValue = argPart === 'true';
4419
- }
4420
- // Handle variables
4421
- else {
4422
- const varResult = resolveSimpleVariable(argPart, variables, contextStack, engine);
4423
- if (varResult && !varResult.startsWith('[undefined:')) {
4424
- argValue = varResult;
4425
- }
4426
- else if (engine) {
4427
- const sessionResult = resolveEngineSessionVariable(argPart, engine);
4428
- argValue = sessionResult !== undefined ? sessionResult : argPart;
4429
- }
4430
- else {
4431
- argValue = argPart;
4432
- }
4433
- }
4434
- args.push(argValue);
4435
- }
4436
- }
4437
- // Execute the safe function
4438
- try {
4439
- switch (funcName) {
4440
- case 'isNaN':
4441
- logger.debug(`Executing isNaN with args: ${args}`);
4442
- return isNaN(args[0]);
4443
- case 'parseInt':
4444
- logger.debug(`Executing parseInt with args: ${args}`);
4445
- return parseInt(args[0], args[1] || 10);
4446
- case 'parseFloat':
4447
- logger.debug(`Executing parseFloat with args: ${args}`);
4448
- return parseFloat(args[0]);
4449
- case 'Boolean':
4450
- logger.debug(`Executing Boolean with args: ${args}`);
4451
- return Boolean(args[0]);
4452
- case 'Number':
4453
- logger.debug(`Executing Number with args: ${args}`);
4454
- return Number(args[0]);
4455
- case 'String':
4456
- logger.debug(`Executing String with args: ${args}`);
4457
- return String(args[0]);
4458
- default:
4459
- logger.warn(`Safe function ${funcName} not implemented`);
4460
- return undefined;
4461
- }
4462
- }
4463
- catch (error) {
4464
- logger.debug(`Error executing safe function ${funcName}: ${error.message}`);
4465
- return undefined;
4466
- }
4467
- }
4468
- }
4469
- // Handle extractCryptoFromInput(...) function call
4470
- const extractCryptoMatch = expression.match(/^extractCryptoFromInput\((.+)\)$/);
4471
- if (extractCryptoMatch) {
4472
- const argExpression = extractCryptoMatch[1].trim();
4473
- // Evaluate the argument expression (could be a variable or literal)
4474
- let argValue;
4475
- if (argExpression.startsWith('"') && argExpression.endsWith('"')) {
4476
- argValue = argExpression.slice(1, -1);
4477
- }
4478
- else if (argExpression.startsWith("'") && argExpression.endsWith("'")) {
4479
- argValue = argExpression.slice(1, -1);
4480
- }
4481
- else {
4482
- // Try to resolve as variable (could be userInput or other variable)
4483
- const varResult = resolveSimpleVariable(argExpression, variables, contextStack, engine);
4484
- if (varResult && !varResult.startsWith('[undefined:')) {
4485
- argValue = varResult;
4486
- }
4487
- else if (engine) {
4488
- // Try engine session variables
4489
- const sessionResult = resolveEngineSessionVariable(argExpression, engine);
4490
- if (sessionResult !== undefined) {
4491
- argValue = String(sessionResult);
4492
- }
4493
- else {
4494
- argValue = argExpression; // Use as literal if can't resolve
4495
- }
4496
- }
4497
- else {
4498
- argValue = argExpression; // Use as literal if can't resolve
4499
- }
4500
- }
4501
- logger.debug(`extractCryptoFromInput called with: ${argValue}`);
4502
- // Extract crypto name from input text
4503
- const cryptoNames = {
4504
- 'bitcoin': 'bitcoin',
4505
- 'btc': 'bitcoin',
4506
- 'ethereum': 'ethereum',
4507
- 'eth': 'ethereum',
4508
- 'litecoin': 'litecoin',
4509
- 'ltc': 'litecoin',
4510
- 'dogecoin': 'dogecoin',
4511
- 'doge': 'dogecoin',
4512
- 'cardano': 'cardano',
4513
- 'ada': 'cardano'
4514
- };
4515
- const lowerInput = argValue.toLowerCase();
4516
- for (const [key, value] of Object.entries(cryptoNames)) {
4517
- if (lowerInput.includes(key)) {
4518
- logger.debug(`Extracted crypto: ${value}`);
4519
- return value;
4520
- }
4521
- }
4522
- logger.debug(`No crypto found in input, defaulting to bitcoin`);
4523
- return 'bitcoin'; // Default fallback
4524
- }
4525
- return undefined; // Function not recognized
4526
- }
4527
- catch (error) {
4528
- logger.warn(`Function call evaluation error: ${error.message}`);
4529
- return undefined;
4530
- }
4531
- }
4532
- function getNestedValue(obj, path) {
4533
- if (!obj || typeof path !== 'string') {
4534
- return undefined;
4535
- }
4536
- if (!path.includes('.')) {
4537
- return obj[path];
4538
- }
4539
- const keys = path.split('.');
4540
- let current = obj;
4541
- for (const key of keys) {
4542
- if (current === null || current === undefined) {
4543
- return undefined;
4544
- }
4545
- current = current[key];
4546
- }
4547
- return current;
4548
- }
4549
- function evaluateSafeOrExpression(expression, variables, contextStack, engine) {
4550
- const parts = expression.split('||').map(part => part.trim());
4551
- for (const part of parts) {
4552
- if ((part.startsWith('"') && part.endsWith('"')) ||
4553
- (part.startsWith("'") && part.endsWith("'"))) {
4554
- return part.slice(1, -1);
4555
- }
4556
- if (isSimpleVariablePath(part)) {
4557
- if (variables && Object.keys(variables).length > 0) {
4558
- const val = part.split('.').reduce((obj, partKey) => obj?.[partKey], variables);
4559
- if (val !== undefined && val !== null && String(val) !== '') {
4560
- return typeof val === 'object' ? JSON.stringify(val) : String(val);
4561
- }
4562
- }
4563
- // Check engine session variables if not found in variables
4564
- if (engine) {
4565
- const sessionValue = resolveEngineSessionVariable(part, engine);
4566
- if (sessionValue !== undefined && sessionValue !== null && String(sessionValue) !== '') {
4567
- return typeof sessionValue === 'object' ? JSON.stringify(sessionValue) : String(sessionValue);
4568
- }
4569
- }
4570
- }
4571
- }
4572
- const lastPart = parts[parts.length - 1];
4573
- if ((lastPart.startsWith('"') && lastPart.endsWith('"')) ||
4574
- (lastPart.startsWith("'") && lastPart.endsWith("'"))) {
4575
- return lastPart.slice(1, -1);
4576
- }
4577
- // Final fallback - check for session variables in the last part
4578
- if (engine && isSimpleVariablePath(lastPart)) {
4579
- const sessionValue = resolveEngineSessionVariable(lastPart, engine);
4580
- if (sessionValue !== undefined) {
4581
- return typeof sessionValue === 'object' ? JSON.stringify(sessionValue) : String(sessionValue);
4582
- }
4583
- }
4584
- return '';
4585
- }
4586
- function evaluateSafeTernaryExpression(expression, variables, contextStack, engine) {
4587
- const ternaryMatch = expression.match(/^([a-zA-Z_$.]+)\s*(===|!==|==|!=|>=|<=|>|<)\s*(\d+|'[^']*'|"[^"]*"|true|false)\s*\?\s*('([^']*)'|"([^"]*)"|[a-zA-Z_$.]+)\s*:\s*('([^']*)'|"([^"]*)"|[a-zA-Z_$.]+)$/);
4588
- if (!ternaryMatch) {
4589
- return `[invalid-ternary: ${expression}]`;
4590
- }
4591
- const [, leftVar, operator, rightValue, , trueStr1, trueStr2, , falseStr1, falseStr2] = ternaryMatch;
4592
- let leftVal;
4593
- if (variables && Object.keys(variables).length > 0) {
4594
- leftVal = leftVar.split('.').reduce((obj, part) => obj?.[part], variables);
4595
- }
4596
- // Check engine session variables if not found
4597
- if (leftVal === undefined && engine) {
4598
- leftVal = resolveEngineSessionVariable(leftVar, engine);
4599
- }
4600
- if (leftVal === undefined) {
4601
- return `[undefined: ${leftVar}]`;
4602
- }
4603
- let rightVal;
4604
- if (rightValue === 'true')
4605
- rightVal = true;
4606
- else if (rightValue === 'false')
4607
- rightVal = false;
4608
- else if (/^\d+$/.test(rightValue))
4609
- rightVal = parseInt(rightValue);
4610
- else if (/^\d*\.\d+$/.test(rightValue))
4611
- rightVal = parseFloat(rightValue);
4612
- else if (rightValue.startsWith("'") && rightValue.endsWith("'"))
4613
- rightVal = rightValue.slice(1, -1);
4614
- else if (rightValue.startsWith('"') && rightValue.endsWith('"'))
4615
- rightVal = rightValue.slice(1, -1);
4616
- else
4617
- rightVal = rightValue;
4618
- let condition = false;
4619
- switch (operator) {
4620
- case '===':
4621
- condition = leftVal === rightVal;
4622
- break;
4623
- case '!==':
4624
- condition = leftVal !== rightVal;
4625
- break;
4626
- case '==':
4627
- condition = leftVal == rightVal;
4628
- break;
4629
- case '!=':
4630
- condition = leftVal != rightVal;
4631
- break;
4632
- case '>':
4633
- condition = leftVal > rightVal;
4634
- break;
4635
- case '<':
4636
- condition = leftVal < rightVal;
4637
- break;
4638
- case '>=':
4639
- condition = leftVal >= rightVal;
4640
- break;
4641
- case '<=':
4642
- condition = leftVal <= rightVal;
4643
- break;
4644
- default: return `[invalid-operator: ${operator}]`;
4645
- }
4646
- if (condition) {
4647
- return trueStr1 || trueStr2 || resolveSimpleVariable(trueStr2, variables, contextStack, engine) || '';
4648
- }
4649
- else {
4650
- return falseStr1 || falseStr2 || resolveSimpleVariable(falseStr2, variables, contextStack, engine) || '';
4651
- }
4652
- }
4653
- function evaluateSafeMathematicalExpression(expression, variables, contextStack, engine) {
4654
- try {
4655
- let evaluatedExpression = expression;
4656
- const variablePattern = /[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/g;
4657
- const variables_found = expression.match(variablePattern) || [];
4658
- for (const varPath of [...new Set(variables_found)]) {
4659
- if (/^\d+(\.\d+)?$/.test(varPath))
4660
- continue;
4661
- let value = resolveSimpleVariable(varPath, variables, contextStack, engine);
4662
- const numValue = Number(value);
4663
- if (!isNaN(numValue)) {
4664
- value = String(numValue);
4665
- }
4666
- else if (value === undefined || value === null) {
4667
- value = '0';
4668
- }
4669
- const regex = new RegExp(`\\b${varPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
4670
- evaluatedExpression = evaluatedExpression.replace(regex, String(value));
4671
- }
4672
- if (!/^[\d+\-*/%().\s]+$/.test(evaluatedExpression)) {
4673
- return `[unsafe-math: ${expression}]`;
4674
- }
4675
- const result = new Function(`"use strict"; return (${evaluatedExpression})`)();
4676
- return String(result);
4677
- }
4678
- catch (error) {
4679
- logger.warn(`Mathematical expression evaluation error: ${error.message}`);
4680
- return `[math-error: ${expression}]`;
4681
- }
4682
- }
4683
- // Handle typeof operator expressions (e.g., "typeof variable", "typeof container.prop")
4684
- function evaluateTypeofExpression(expression, variables, contextStack, engine) {
4685
- try {
4686
- // Extract the variable name after "typeof "
4687
- const typeofMatch = expression.trim().match(/^typeof\s+(.+)$/);
4688
- if (!typeofMatch) {
4689
- logger.warn(`Invalid typeof expression: ${expression}`);
4690
- return 'undefined';
4691
- }
4692
- const variablePath = typeofMatch[1].trim();
4693
- logger.debug(`Evaluating typeof for variable path: ${variablePath}`);
4694
- // Resolve the variable value
4695
- let value;
4696
- // Try to resolve from variables first
4697
- if (variables && Object.keys(variables).length > 0) {
4698
- value = variablePath.split('.').reduce((obj, part) => obj?.[part], variables);
4699
- }
4700
- // If not found in variables, check engine session variables
4701
- if (value === undefined && engine) {
4702
- value = resolveEngineSessionVariable(variablePath, engine);
4703
- }
4704
- // Return the JavaScript typeof result
4705
- const typeResult = typeof value;
4706
- logger.debug(`typeof ${variablePath} = ${typeResult} (value: ${JSON.stringify(value)})`);
4707
- return typeResult;
4708
- }
4709
- catch (error) {
4710
- logger.warn(`Typeof expression evaluation error: ${error.message}`);
4711
- return 'undefined';
4712
- }
4228
+ return Boolean(result);
4713
4229
  }
4714
4230
  // === ENHANCED FLOW CONTROL COMMANDS ===
4715
4231
  // Universal commands that work during any flow
@@ -5071,10 +4587,28 @@ async function processActivity(input, userId, engine) {
5071
4587
  if (flowControlResult) {
5072
4588
  return flowControlResult;
5073
4589
  }
5074
- // Check for strong intent interruption (new flows)
5075
- const interruptionResult = await handleIntentInterruption(String(sanitizedInput), engine, userId);
5076
- if (interruptionResult) {
5077
- return interruptionResult;
4590
+ // Check for strong intent interruption (new flows) - only if current flow AND all parent flows are interruptable
4591
+ const flowStacks = engine.flowStacks;
4592
+ const currentStackIndex = flowStacks.length - 1;
4593
+ const currentStack = flowStacks[currentStackIndex] || [];
4594
+ let isAnyFlowNonInterruptable = false;
4595
+ for (const flowFrame of currentStack) {
4596
+ const flowDefinition = engine.flowsMenu.find((f) => f.name === flowFrame.flowName);
4597
+ const isFlowInterruptable = flowDefinition?.interruptable ?? false; // Default to false
4598
+ if (!isFlowInterruptable) {
4599
+ isAnyFlowNonInterruptable = true;
4600
+ logger.debug(`Flow "${flowFrame.flowName}" in stack is not interruptable`);
4601
+ break;
4602
+ }
4603
+ }
4604
+ if (!isAnyFlowNonInterruptable) {
4605
+ const interruptionResult = await handleIntentInterruption(String(sanitizedInput), engine, userId);
4606
+ if (interruptionResult) {
4607
+ return interruptionResult;
4608
+ }
4609
+ }
4610
+ else {
4611
+ logger.debug(`Skipping intent interruption check - one or more flows in the stack are not interruptable`);
5078
4612
  }
5079
4613
  // Clear the last SAY message if we had one
5080
4614
  if (currentFlowFrame.lastSayMessage) {
@@ -5509,7 +5043,8 @@ export class WorkflowEngine {
5509
5043
  flowCallGraph: new Map(),
5510
5044
  currentDepth: 0,
5511
5045
  variableScopes: new Map(),
5512
- toolRegistry: new Set(this.toolsRegistry.map((t) => t.id))
5046
+ toolRegistry: new Set(this.toolsRegistry.map((t) => t.id)),
5047
+ flowCallStack: [] // Track parent flow chain for variable inheritance
5513
5048
  };
5514
5049
  try {
5515
5050
  this._validateFlowRecursive(flowName, validationState, opts);
@@ -5549,7 +5084,7 @@ export class WorkflowEngine {
5549
5084
  return;
5550
5085
  }
5551
5086
  // Find the flow definition
5552
- const flowDef = this.flowsMenu.find((f) => f.name === flowName);
5087
+ const flowDef = this.flowsMenu.find((f) => f.id === flowName || f.name === flowName);
5553
5088
  if (!flowDef) {
5554
5089
  state.errors.push(`Flow not found: ${flowName}`);
5555
5090
  return;
@@ -5557,6 +5092,8 @@ export class WorkflowEngine {
5557
5092
  // Mark as visited
5558
5093
  state.visitedFlows.add(flowName);
5559
5094
  state.currentDepth++;
5095
+ // Add to flow call stack for variable inheritance tracking
5096
+ state.flowCallStack.push(flowName);
5560
5097
  // Initialize flow call graph
5561
5098
  if (!state.flowCallGraph.has(flowName)) {
5562
5099
  state.flowCallGraph.set(flowName, []);
@@ -5586,6 +5123,8 @@ export class WorkflowEngine {
5586
5123
  this._validateFlowRecursive(calledFlow, state, opts);
5587
5124
  }
5588
5125
  }
5126
+ // Remove from flow call stack when exiting this flow
5127
+ state.flowCallStack.pop();
5589
5128
  state.currentDepth--;
5590
5129
  }
5591
5130
  /**
@@ -5975,7 +5514,7 @@ export class WorkflowEngine {
5975
5514
  // PHASE 2: Validate only top-level flows (not referenced as sub-flows)
5976
5515
  logger.info('🔍 Phase 2: Validating top-level flows...');
5977
5516
  for (const flow of this.flowsMenu) {
5978
- if (referencedSubFlows.has(flow.name)) {
5517
+ if (referencedSubFlows.has(flow.name) || referencedSubFlows.has(flow.id)) {
5979
5518
  // Skip validation of sub-flows - they'll be validated in parent context
5980
5519
  logger.info(`⏭️ Skipping sub-flow "${flow.name}" - will be validated in parent context`);
5981
5520
  results.skippedSubFlows.push(flow.name);
@@ -6024,11 +5563,24 @@ export class WorkflowEngine {
6024
5563
  // === ENHANCED VARIABLE SCOPE TRACKING ===
6025
5564
  /**
6026
5565
  * Gets the current variable scope available at a specific step
6027
- * This includes variables defined in flow definition + variables created by previous steps
5566
+ * This includes variables defined in flow definition + variables created by previous steps + parent flow variables
6028
5567
  */
6029
5568
  _getCurrentStepScope(flowDef, stepIndex, state) {
6030
5569
  const scope = new Set();
6031
- // Add flow-defined variables
5570
+ // Add parent flow variables (variable inheritance from call stack)
5571
+ if (state.flowCallStack && state.flowCallStack.length > 0) {
5572
+ // Iterate through parent flows in the call stack (excluding current flow)
5573
+ for (let i = 0; i < state.flowCallStack.length - 1; i++) {
5574
+ const parentFlowName = state.flowCallStack[i];
5575
+ const parentFlowDef = this.flowsMenu.find((f) => f.id === parentFlowName || f.name === parentFlowName);
5576
+ if (parentFlowDef && parentFlowDef.variables) {
5577
+ for (const varName of Object.keys(parentFlowDef.variables)) {
5578
+ scope.add(varName);
5579
+ }
5580
+ }
5581
+ }
5582
+ }
5583
+ // Add flow-defined variables (current flow)
6032
5584
  if (flowDef.variables) {
6033
5585
  for (const varName of Object.keys(flowDef.variables)) {
6034
5586
  scope.add(varName);
@@ -6197,7 +5749,7 @@ export class WorkflowEngine {
6197
5749
  const variableNames = extractVariableNames(varPath);
6198
5750
  for (const rootVar of variableNames) {
6199
5751
  // Check if it's a registered function first (global scope)
6200
- const isApprovedFunction = this.APPROVED_FUNCTIONS && this.APPROVED_FUNCTIONS.get && this.APPROVED_FUNCTIONS.get(rootVar);
5752
+ const isApprovedFunction = this.APPROVED_FUNCTIONS && this.APPROVED_FUNCTIONS[rootVar];
6201
5753
  // If not an approved function, check if it's in current scope as a variable
6202
5754
  if (!isApprovedFunction && !currentScope.has(rootVar)) {
6203
5755
  state.errors.push(`${context} in step "${step.id}" references undefined variable: ${rootVar} (full path: ${varPath})`);