diff --git a/packages/components/nodes/agentflow/CustomFunction/CustomFunction.ts b/packages/components/nodes/agentflow/CustomFunction/CustomFunction.ts index f5d4bb615..8f31c0143 100644 --- a/packages/components/nodes/agentflow/CustomFunction/CustomFunction.ts +++ b/packages/components/nodes/agentflow/CustomFunction/CustomFunction.ts @@ -8,8 +8,7 @@ import { INodeParams, IServerSideEventStreamer } from '../../../src/Interface' -import { availableDependencies, defaultAllowBuiltInDep, getVars, prepareSandboxVars } from '../../../src/utils' -import { NodeVM } from '@flowiseai/nodevm' +import { getVars, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' import { updateFlowState } from '../utils' interface ICustomFunctionInputVariables { @@ -19,9 +18,9 @@ interface ICustomFunctionInputVariables { const exampleFunc = `/* * You can use any libraries imported in Flowise -* You can use properties specified in Input Schema as variables. Ex: Property = userid, Variable = $userid +* You can use properties specified in Input Variables with the prefix $. For example: $foo * You can get default flow config: $flow.sessionId, $flow.chatId, $flow.chatflowId, $flow.input, $flow.state -* You can get custom variables: $vars. +* You can get global variables: $vars. * Must return a string value at the end of function */ @@ -161,55 +160,36 @@ class CustomFunction_Agentflow implements INode { state: newState } - let sandbox: any = { - $input: input, - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined - } - sandbox['$vars'] = prepareSandboxVars(variables) - sandbox['$flow'] = flow - + // Create additional sandbox variables for custom function inputs + const additionalSandbox: ICommonObject = {} for (const item of functionInputVariables) { const variableName = item.variableName const variableValue = item.variableValue - sandbox[`$${variableName}`] = variableValue + additionalSandbox[`$${variableName}`] = variableValue } - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) + const sandbox = createCodeExecutionSandbox(input, variables, flow, additionalSandbox) - const nodeVMOptions = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, - timeout: 10000 - } as any + // Setup streaming function if needed + const streamOutput = isStreamable + ? (output: string) => { + const sseStreamer: IServerSideEventStreamer = options.sseStreamer + sseStreamer.streamTokenEvent(chatId, output) + } + : undefined - const vm = new NodeVM(nodeVMOptions) try { - const response = await vm.run(`module.exports = async function() {${javascriptFunction}}()`, __dirname) + const response = await executeJavaScriptCode(javascriptFunction, sandbox, { + libraries: ['axios'], + streamOutput, + timeout: 10000 + }) let finalOutput = response if (typeof response === 'object') { finalOutput = JSON.stringify(response, null, 2) } - if (isStreamable) { - const sseStreamer: IServerSideEventStreamer = options.sseStreamer - sseStreamer.streamTokenEvent(chatId, finalOutput) - } - // Process template variables in state if (newState && Object.keys(newState).length > 0) { for (const key in newState) { diff --git a/packages/components/nodes/documentloaders/CustomDocumentLoader/CustomDocumentLoader.ts b/packages/components/nodes/documentloaders/CustomDocumentLoader/CustomDocumentLoader.ts index 18f58c25d..1c1b3a26f 100644 --- a/packages/components/nodes/documentloaders/CustomDocumentLoader/CustomDocumentLoader.ts +++ b/packages/components/nodes/documentloaders/CustomDocumentLoader/CustomDocumentLoader.ts @@ -1,7 +1,6 @@ import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' -import { NodeVM } from '@flowiseai/nodevm' import { DataSource } from 'typeorm' -import { availableDependencies, defaultAllowBuiltInDep, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils' +import { getVars, handleEscapeCharacters, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' class CustomDocumentLoader_DocumentLoaders implements INode { label: string @@ -106,44 +105,23 @@ class CustomDocumentLoader_DocumentLoaders implements INode { } } - let sandbox: any = { - $input: input, - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined - } - sandbox['$vars'] = prepareSandboxVars(variables) - sandbox['$flow'] = flow + // Create additional sandbox variables + const additionalSandbox: ICommonObject = {} + // Add input variables to sandbox if (Object.keys(inputVars).length) { for (const item in inputVars) { - sandbox[`$${item}`] = inputVars[item] + additionalSandbox[`$${item}`] = inputVars[item] } } - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) + const sandbox = createCodeExecutionSandbox(input, variables, flow, additionalSandbox) - const nodeVMOptions = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, - timeout: 10000 - } as any - - const vm = new NodeVM(nodeVMOptions) try { - const response = await vm.run(`module.exports = async function() {${javascriptFunction}}()`, __dirname) + const response = await executeJavaScriptCode(javascriptFunction, sandbox, { + libraries: ['axios'], + timeout: 10000 + }) if (output === 'document' && Array.isArray(response)) { if (response.length === 0) return response diff --git a/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts b/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts index 5cde62c43..d4a7847ee 100644 --- a/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts +++ b/packages/components/nodes/prompts/ChatPromptTemplate/ChatPromptTemplate.ts @@ -1,7 +1,6 @@ import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams } from '../../../src/Interface' -import { getBaseClasses, transformBracesWithColon } from '../../../src/utils' +import { getBaseClasses, transformBracesWithColon, getVars, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' import { ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate } from '@langchain/core/prompts' -import { getVM } from '../../sequentialagents/commonUtils' import { DataSource } from 'typeorm' const defaultFunc = `const { AIMessage, HumanMessage, ToolMessage } = require('@langchain/core/messages'); @@ -120,13 +119,29 @@ class ChatPromptTemplate_Prompts implements INode { ) { const appDataSource = options.appDataSource as DataSource const databaseEntities = options.databaseEntities as IDatabaseEntity - const vm = await getVM(appDataSource, databaseEntities, nodeData, options, {}) + const variables = await getVars(appDataSource, databaseEntities, nodeData, options) + const flow = { + chatflowId: options.chatflowid, + sessionId: options.sessionId, + chatId: options.chatId + } + + const sandbox = createCodeExecutionSandbox('', variables, flow) + try { - const response = await vm.run(`module.exports = async function() {${messageHistoryCode}}()`, __dirname) - if (!Array.isArray(response)) throw new Error('Returned message history must be an array') + const response = await executeJavaScriptCode(messageHistoryCode, sandbox, { + libraries: ['axios', '@langchain/core'], + timeout: 10000 + }) + + const parsedResponse = JSON.parse(response) + + if (!Array.isArray(parsedResponse)) { + throw new Error('Returned message history must be an array') + } prompt = ChatPromptTemplate.fromMessages([ SystemMessagePromptTemplate.fromTemplate(systemMessagePrompt), - ...response, + ...parsedResponse, HumanMessagePromptTemplate.fromTemplate(humanMessagePrompt) ]) } catch (e) { diff --git a/packages/components/nodes/sequentialagents/Agent/Agent.ts b/packages/components/nodes/sequentialagents/Agent/Agent.ts index 0ad1372c1..94812c492 100644 --- a/packages/components/nodes/sequentialagents/Agent/Agent.ts +++ b/packages/components/nodes/sequentialagents/Agent/Agent.ts @@ -36,11 +36,12 @@ import { handleEscapeCharacters, prepareSandboxVars, removeInvalidImageMarkdown, - transformBracesWithColon + transformBracesWithColon, + executeJavaScriptCode, + createCodeExecutionSandbox } from '../../../src/utils' import { customGet, - getVM, processImageMessage, transformObjectPropertyToFunction, filterConversationHistory, @@ -936,9 +937,13 @@ const getReturnOutput = async (nodeData: INodeData, input: string, options: ICom throw new Error(e) } } else if (selectedTab === 'updateStateMemoryCode' && updateStateMemoryCode) { - const vm = await getVM(appDataSource, databaseEntities, nodeData, options, flow) + const sandbox = createCodeExecutionSandbox(input, variables, flow) + try { - const response = await vm.run(`module.exports = async function() {${updateStateMemoryCode}}()`, __dirname) + const response = await executeJavaScriptCode(updateStateMemoryCode, sandbox, { + timeout: 10000 + }) + if (typeof response !== 'object') throw new Error('Return output must be an object') return response } catch (e) { diff --git a/packages/components/nodes/sequentialagents/Condition/Condition.ts b/packages/components/nodes/sequentialagents/Condition/Condition.ts index 1f855091f..9ce4d6928 100644 --- a/packages/components/nodes/sequentialagents/Condition/Condition.ts +++ b/packages/components/nodes/sequentialagents/Condition/Condition.ts @@ -10,8 +10,8 @@ import { ISeqAgentNode, ISeqAgentsState } from '../../../src/Interface' -import { checkCondition, customGet, getVM } from '../commonUtils' -import { getVars, prepareSandboxVars } from '../../../src/utils' +import { checkCondition, customGet } from '../commonUtils' +import { getVars, prepareSandboxVars, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' const howToUseCode = ` 1. Must return a string value at the end of function. For example: @@ -279,9 +279,13 @@ const runCondition = async (nodeData: INodeData, input: string, options: ICommon } if (selectedTab === 'conditionFunction' && conditionFunction) { - const vm = await getVM(appDataSource, databaseEntities, nodeData, options, flow) + const sandbox = createCodeExecutionSandbox(input, variables, flow) + try { - const response = await vm.run(`module.exports = async function() {${conditionFunction}}()`, __dirname) + const response = await executeJavaScriptCode(conditionFunction, sandbox, { + timeout: 10000 + }) + if (typeof response !== 'string') throw new Error('Condition function must return a string') return response } catch (e) { diff --git a/packages/components/nodes/sequentialagents/ConditionAgent/ConditionAgent.ts b/packages/components/nodes/sequentialagents/ConditionAgent/ConditionAgent.ts index 1b0db13a2..4aaf3b97d 100644 --- a/packages/components/nodes/sequentialagents/ConditionAgent/ConditionAgent.ts +++ b/packages/components/nodes/sequentialagents/ConditionAgent/ConditionAgent.ts @@ -16,12 +16,19 @@ import { ISeqAgentNode, ISeqAgentsState } from '../../../src/Interface' -import { getInputVariables, getVars, handleEscapeCharacters, prepareSandboxVars, transformBracesWithColon } from '../../../src/utils' +import { + getInputVariables, + getVars, + handleEscapeCharacters, + prepareSandboxVars, + transformBracesWithColon, + executeJavaScriptCode, + createCodeExecutionSandbox +} from '../../../src/utils' import { checkCondition, convertStructuredSchemaToZod, customGet, - getVM, transformObjectPropertyToFunction, filterConversationHistory, restructureMessages @@ -539,9 +546,13 @@ const runCondition = async ( } if (selectedTab === 'conditionFunction' && conditionFunction) { - const vm = await getVM(appDataSource, databaseEntities, nodeData, options, flow) + const sandbox = createCodeExecutionSandbox(input, variables, flow) + try { - const response = await vm.run(`module.exports = async function() {${conditionFunction}}()`, __dirname) + const response = await executeJavaScriptCode(conditionFunction, sandbox, { + timeout: 10000 + }) + if (typeof response !== 'string') throw new Error('Condition function must return a string') return response } catch (e) { diff --git a/packages/components/nodes/sequentialagents/CustomFunction/CustomFunction.ts b/packages/components/nodes/sequentialagents/CustomFunction/CustomFunction.ts index 493102b0b..c104a74f9 100644 --- a/packages/components/nodes/sequentialagents/CustomFunction/CustomFunction.ts +++ b/packages/components/nodes/sequentialagents/CustomFunction/CustomFunction.ts @@ -1,6 +1,5 @@ -import { NodeVM } from '@flowiseai/nodevm' import { DataSource } from 'typeorm' -import { availableDependencies, defaultAllowBuiltInDep, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils' +import { getVars, handleEscapeCharacters, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams, ISeqAgentNode, ISeqAgentsState } from '../../../src/Interface' import { AIMessage, BaseMessage, HumanMessage } from '@langchain/core/messages' import { customGet } from '../commonUtils' @@ -154,44 +153,22 @@ class CustomFunction_SeqAgents implements INode { } } - let sandbox: any = { - $input: input, - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined - } - sandbox['$vars'] = prepareSandboxVars(variables) - sandbox['$flow'] = flow + // Create additional sandbox variables + const additionalSandbox: ICommonObject = {} + // Add input variables to sandbox if (Object.keys(inputVars).length) { for (const item in inputVars) { - sandbox[`$${item}`] = inputVars[item] + additionalSandbox[`$${item}`] = inputVars[item] } } - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) + const sandbox = createCodeExecutionSandbox(input, variables, flow, additionalSandbox) - const nodeVMOptions = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, - timeout: 10000 - } as any - - const vm = new NodeVM(nodeVMOptions) try { - const response = await vm.run(`module.exports = async function() {${javascriptFunction}}()`, __dirname) + const response = await executeJavaScriptCode(javascriptFunction, sandbox, { + timeout: 10000 + }) if (returnValueAs === 'stateObj') { if (typeof response !== 'object') { diff --git a/packages/components/nodes/sequentialagents/ExecuteFlow/ExecuteFlow.ts b/packages/components/nodes/sequentialagents/ExecuteFlow/ExecuteFlow.ts index 919b7e7a2..6b46c05e6 100644 --- a/packages/components/nodes/sequentialagents/ExecuteFlow/ExecuteFlow.ts +++ b/packages/components/nodes/sequentialagents/ExecuteFlow/ExecuteFlow.ts @@ -1,13 +1,6 @@ -import { NodeVM } from '@flowiseai/nodevm' import { DataSource } from 'typeorm' -import { - availableDependencies, - defaultAllowBuiltInDep, - getCredentialData, - getCredentialParam, - getVars, - prepareSandboxVars -} from '../../../src/utils' +import { getCredentialData, getCredentialParam, getVars, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' +import { isValidUUID, isValidURL } from '../../../src/validator' import { ICommonObject, IDatabaseEntity, @@ -177,6 +170,16 @@ class ExecuteFlow_SeqAgents implements INode { const baseURL = (nodeData.inputs?.baseURL as string) || (options.baseURL as string) const returnValueAs = nodeData.inputs?.returnValueAs as string + // Validate selectedFlowId is a valid UUID + if (!selectedFlowId || !isValidUUID(selectedFlowId)) { + throw new Error('Invalid flow ID: must be a valid UUID') + } + + // Validate baseURL is a valid URL + if (!baseURL || !isValidURL(baseURL)) { + throw new Error('Invalid base URL: must be a valid URL') + } + const credentialData = await getCredentialData(nodeData.credential ?? '', options) const chatflowApiKey = getCredentialParam('chatflowApiKey', credentialData, nodeData) @@ -233,18 +236,13 @@ class ExecuteFlow_SeqAgents implements INode { body: JSON.stringify(body) } - let sandbox: ICommonObject = { - $input: flowInput, + // Create additional sandbox variables + const additionalSandbox: ICommonObject = { $callOptions: callOptions, - $callBody: body, - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined + $callBody: body } - sandbox['$vars'] = prepareSandboxVars(variables) - sandbox['$flow'] = flow + + const sandbox = createCodeExecutionSandbox(flowInput, variables, flow, additionalSandbox) const code = ` const fetch = require('node-fetch'); @@ -264,27 +262,11 @@ class ExecuteFlow_SeqAgents implements INode { } ` - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) - - const nodeVMOptions = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, - timeout: 10000 - } as any - - const vm = new NodeVM(nodeVMOptions) try { - let response = await vm.run(`module.exports = async function() {${code}}()`, __dirname) + let response = await executeJavaScriptCode(code, sandbox, { + useSandbox: false, + timeout: 10000 + }) if (typeof response === 'object') { response = JSON.stringify(response) diff --git a/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts b/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts index 3a4edb0be..f52fe2c87 100644 --- a/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts +++ b/packages/components/nodes/sequentialagents/LLMNode/LLMNode.ts @@ -24,12 +24,13 @@ import { getVars, handleEscapeCharacters, prepareSandboxVars, - transformBracesWithColon + transformBracesWithColon, + executeJavaScriptCode, + createCodeExecutionSandbox } from '../../../src/utils' import { convertStructuredSchemaToZod, customGet, - getVM, processImageMessage, transformObjectPropertyToFunction, filterConversationHistory, @@ -708,9 +709,13 @@ const getReturnOutput = async (nodeData: INodeData, input: string, options: ICom throw new Error(e) } } else if (selectedTab === 'updateStateMemoryCode' && updateStateMemoryCode) { - const vm = await getVM(appDataSource, databaseEntities, nodeData, options, flow) + const sandbox = createCodeExecutionSandbox(input, variables, flow) + try { - const response = await vm.run(`module.exports = async function() {${updateStateMemoryCode}}()`, __dirname) + const response = await executeJavaScriptCode(updateStateMemoryCode, sandbox, { + timeout: 10000 + }) + if (typeof response !== 'object') throw new Error('Return output must be an object') return response } catch (e) { diff --git a/packages/components/nodes/sequentialagents/State/State.ts b/packages/components/nodes/sequentialagents/State/State.ts index 92e0769d5..01e07e6e8 100644 --- a/packages/components/nodes/sequentialagents/State/State.ts +++ b/packages/components/nodes/sequentialagents/State/State.ts @@ -1,13 +1,16 @@ import { START } from '@langchain/langgraph' -import { NodeVM } from '@flowiseai/nodevm' import { DataSource } from 'typeorm' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams, ISeqAgentNode } from '../../../src/Interface' -import { availableDependencies, defaultAllowBuiltInDep, getVars, prepareSandboxVars } from '../../../src/utils' +import { getVars, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' const defaultFunc = `{ aggregate: { value: (x, y) => x.concat(y), // here we append the new message to the existing messages default: () => [] + }, + replacedValue: { + value: (x, y) => y ?? x, + default: () => null } }` @@ -198,37 +201,13 @@ class State_SeqAgents implements INode { input } - let sandbox: any = { - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined - } - sandbox['$vars'] = prepareSandboxVars(variables) - sandbox['$flow'] = flow + const sandbox = createCodeExecutionSandbox('', variables, flow) - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) - - const nodeVMOptions = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, - timeout: 10000 - } as any - - const vm = new NodeVM(nodeVMOptions) try { - const response = await vm.run(`module.exports = async function() {return ${stateMemoryCode}}()`, __dirname) + const response = await executeJavaScriptCode(`return ${stateMemoryCode}`, sandbox, { + timeout: 10000 + }) + if (typeof response !== 'object') throw new Error('State must be an object') const returnOutput: ISeqAgentNode = { id: nodeData.id, diff --git a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts index 7ab010c1a..3c126793d 100644 --- a/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts +++ b/packages/components/nodes/sequentialagents/ToolNode/ToolNode.ts @@ -15,8 +15,8 @@ import { RunnableConfig } from '@langchain/core/runnables' import { ARTIFACTS_PREFIX, SOURCE_DOCUMENTS_PREFIX, TOOL_ARGS_PREFIX } from '../../../src/agents' import { Document } from '@langchain/core/documents' import { DataSource } from 'typeorm' -import { MessagesState, RunnableCallable, customGet, getVM } from '../commonUtils' -import { getVars, prepareSandboxVars } from '../../../src/utils' +import { MessagesState, RunnableCallable, customGet } from '../commonUtils' +import { getVars, prepareSandboxVars, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' import { ChatPromptTemplate } from '@langchain/core/prompts' const defaultApprovalPrompt = `You are about to execute tool: {tools}. Ask if user want to proceed` @@ -572,9 +572,13 @@ const getReturnOutput = async ( throw new Error(e) } } else if (selectedTab === 'updateStateMemoryCode' && updateStateMemoryCode) { - const vm = await getVM(appDataSource, databaseEntities, nodeData, options, flow) + const sandbox = createCodeExecutionSandbox(input, variables, flow) + try { - const response = await vm.run(`module.exports = async function() {${updateStateMemoryCode}}()`, __dirname) + const response = await executeJavaScriptCode(updateStateMemoryCode, sandbox, { + timeout: 10000 + }) + if (typeof response !== 'object') throw new Error('Return output must be an object') return response } catch (e) { diff --git a/packages/components/nodes/sequentialagents/commonUtils.ts b/packages/components/nodes/sequentialagents/commonUtils.ts index 5769da816..42456bc4f 100644 --- a/packages/components/nodes/sequentialagents/commonUtils.ts +++ b/packages/components/nodes/sequentialagents/commonUtils.ts @@ -1,7 +1,6 @@ import { get } from 'lodash' import { z } from 'zod' import { DataSource } from 'typeorm' -import { NodeVM } from '@flowiseai/nodevm' import { StructuredTool } from '@langchain/core/tools' import { ChatMistralAI } from '@langchain/mistralai' import { ChatAnthropic } from '@langchain/anthropic' @@ -17,7 +16,7 @@ import { IVisionChatModal, ConversationHistorySelection } from '../../src/Interface' -import { availableDependencies, defaultAllowBuiltInDep, getVars, prepareSandboxVars } from '../../src/utils' +import { getVars, executeJavaScriptCode, createCodeExecutionSandbox } from '../../src/utils' import { ChatPromptTemplate, BaseMessagePromptTemplateLike } from '@langchain/core/prompts' export const checkCondition = (input: string | number | undefined, condition: string, value: string | number = ''): boolean => { @@ -150,46 +149,6 @@ export const processImageMessage = async (llm: BaseChatModel, nodeData: INodeDat return multiModalMessageContent } -export const getVM = async ( - appDataSource: DataSource, - databaseEntities: IDatabaseEntity, - nodeData: INodeData, - options: ICommonObject, - flow: ICommonObject -) => { - const variables = await getVars(appDataSource, databaseEntities, nodeData, options) - - let sandbox: any = { - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined - } - sandbox['$vars'] = prepareSandboxVars(variables) - sandbox['$flow'] = flow - - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) - - const nodeVMOptions = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, - timeout: 10000 - } as any - - return new NodeVM(nodeVMOptions) -} - export const customGet = (obj: any, path: string) => { if (path.includes('[-1]')) { const parts = path.split('.') @@ -426,9 +385,21 @@ export const checkMessageHistory = async ( if (messageHistory) { const appDataSource = options.appDataSource as DataSource const databaseEntities = options.databaseEntities as IDatabaseEntity - const vm = await getVM(appDataSource, databaseEntities, nodeData, options, {}) + + const variables = await getVars(appDataSource, databaseEntities, nodeData, options) + const flow = { + chatflowId: options.chatflowid, + sessionId: options.sessionId, + chatId: options.chatId + } + + const sandbox = createCodeExecutionSandbox('', variables, flow) + try { - const response = await vm.run(`module.exports = async function() {${messageHistory}}()`, __dirname) + const response = await executeJavaScriptCode(messageHistory, sandbox, { + timeout: 10000 + }) + if (!Array.isArray(response)) throw new Error('Returned message history must be an array') if (sysPrompt) { // insert at index 1 diff --git a/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts b/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts index 5f3d7e13b..cedf338e6 100644 --- a/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts +++ b/packages/components/nodes/tools/AgentAsTool/AgentAsTool.ts @@ -1,11 +1,11 @@ import { DataSource } from 'typeorm' import { z } from 'zod' -import { NodeVM } from '@flowiseai/nodevm' import { RunnableConfig } from '@langchain/core/runnables' import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' import { StructuredTool } from '@langchain/core/tools' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' -import { availableDependencies, defaultAllowBuiltInDep, getCredentialData, getCredentialParam } from '../../../src/utils' +import { getCredentialData, getCredentialParam, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' +import { isValidUUID, isValidURL } from '../../../src/validator' import { v4 as uuidv4 } from 'uuid' class AgentAsTool_Tools implements INode { @@ -160,6 +160,16 @@ class AgentAsTool_Tools implements INode { const baseURL = (nodeData.inputs?.baseURL as string) || (options.baseURL as string) + // Validate agentflowid is a valid UUID + if (!selectedAgentflowId || !isValidUUID(selectedAgentflowId)) { + throw new Error('Invalid agentflow ID: must be a valid UUID') + } + + // Validate baseURL is a valid URL + if (!baseURL || !isValidURL(baseURL)) { + throw new Error('Invalid base URL: must be a valid URL') + } + const credentialData = await getCredentialData(nodeData.credential ?? '', options) const agentflowApiKey = getCredentialParam('agentflowApiKey', credentialData, nodeData) @@ -326,16 +336,6 @@ class AgentflowTool extends StructuredTool { body: JSON.stringify(body) } - let sandbox = { - $callOptions: options, - $callBody: body, - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined - } - const code = ` const fetch = require('node-fetch'); const url = "${this.baseURL}/api/v1/prediction/${this.agentflowid}"; @@ -353,26 +353,19 @@ try { return ''; } ` - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) - const vmOptions = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, + // Create additional sandbox variables + const additionalSandbox: ICommonObject = { + $callOptions: options, + $callBody: body + } + + const sandbox = createCodeExecutionSandbox('', [], {}, additionalSandbox) + + const response = await executeJavaScriptCode(code, sandbox, { + useSandbox: false, timeout: 10000 - } as any - - const vm = new NodeVM(vmOptions) - const response = await vm.run(`module.exports = async function() {${code}}()`, __dirname) + }) return response } diff --git a/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts b/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts index 993d12aeb..b9d0d449a 100644 --- a/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts +++ b/packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts @@ -1,11 +1,11 @@ import { DataSource } from 'typeorm' import { z } from 'zod' -import { NodeVM } from '@flowiseai/nodevm' import { RunnableConfig } from '@langchain/core/runnables' import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' import { StructuredTool } from '@langchain/core/tools' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../src/Interface' -import { availableDependencies, defaultAllowBuiltInDep, getCredentialData, getCredentialParam } from '../../../src/utils' +import { getCredentialData, getCredentialParam, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' +import { isValidUUID, isValidURL } from '../../../src/validator' import { v4 as uuidv4 } from 'uuid' class ChatflowTool_Tools implements INode { @@ -168,6 +168,16 @@ class ChatflowTool_Tools implements INode { const baseURL = (nodeData.inputs?.baseURL as string) || (options.baseURL as string) + // Validate selectedChatflowId is a valid UUID + if (!selectedChatflowId || !isValidUUID(selectedChatflowId)) { + throw new Error('Invalid chatflow ID: must be a valid UUID') + } + + // Validate baseURL is a valid URL + if (!baseURL || !isValidURL(baseURL)) { + throw new Error('Invalid base URL: must be a valid URL') + } + const credentialData = await getCredentialData(nodeData.credential ?? '', options) const chatflowApiKey = getCredentialParam('chatflowApiKey', credentialData, nodeData) @@ -334,16 +344,6 @@ class ChatflowTool extends StructuredTool { body: JSON.stringify(body) } - let sandbox = { - $callOptions: options, - $callBody: body, - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined - } - const code = ` const fetch = require('node-fetch'); const url = "${this.baseURL}/api/v1/prediction/${this.chatflowid}"; @@ -361,26 +361,19 @@ try { return ''; } ` - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) - const vmOptions = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, + // Create additional sandbox variables + const additionalSandbox: ICommonObject = { + $callOptions: options, + $callBody: body + } + + const sandbox = createCodeExecutionSandbox('', [], {}, additionalSandbox) + + const response = await executeJavaScriptCode(code, sandbox, { + useSandbox: false, timeout: 10000 - } as any - - const vm = new NodeVM(vmOptions) - const response = await vm.run(`module.exports = async function() {${code}}()`, __dirname) + }) return response } diff --git a/packages/components/nodes/tools/CodeInterpreterE2B/CodeInterpreterE2B.ts b/packages/components/nodes/tools/CodeInterpreterE2B/CodeInterpreterE2B.ts index 8d60a823a..5a03ce188 100644 --- a/packages/components/nodes/tools/CodeInterpreterE2B/CodeInterpreterE2B.ts +++ b/packages/components/nodes/tools/CodeInterpreterE2B/CodeInterpreterE2B.ts @@ -1,7 +1,7 @@ import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface' import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils' import { StructuredTool, ToolInputParsingException, ToolParams } from '@langchain/core/tools' -import { CodeInterpreter } from '@e2b/code-interpreter' +import { Sandbox } from '@e2b/code-interpreter' import { z } from 'zod' import { addSingleFileToStorage } from '../../../src/storageUtils' import { CallbackManager, CallbackManagerForToolRun, Callbacks, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' @@ -107,7 +107,7 @@ export class E2BTool extends StructuredTool { description = DESC - instance: CodeInterpreter + instance: Sandbox apiKey: string @@ -204,8 +204,8 @@ export class E2BTool extends StructuredTool { flowConfig = { ...this.flowObj, ...flowConfig } try { if ('input' in arg) { - this.instance = await CodeInterpreter.create({ apiKey: this.apiKey }) - const execution = await this.instance.notebook.execCell(arg?.input) + this.instance = await Sandbox.create({ apiKey: this.apiKey }) + const execution = await this.instance.runCode(arg?.input, { language: 'python' }) const artifacts = [] for (const result of execution.results) { @@ -251,8 +251,6 @@ export class E2BTool extends StructuredTool { } } - this.instance.close() - let output = '' if (execution.text) output = execution.text @@ -267,7 +265,7 @@ export class E2BTool extends StructuredTool { return 'No input provided' } } catch (e) { - if (this.instance) this.instance.close() + if (this.instance) this.instance.kill() return typeof e === 'string' ? e : JSON.stringify(e, null, 2) } } diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index e64f776b8..f27f36ff6 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -1,9 +1,8 @@ import { z } from 'zod' -import { NodeVM } from '@flowiseai/nodevm' import { RunnableConfig } from '@langchain/core/runnables' import { StructuredTool, ToolParams } from '@langchain/core/tools' import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' -import { availableDependencies, defaultAllowBuiltInDep, prepareSandboxVars } from '../../../src/utils' +import { executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' import { ICommonObject } from '../../../src/Interface' class ToolInputParsingException extends Error { @@ -111,46 +110,23 @@ export class DynamicStructuredTool< _?: CallbackManagerForToolRun, flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject } ): Promise { - let sandbox: any = { - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined - } + // Create additional sandbox variables for tool arguments + const additionalSandbox: ICommonObject = {} + if (typeof arg === 'object' && Object.keys(arg).length) { for (const item in arg) { - sandbox[`$${item}`] = arg[item] + additionalSandbox[`$${item}`] = arg[item] } } - sandbox['$vars'] = prepareSandboxVars(this.variables) + // Prepare flow object for sandbox + const flow = this.flowObj ? { ...this.flowObj, ...flowConfig } : {} - // inject flow properties - if (this.flowObj) { - sandbox['$flow'] = { ...this.flowObj, ...flowConfig } - } + const sandbox = createCodeExecutionSandbox('', this.variables || [], flow, additionalSandbox) - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) - - const options = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, + const response = await executeJavaScriptCode(this.code, sandbox, { timeout: 10000 - } as any - - const vm = new NodeVM(options) - const response = await vm.run(`module.exports = async function() {${this.code}}()`, __dirname) + }) return response } diff --git a/packages/components/nodes/tools/OpenAPIToolkit/core.ts b/packages/components/nodes/tools/OpenAPIToolkit/core.ts index f7701770e..491033ce7 100644 --- a/packages/components/nodes/tools/OpenAPIToolkit/core.ts +++ b/packages/components/nodes/tools/OpenAPIToolkit/core.ts @@ -1,10 +1,9 @@ import { z } from 'zod' import { RequestInit } from 'node-fetch' -import { NodeVM } from '@flowiseai/nodevm' import { RunnableConfig } from '@langchain/core/runnables' import { StructuredTool, ToolParams } from '@langchain/core/tools' import { CallbackManagerForToolRun, Callbacks, CallbackManager, parseCallbackConfigArg } from '@langchain/core/callbacks/manager' -import { availableDependencies, defaultAllowBuiltInDep, prepareSandboxVars } from '../../../src/utils' +import { executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' import { ICommonObject } from '../../../src/Interface' const removeNulls = (obj: Record) => { @@ -217,32 +216,22 @@ export class DynamicStructuredTool< _?: CallbackManagerForToolRun, flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject } ): Promise { - let sandbox: any = { - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined - } let processedArg = { ...arg } if (this.removeNulls && typeof processedArg === 'object' && processedArg !== null) { processedArg = removeNulls(processedArg) } + // Create additional sandbox variables for tool arguments + const additionalSandbox: ICommonObject = {} + if (typeof processedArg === 'object' && Object.keys(processedArg).length) { for (const item in processedArg) { - sandbox[`$${item}`] = processedArg[item] + additionalSandbox[`$${item}`] = processedArg[item] } } - sandbox['$vars'] = prepareSandboxVars(this.variables) - - // inject flow properties - if (this.flowObj) { - sandbox['$flow'] = { ...this.flowObj, ...flowConfig } - } - + // Prepare HTTP request options const callOptions: RequestInit = { method: this.method, headers: { @@ -253,31 +242,20 @@ export class DynamicStructuredTool< if (arg.RequestBody && this.method.toUpperCase() !== 'GET') { callOptions.body = JSON.stringify(arg.RequestBody) } - sandbox['$options'] = callOptions + additionalSandbox['$options'] = callOptions + // Generate complete URL const completeUrl = getUrl(this.baseUrl, arg) - sandbox['$url'] = completeUrl + additionalSandbox['$url'] = completeUrl - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) + // Prepare flow object for sandbox + const flow = this.flowObj ? { ...this.flowObj, ...flowConfig } : {} - const options = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, + const sandbox = createCodeExecutionSandbox('', this.variables || [], flow, additionalSandbox) + + const response = await executeJavaScriptCode(this.customCode || defaultCode, sandbox, { timeout: 10000 - } as any - - const vm = new NodeVM(options) - const response = await vm.run(`module.exports = async function() {${this.customCode || defaultCode}}()`, __dirname) + }) return response } diff --git a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts index cd09b2717..cfae8b6d3 100644 --- a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts +++ b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts @@ -1,8 +1,7 @@ import { flatten } from 'lodash' import { type StructuredTool } from '@langchain/core/tools' -import { NodeVM } from '@flowiseai/nodevm' import { DataSource } from 'typeorm' -import { availableDependencies, defaultAllowBuiltInDep, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils' +import { getVars, handleEscapeCharacters, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' class CustomFunction_Utilities implements INode { @@ -118,45 +117,24 @@ class CustomFunction_Utilities implements INode { } } - let sandbox: any = { - $input: input, - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined + // Create additional sandbox variables + const additionalSandbox: ICommonObject = { + $tools: tools } - sandbox['$vars'] = prepareSandboxVars(variables) - sandbox['$flow'] = flow - sandbox['$tools'] = tools + // Add input variables to sandbox if (Object.keys(inputVars).length) { for (const item in inputVars) { - sandbox[`$${item}`] = inputVars[item] + additionalSandbox[`$${item}`] = inputVars[item] } } - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) + const sandbox = createCodeExecutionSandbox(input, variables, flow, additionalSandbox) - const nodeVMOptions = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, - timeout: 10000 - } as any - - const vm = new NodeVM(nodeVMOptions) try { - const response = await vm.run(`module.exports = async function() {${javascriptFunction}}()`, __dirname) + const response = await executeJavaScriptCode(javascriptFunction, sandbox, { + timeout: 10000 + }) if (typeof response === 'string' && !isEndingNode) { return handleEscapeCharacters(response, false) diff --git a/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts b/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts index 3eb15ffeb..f74395d3a 100644 --- a/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts +++ b/packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts @@ -1,6 +1,5 @@ -import { NodeVM } from '@flowiseai/nodevm' import { DataSource } from 'typeorm' -import { availableDependencies, defaultAllowBuiltInDep, getVars, handleEscapeCharacters, prepareSandboxVars } from '../../../src/utils' +import { getVars, handleEscapeCharacters, executeJavaScriptCode, createCodeExecutionSandbox } from '../../../src/utils' import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' class IfElseFunction_Utilities implements INode { @@ -119,48 +118,30 @@ class IfElseFunction_Utilities implements INode { } } - let sandbox: any = { - $input: input, - util: undefined, - Symbol: undefined, - child_process: undefined, - fs: undefined, - process: undefined - } - sandbox['$vars'] = prepareSandboxVars(variables) - sandbox['$flow'] = flow + // Create additional sandbox variables + const additionalSandbox: ICommonObject = {} + // Add input variables to sandbox if (Object.keys(inputVars).length) { for (const item in inputVars) { - sandbox[`$${item}`] = inputVars[item] + additionalSandbox[`$${item}`] = inputVars[item] } } - const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP - ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) - : defaultAllowBuiltInDep - const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] - const deps = availableDependencies.concat(externalDeps) + const sandbox = createCodeExecutionSandbox(input, variables, flow, additionalSandbox) - const nodeVMOptions = { - console: 'inherit', - sandbox, - require: { - external: { modules: deps }, - builtin: builtinDeps - }, - eval: false, - wasm: false, - timeout: 10000 - } as any - - const vm = new NodeVM(nodeVMOptions) try { - const responseTrue = await vm.run(`module.exports = async function() {${ifFunction}}()`, __dirname) + const responseTrue = await executeJavaScriptCode(ifFunction, sandbox, { + timeout: 10000 + }) + if (responseTrue) return { output: typeof responseTrue === 'string' ? handleEscapeCharacters(responseTrue, false) : responseTrue, type: true } - const responseFalse = await vm.run(`module.exports = async function() {${elseFunction}}()`, __dirname) + const responseFalse = await executeJavaScriptCode(elseFunction, sandbox, { + timeout: 10000 + }) + return { output: typeof responseFalse === 'string' ? handleEscapeCharacters(responseFalse, false) : responseFalse, type: false } } catch (e) { throw new Error(e) diff --git a/packages/components/package.json b/packages/components/package.json index 550d9ee86..6ce136913 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -26,7 +26,7 @@ "@aws-sdk/client-secrets-manager": "^3.699.0", "@datastax/astra-db-ts": "1.5.0", "@dqbd/tiktoken": "^1.0.21", - "@e2b/code-interpreter": "^0.0.5", + "@e2b/code-interpreter": "^1.5.1", "@elastic/elasticsearch": "^8.9.0", "@flowiseai/nodevm": "^3.9.25", "@getzep/zep-cloud": "~1.0.7", diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 5947d4fa1..a8cf314ba 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -16,6 +16,8 @@ import { GetSecretValueCommand, SecretsManagerClient, SecretsManagerClientConfig import { customGet } from '../nodes/sequentialagents/commonUtils' import { TextSplitter } from 'langchain/text_splitter' import { DocumentLoader } from 'langchain/document_loaders/base' +import { NodeVM } from '@flowiseai/nodevm' +import { Sandbox } from '@e2b/code-interpreter' export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}} export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank @@ -1348,3 +1350,227 @@ export const stripHTMLFromToolInput = (input: string) => { cleanedInput = cleanedInput.replace(/\\_/g, '_') return cleanedInput } + +// Helper function to convert require statements to ESM imports +const convertRequireToImport = (requireLine: string): string | null => { + // Remove leading/trailing whitespace and get the indentation + const indent = requireLine.match(/^(\s*)/)?.[1] || '' + const trimmed = requireLine.trim() + + // Match patterns like: const/let/var name = require('module') + const defaultRequireMatch = trimmed.match(/^(const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/) + if (defaultRequireMatch) { + const [, , varName, moduleName] = defaultRequireMatch + return `${indent}import ${varName} from '${moduleName}';` + } + + // Match patterns like: const { name1, name2 } = require('module') + const destructureMatch = trimmed.match(/^(const|let|var)\s+\{\s*([^}]+)\s*\}\s*=\s*require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/) + if (destructureMatch) { + const [, , destructuredVars, moduleName] = destructureMatch + return `${indent}import { ${destructuredVars.trim()} } from '${moduleName}';` + } + + // Match patterns like: const name = require('module').property + const propertyMatch = trimmed.match(/^(const|let|var)\s+(\w+)\s*=\s*require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)\.(\w+)/) + if (propertyMatch) { + const [, , varName, moduleName, property] = propertyMatch + return `${indent}import { ${property} as ${varName} } from '${moduleName}';` + } + + // If no pattern matches, return null to skip conversion + return null +} + +/** + * Execute JavaScript code using either Sandbox or NodeVM + * @param {string} code - The JavaScript code to execute + * @param {ICommonObject} sandbox - The sandbox object with variables + * @param {ICommonObject} options - Execution options + * @returns {Promise} - The execution result + */ +export const executeJavaScriptCode = async ( + code: string, + sandbox: ICommonObject, + options: { + timeout?: number + useSandbox?: boolean + libraries?: string[] + streamOutput?: (output: string) => void + nodeVMOptions?: ICommonObject + } = {} +): Promise => { + const { timeout = 10000, useSandbox = true, streamOutput, libraries = [], nodeVMOptions = {} } = options + const shouldUseSandbox = useSandbox && process.env.E2B_APIKEY + + if (shouldUseSandbox) { + try { + const variableDeclarations = [] + + if (sandbox['$vars']) { + variableDeclarations.push(`const $vars = ${JSON.stringify(sandbox['$vars'])};`) + } + + if (sandbox['$flow']) { + variableDeclarations.push(`const $flow = ${JSON.stringify(sandbox['$flow'])};`) + } + + // Add other sandbox variables + for (const [key, value] of Object.entries(sandbox)) { + if ( + key !== '$vars' && + key !== '$flow' && + key !== 'util' && + key !== 'Symbol' && + key !== 'child_process' && + key !== 'fs' && + key !== 'process' + ) { + variableDeclarations.push(`const ${key} = ${JSON.stringify(value)};`) + } + } + + // Handle import statements properly - they must be at the top + const lines = code.split('\n') + const importLines = [] + const otherLines = [] + + for (const line of lines) { + const trimmedLine = line.trim() + + // Skip node-fetch imports since Node.js has built-in fetch + if (trimmedLine.includes('node-fetch') || trimmedLine.includes("'fetch'") || trimmedLine.includes('"fetch"')) { + continue // Skip this line entirely + } + + // Check for existing ES6 imports and exports + if (trimmedLine.startsWith('import ') || trimmedLine.startsWith('export ')) { + importLines.push(line) + } + // Check for CommonJS require statements and convert them to ESM imports + else if (/^(const|let|var)\s+.*=\s*require\s*\(/.test(trimmedLine)) { + const convertedImport = convertRequireToImport(trimmedLine) + if (convertedImport) { + importLines.push(convertedImport) + } + } else { + otherLines.push(line) + } + } + + const sbx = await Sandbox.create({ apiKey: process.env.E2B_APIKEY }) + + // Install libraries + for (const library of libraries) { + await sbx.commands.run(`npm install ${library}`) + } + + // Separate imports from the rest of the code for proper ES6 module structure + const codeWithImports = [ + ...importLines, + `module.exports = async function() {`, + ...variableDeclarations, + ...otherLines, + `}()` + ].join('\n') + + const execution = await sbx.runCode(codeWithImports, { language: 'js' }) + + let output = '' + + if (execution.text) output = execution.text + if (!execution.text && execution.logs.stdout.length) output = execution.logs.stdout.join('\n') + + if (execution.error) { + throw new Error(`${execution.error.name}: ${execution.error.value}`) + } + + if (execution.logs.stderr.length) { + throw new Error(execution.logs.stderr.join('\n')) + } + + // Stream output if streaming function provided + if (streamOutput && output) { + streamOutput(output) + } + + // Clean up sandbox + sbx.kill() + + return output + } catch (e) { + throw new Error(`Sandbox Execution Error: ${e}`) + } + } else { + const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP + ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) + : defaultAllowBuiltInDep + const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] + const deps = availableDependencies.concat(externalDeps) + + const defaultNodeVMOptions: any = { + console: 'inherit', + sandbox, + require: { + external: { modules: deps }, + builtin: builtinDeps + }, + eval: false, + wasm: false, + timeout + } + + // Merge with custom nodeVMOptions if provided + const finalNodeVMOptions = { ...defaultNodeVMOptions, ...nodeVMOptions } + + const vm = new NodeVM(finalNodeVMOptions) + + try { + const response = await vm.run(`module.exports = async function() {${code}}()`, __dirname) + + let finalOutput = response + if (typeof response === 'object') { + finalOutput = JSON.stringify(response, null, 2) + } + + // Stream output if streaming function provided + if (streamOutput && finalOutput) { + streamOutput(finalOutput) + } + + return finalOutput + } catch (e) { + throw new Error(`NodeVM Execution Error: ${e}`) + } + } +} + +/** + * Create a standard sandbox object for code execution + * @param {string} input - The input string + * @param {ICommonObject} variables - Variables from getVars + * @param {ICommonObject} flow - Flow object with chatflowId, sessionId, etc. + * @param {ICommonObject} additionalSandbox - Additional sandbox variables + * @returns {ICommonObject} - The sandbox object + */ +export const createCodeExecutionSandbox = ( + input: string, + variables: IVariable[], + flow: ICommonObject, + additionalSandbox: ICommonObject = {} +): ICommonObject => { + const sandbox: ICommonObject = { + $input: input, + util: undefined, + Symbol: undefined, + child_process: undefined, + fs: undefined, + process: undefined, + ...additionalSandbox + } + + sandbox['$vars'] = prepareSandboxVars(variables) + sandbox['$flow'] = flow + + return sandbox +} diff --git a/packages/components/src/validator.ts b/packages/components/src/validator.ts index 4948165eb..c2ca53845 100644 --- a/packages/components/src/validator.ts +++ b/packages/components/src/validator.ts @@ -9,6 +9,20 @@ export const isValidUUID = (uuid: string): boolean => { return uuidV4Pattern.test(uuid) } +/** + * Validates if a string is a valid URL + * @param {string} url The string to validate + * @returns {boolean} True if valid URL, false otherwise + */ +export const isValidURL = (url: string): boolean => { + try { + new URL(url) + return true + } catch { + return false + } +} + /** * Validates if a string contains path traversal attempts * @param {string} path The string to validate diff --git a/packages/ui/src/views/docstore/LoaderConfigPreviewChunks.jsx b/packages/ui/src/views/docstore/LoaderConfigPreviewChunks.jsx index d53a0e725..618d67c13 100644 --- a/packages/ui/src/views/docstore/LoaderConfigPreviewChunks.jsx +++ b/packages/ui/src/views/docstore/LoaderConfigPreviewChunks.jsx @@ -183,7 +183,7 @@ const LoaderConfigPreviewChunks = () => { const previewResp = await documentStoreApi.previewChunks(config) if (previewResp.data) { setTotalChunks(previewResp.data.totalChunks) - setDocumentChunks(previewResp.data.chunks) + setDocumentChunks(Array.isArray(previewResp.data.chunks) ? previewResp.data.chunks : []) setCurrentPreviewCount(previewResp.data.previewChunkCount) } setLoading(false) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 865ce3dc1..cb20ccc44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,8 +146,8 @@ importers: specifier: ^1.0.21 version: 1.0.21 '@e2b/code-interpreter': - specifier: ^0.0.5 - version: 0.0.5(bufferutil@4.0.8)(utf-8-validate@6.0.4) + specifier: ^1.5.1 + version: 1.5.1 '@elastic/elasticsearch': specifier: ^8.9.0 version: 8.12.2 @@ -2989,6 +2989,9 @@ packages: openai: 4.96.0 zod: ^3.23.8 + '@bufbuild/protobuf@2.6.1': + resolution: { integrity: sha512-DaG6XlyKpz08bmHY5SGX2gfIllaqtDJ/KwVoxsmP22COOLYwDBe7yD3DZGwXem/Xq7QOc9cuR7R3MpAv5CFfDw== } + '@bull-board/api@6.11.0': resolution: { integrity: sha512-HLbIuXIthrgeVRmN7Vec9/7ZKWx8i1xTC6Nzi//l7ua+Xu5wn6f/aZllUNVzty5ilLTHqWFkfVOwpuN91o7yxA== } peerDependencies: @@ -3058,6 +3061,17 @@ packages: resolution: { integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA== } engines: { node: '>=0.1.90' } + '@connectrpc/connect-web@2.0.0-rc.3': + resolution: { integrity: sha512-w88P8Lsn5CCsA7MFRl2e6oLY4J/5toiNtJns/YJrlyQaWOy3RO8pDgkz+iIkG98RPMhj2thuBvsd3Cn4DKKCkw== } + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + '@connectrpc/connect': 2.0.0-rc.3 + + '@connectrpc/connect@2.0.0-rc.3': + resolution: { integrity: sha512-ARBt64yEyKbanyRETTjcjJuHr2YXorzQo0etyS5+P6oSeW8xEuzajA9g+zDnMcj1hlX2dQE93foIWQGfpru7gQ== } + peerDependencies: + '@bufbuild/protobuf': ^2.2.0 + '@couchbase/couchbase-darwin-arm64-napi@4.4.1': resolution: { integrity: sha512-YHS0TDrXe+S3nKywjZ/11Ia+25l5DypXYdyeOf4/le+zsJomnR5E3w4h1RaZ669/gvR8ltyOJn6TG//VDSp6qg== } engines: { node: '>=16' } @@ -3218,8 +3232,8 @@ packages: '@dqbd/tiktoken@1.0.21': resolution: { integrity: sha512-grBxRSY9+/iBM205EWjbMm5ySeXQrhJyXWMP38VJd+pO2DRGraDAbi4n8J8T9M4XY1M/FHgonMcmu3J+KjcX0Q== } - '@e2b/code-interpreter@0.0.5': - resolution: { integrity: sha512-ToFQ6N6EU8t91z3EJzh+mG+zf7uK8I1PRLWeu1f3bPS0pAJfM0puzBWOB3XlF9A9R5zZu/g8iO1gGPQXzXY5vA== } + '@e2b/code-interpreter@1.5.1': + resolution: { integrity: sha512-mkyKjAW2KN5Yt0R1I+1lbH3lo+W/g/1+C2lnwlitXk5wqi/g94SEO41XKdmDf5WWpKG3mnxWDR5d6S/lyjmMEw== } engines: { node: '>=18' } '@elastic/elasticsearch@8.12.2': @@ -9115,6 +9129,9 @@ packages: commondir@1.0.1: resolution: { integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== } + compare-versions@6.1.1: + resolution: { integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg== } + component-emitter@1.3.1: resolution: { integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ== } @@ -9986,8 +10003,8 @@ packages: duplexify@4.1.3: resolution: { integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA== } - e2b@0.16.1: - resolution: { integrity: sha512-2L1R/REEB+EezD4Q4MmcXXNATjvCYov2lv/69+PY6V95+wl1PZblIMTYAe7USxX6P6sqANxNs+kXqZr6RvXkSw== } + e2b@1.9.0: + resolution: { integrity: sha512-MM3RhWW7YENYocTy20BvKVcn8li/FxkDrHINS7tmz00ffl1ZavQTRxCI9Sl8ofeRg+HVMlqO4W8LJ+ij9VTZPg== } engines: { node: '>=18' } each-props@1.3.2: @@ -11992,11 +12009,6 @@ packages: isomorphic-fetch@3.0.0: resolution: { integrity: sha512-qvUtwJ3j6qwsF3jLxkZ72qCgjMysPzDfeV240JHiGZsANBYd+EEuu35v7dfrJ9Up0Ak07D7GGSkGhCHTqg/5wA== } - isomorphic-ws@5.0.0: - resolution: { integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== } - peerDependencies: - ws: 8.18.3 - isstream@0.1.2: resolution: { integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== } @@ -13919,12 +13931,14 @@ packages: zod: optional: true + openapi-fetch@0.9.8: + resolution: { integrity: sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg== } + openapi-types@12.1.3: resolution: { integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw== } - openapi-typescript-fetch@1.1.3: - resolution: { integrity: sha512-smLZPck4OkKMNExcw8jMgrMOGgVGx2N/s6DbKL2ftNl77g5HfoGpZGFy79RBzU/EkaO0OZpwBnslfdBfh7ZcWg== } - engines: { node: '>= 12.0.0', npm: '>= 7.0.0' } + openapi-typescript-helpers@0.0.8: + resolution: { integrity: sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g== } option@0.2.4: resolution: { integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A== } @@ -14135,9 +14149,6 @@ packages: resolution: { integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ== } engines: { node: '>= 0.4.0' } - path-browserify@1.0.1: - resolution: { integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== } - path-case@3.0.4: resolution: { integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg== } @@ -21782,6 +21793,8 @@ snapshots: - encoding - utf-8-validate + '@bufbuild/protobuf@2.6.1': {} + '@bull-board/api@6.11.0(@bull-board/ui@6.11.0)': dependencies: '@bull-board/ui': 6.11.0 @@ -21911,6 +21924,15 @@ snapshots: '@colors/colors@1.6.0': {} + '@connectrpc/connect-web@2.0.0-rc.3(@bufbuild/protobuf@2.6.1)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.6.1))': + dependencies: + '@bufbuild/protobuf': 2.6.1 + '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.6.1) + + '@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.6.1)': + dependencies: + '@bufbuild/protobuf': 2.6.1 + '@couchbase/couchbase-darwin-arm64-napi@4.4.1': optional: true @@ -22063,14 +22085,9 @@ snapshots: '@dqbd/tiktoken@1.0.21': {} - '@e2b/code-interpreter@0.0.5(bufferutil@4.0.8)(utf-8-validate@6.0.4)': + '@e2b/code-interpreter@1.5.1': dependencies: - e2b: 0.16.1 - isomorphic-ws: 5.0.0(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - ws: 8.18.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - transitivePeerDependencies: - - bufferutil - - utf-8-validate + e2b: 1.9.0 '@elastic/elasticsearch@8.12.2': dependencies: @@ -29311,6 +29328,8 @@ snapshots: commondir@1.0.1: {} + compare-versions@6.1.1: {} + component-emitter@1.3.1: {} component-register@0.8.3: {} @@ -30259,17 +30278,14 @@ snapshots: readable-stream: 3.6.2 stream-shift: 1.0.3 - e2b@0.16.1: + e2b@1.9.0: dependencies: - isomorphic-ws: 5.0.0(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)) - normalize-path: 3.0.0 - openapi-typescript-fetch: 1.1.3 - path-browserify: 1.0.1 + '@bufbuild/protobuf': 2.6.1 + '@connectrpc/connect': 2.0.0-rc.3(@bufbuild/protobuf@2.6.1) + '@connectrpc/connect-web': 2.0.0-rc.3(@bufbuild/protobuf@2.6.1)(@connectrpc/connect@2.0.0-rc.3(@bufbuild/protobuf@2.6.1)) + compare-versions: 6.1.1 + openapi-fetch: 0.9.8 platform: 1.3.6 - ws: 8.18.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - optionalDependencies: - bufferutil: 4.0.8 - utf-8-validate: 6.0.4 each-props@1.3.2: dependencies: @@ -32981,10 +32997,6 @@ snapshots: transitivePeerDependencies: - encoding - isomorphic-ws@5.0.0(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)): - dependencies: - ws: 8.18.3(bufferutil@4.0.8)(utf-8-validate@6.0.4) - isstream@0.1.2: {} istanbul-lib-coverage@3.2.2: {} @@ -35828,9 +35840,13 @@ snapshots: transitivePeerDependencies: - encoding + openapi-fetch@0.9.8: + dependencies: + openapi-typescript-helpers: 0.0.8 + openapi-types@12.1.3: {} - openapi-typescript-fetch@1.1.3: {} + openapi-typescript-helpers@0.0.8: {} option@0.2.4: {} @@ -36080,8 +36096,6 @@ snapshots: pause: 0.0.1 utils-merge: 1.0.1 - path-browserify@1.0.1: {} - path-case@3.0.4: dependencies: dot-case: 3.0.4