Refactor/Update code execution sandbox implementation across components (#4904)

refactor: Update code execution sandbox implementation across components

- Replaced NodeVM usage with a new createCodeExecutionSandbox function for improved sandbox management.
- Enhanced JavaScript code execution with executeJavaScriptCode function, allowing for better handling of libraries and output streaming.
- Updated multiple components to utilize the new sandboxing approach, ensuring consistent execution environment.
- Added validation for UUIDs and URLs in various tools to enhance input safety.
- Refactored input handling in CustomFunction and IfElseFunction to streamline variable management.
This commit is contained in:
Henry Heng
2025-07-21 00:09:01 +01:00
committed by GitHub
parent 9a06a85a8d
commit dca91b979b
24 changed files with 550 additions and 488 deletions
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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') {
@@ -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)
@@ -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) {
@@ -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,
@@ -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) {
@@ -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