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
@@ -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
}
@@ -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
}
@@ -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)
}
}
@@ -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<string> {
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
}
@@ -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<string, any>) => {
@@ -217,32 +216,22 @@ export class DynamicStructuredTool<
_?: CallbackManagerForToolRun,
flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject }
): Promise<string> {
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
}