Files
simstudioai--sim/apps/sim/app/api/function/execute/route.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

1937 lines
58 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { functionExecuteContract } from '@/lib/api/contracts'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
FORMAT_TO_CONTENT_TYPE,
getOutputFileDeclarations,
normalizeOutputWorkspaceFileName,
type OutputFileDeclaration,
resolveOutputFormat,
} from '@/lib/copilot/request/tools/files'
import {
validateWorkspaceFileWriteTarget,
writeWorkspaceFileByPath,
} from '@/lib/copilot/vfs/resource-writer'
import { isE2bEnabled } from '@/lib/core/config/env-flags'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeInE2B, executeShellInE2B } from '@/lib/execution/e2b'
import { executeInIsolatedVM, type IsolatedVMBrokerHandler } from '@/lib/execution/isolated-vm'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import { recordMaterializedAccessKeys } from '@/lib/execution/payloads/access-keys'
import {
isLargeArrayManifest,
materializeLargeArrayManifest,
} from '@/lib/execution/payloads/large-array-manifest'
import { containsLargeValueRef, isLargeValueRef } from '@/lib/execution/payloads/large-value-ref'
import {
MAX_FUNCTION_INLINE_BYTES,
MAX_INLINE_MATERIALIZATION_BYTES,
readUserFileContent,
unavailableLargeValueError,
} from '@/lib/execution/payloads/materialization.server'
import { compactExecutionPayload } from '@/lib/execution/payloads/serializer'
import { materializeLargeValueRef } from '@/lib/execution/payloads/store'
import { isExecutionResourceLimitError } from '@/lib/execution/resource-errors'
import { getWorkflowById } from '@/lib/workflows/utils'
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
import { formatLiteralForCode } from '@/executor/utils/code-formatting'
import {
createEnvVarPattern,
createReferencePattern,
createWorkflowVariablePattern,
} from '@/executor/utils/reference-validation'
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
const logger = createLogger('FunctionExecuteAPI')
const TAG_PATTERN = createReferencePattern()
const E2B_JS_WRAPPER_LINES = 3
const E2B_PYTHON_WRAPPER_LINES = 1
const MAX_SANDBOX_OUTPUT_FILES = 20
const MAX_SANDBOX_OUTPUT_BYTES = 50 * 1024 * 1024
/** Matches valid JS identifier names (letters, digits, underscore; no leading digit). */
const SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/
/** ES2023 reserved words — using these as `const` variable names produces a SyntaxError. */
const JS_RESERVED_WORDS = new Set([
'break',
'case',
'catch',
'class',
'const',
'continue',
'debugger',
'default',
'delete',
'do',
'else',
'export',
'extends',
'false',
'finally',
'for',
'function',
'if',
'import',
'in',
'instanceof',
'let',
'new',
'null',
'return',
'static',
'super',
'switch',
'this',
'throw',
'true',
'try',
'typeof',
'var',
'void',
'while',
'with',
'yield',
'enum',
'await',
'implements',
'interface',
'package',
'private',
'protected',
'public',
])
type TypeScriptModule = typeof import('@typescript/typescript6')
let typescriptModulePromise: Promise<TypeScriptModule> | null = null
async function loadTypeScriptModule(): Promise<TypeScriptModule> {
if (!typescriptModulePromise) {
typescriptModulePromise = import('@typescript/typescript6').then(
(mod) => (mod?.default ?? mod) as TypeScriptModule,
(error) => {
typescriptModulePromise = null
throw error
}
)
}
return typescriptModulePromise
}
async function extractJavaScriptImports(
code: string
): Promise<{ imports: string; remainingCode: string; importLineCount: number }> {
try {
const tsModule = await loadTypeScriptModule()
const sourceFile = tsModule.createSourceFile(
'user-code.js',
code,
tsModule.ScriptTarget.Latest,
true,
tsModule.ScriptKind.JS
)
const importSegments: Array<{ text: string; start: number; end: number }> = []
sourceFile.statements.forEach((statement) => {
if (
tsModule.isImportDeclaration(statement) ||
tsModule.isImportEqualsDeclaration(statement)
) {
importSegments.push({
text: statement.getFullText(sourceFile).trim(),
start: statement.getFullStart(),
end: statement.getEnd(),
})
}
})
if (importSegments.length === 0) {
return { imports: '', remainingCode: code, importLineCount: 0 }
}
importSegments.sort((a, b) => a.start - b.start)
const imports = importSegments.map((segment) => segment.text).join('\n')
let cursor = 0
const parts: string[] = []
let importLineCount = 0
for (const segment of importSegments) {
if (segment.start > cursor) {
parts.push(code.slice(cursor, segment.start))
}
const removedSegment = code.slice(segment.start, segment.end)
importLineCount += removedSegment.split('\n').length - 1
const newlinePlaceholder = removedSegment.replace(/[^\n]/g, '')
parts.push(newlinePlaceholder)
cursor = segment.end
}
if (cursor < code.length) {
parts.push(code.slice(cursor))
}
const remainingCode = parts.join('')
return { imports, remainingCode, importLineCount: Math.max(importLineCount, 0) }
} catch (error) {
logger.error('Failed to extract JavaScript imports', { error })
return { imports: '', remainingCode: code, importLineCount: 0 }
}
}
/**
* Enhanced error information interface
*/
interface EnhancedError {
message: string
line?: number
column?: number
stack?: string
name: string
originalError: any
lineContent?: string
}
/**
* Extract enhanced error information from VM execution errors
*/
function extractEnhancedError(
error: any,
userCodeStartLine: number,
userCode?: string
): EnhancedError {
const enhanced: EnhancedError = {
message: error.message || 'Unknown error',
name: error.name || 'Error',
originalError: error,
}
if (error.stack) {
enhanced.stack = error.stack
const stackLines: string[] = error.stack.split('\n')
for (const line of stackLines) {
let match = line.match(/user-function\.js:(\d+)(?::(\d+))?/)
if (!match) {
match = line.match(/at\s+user-function\.js:(\d+):(\d+)/)
}
if (match) {
const stackLine = Number.parseInt(match[1], 10)
const stackColumn = match[2] ? Number.parseInt(match[2], 10) : undefined
const adjustedLine = stackLine - userCodeStartLine + 1
const isWrapperSyntaxError =
stackLine > userCodeStartLine &&
error.name === 'SyntaxError' &&
(error.message.includes('Unexpected token') ||
error.message.includes('Unexpected end of input'))
if (isWrapperSyntaxError && userCode) {
const codeLines = userCode.split('\n')
const lastUserLine = codeLines.length
enhanced.line = lastUserLine
enhanced.column = codeLines[lastUserLine - 1]?.length || 0
enhanced.lineContent = codeLines[lastUserLine - 1]?.trim()
break
}
if (adjustedLine > 0) {
enhanced.line = adjustedLine
enhanced.column = stackColumn
if (userCode) {
const codeLines = userCode.split('\n')
if (adjustedLine <= codeLines.length) {
enhanced.lineContent = codeLines[adjustedLine - 1]?.trim()
}
}
break
}
if (stackLine <= userCodeStartLine) {
enhanced.line = stackLine
enhanced.column = stackColumn
break
}
}
}
const cleanedStackLines: string[] = stackLines
.filter(
(line: string) =>
line.includes('user-function.js') ||
(!line.includes('vm.js') && !line.includes('internal/'))
)
.map((line: string) => line.replace(/\s+at\s+/, ' at '))
if (cleanedStackLines.length > 0) {
enhanced.stack = cleanedStackLines.join('\n')
}
}
return enhanced
}
/**
* Parse and format E2B error message
* Removes E2B-specific line references and adds correct user line numbers
*/
function formatE2BError(
errorMessage: string,
errorOutput: string,
language: CodeLanguage,
userCode: string,
prologueLineCount: number
): { formattedError: string; cleanedOutput: string } {
const wrapperLines =
language === CodeLanguage.Python ? E2B_PYTHON_WRAPPER_LINES : E2B_JS_WRAPPER_LINES
const totalOffset = prologueLineCount + wrapperLines
let userLine: number | undefined
let cleanErrorType = ''
let cleanErrorMsg = ''
if (language === CodeLanguage.Python) {
const cellMatch = errorOutput.match(/Cell In\[\d+\], line (\d+)/)
if (cellMatch) {
const originalLine = Number.parseInt(cellMatch[1], 10)
userLine = originalLine - totalOffset
}
cleanErrorMsg = errorMessage
.replace(/\s*\(detected at line \d+\)/g, '')
.replace(/\s*\([^)]+\.py, line \d+\)/g, '')
.trim()
} else if (language === CodeLanguage.JavaScript) {
const firstLineEnd = errorMessage.indexOf('\n')
const firstLine = firstLineEnd > 0 ? errorMessage.substring(0, firstLineEnd) : errorMessage
const jsErrorMatch = firstLine.match(/^(\w+Error):\s*[^:]+:\s*([^(]+)\.\s*\((\d+):(\d+)\)/)
if (jsErrorMatch) {
cleanErrorType = jsErrorMatch[1]
cleanErrorMsg = jsErrorMatch[2].trim()
const originalLine = Number.parseInt(jsErrorMatch[3], 10)
userLine = originalLine - totalOffset
} else {
const arrowMatch = errorMessage.match(/^>\s*(\d+)\s*\|/m)
if (arrowMatch) {
const originalLine = Number.parseInt(arrowMatch[1], 10)
userLine = originalLine - totalOffset
}
const errorMatch = firstLine.match(/^(\w+Error):\s*(.+)/)
if (errorMatch) {
cleanErrorType = errorMatch[1]
cleanErrorMsg = errorMatch[2]
.replace(/^[^:]+:\s*/, '') // Remove file path
.replace(/\s*\(\d+:\d+\)\s*$/, '') // Remove line:col at end
.trim()
} else {
cleanErrorMsg = firstLine
}
}
}
const finalErrorMsg =
cleanErrorType && cleanErrorMsg
? `${cleanErrorType}: ${cleanErrorMsg}`
: cleanErrorMsg || errorMessage
let formattedError = finalErrorMsg
if (userLine && userLine > 0) {
const codeLines = userCode.split('\n')
// Clamp userLine to the actual user code range
const actualUserLine = Math.min(userLine, codeLines.length)
if (actualUserLine > 0 && actualUserLine <= codeLines.length) {
const lineContent = codeLines[actualUserLine - 1]?.trim()
if (lineContent) {
formattedError = `Line ${actualUserLine}: \`${lineContent}\` - ${finalErrorMsg}`
} else {
formattedError = `Line ${actualUserLine} - ${finalErrorMsg}`
}
}
}
const cleanedOutput = finalErrorMsg
return { formattedError, cleanedOutput }
}
/**
* Create a detailed error message for users
*/
function createUserFriendlyErrorMessage(
enhanced: EnhancedError,
requestId: string,
userCode?: string
): string {
let errorMessage = enhanced.message
if (enhanced.line !== undefined) {
let lineInfo = `Line ${enhanced.line}`
// Add the actual line content if available
if (enhanced.lineContent) {
lineInfo += `: \`${enhanced.lineContent}\``
}
errorMessage = `${lineInfo} - ${errorMessage}`
} else {
if (enhanced.stack) {
const stackMatch = enhanced.stack.match(/user-function\.js:(\d+)(?::(\d+))?/)
if (stackMatch) {
const line = Number.parseInt(stackMatch[1], 10)
let lineInfo = `Line ${line}`
if (userCode) {
const codeLines = userCode.split('\n')
if (line <= codeLines.length) {
const lineContent = codeLines[line - 1]?.trim()
if (lineContent) {
lineInfo += `: \`${lineContent}\``
}
}
}
errorMessage = `${lineInfo} - ${errorMessage}`
}
}
}
if (enhanced.name !== 'Error') {
const errorTypePrefix =
enhanced.name === 'SyntaxError'
? 'Syntax Error'
: enhanced.name === 'TypeError'
? 'Type Error'
: enhanced.name === 'ReferenceError'
? 'Reference Error'
: enhanced.name
if (!errorMessage.toLowerCase().includes(errorTypePrefix.toLowerCase())) {
errorMessage = `${errorTypePrefix}: ${errorMessage}`
}
}
return errorMessage
}
function getErrorDisplayCode(sourceCode: string | undefined, resolvedCode: string): string {
return sourceCode && sourceCode.length > 0 ? sourceCode : resolvedCode
}
function getLineContent(code: string, line: number | undefined): string | undefined {
if (line === undefined || line < 1) {
return undefined
}
return code.split('\n')[line - 1]?.trim()
}
function getErrorDisplayMessage(
message: string,
sourceCode: string | undefined,
resolvedCode: string
): string {
if (!sourceCode || sourceCode === resolvedCode || !resolvedCode.includes('__blockRef_')) {
return message
}
return message.replace(/\s+["']globalThis["']/g, '')
}
function resolveWorkflowVariables(
code: string,
workflowVariables: Record<string, any>,
contextVariables: Record<string, any>
): string {
let resolvedCode = code
const regex = createWorkflowVariablePattern()
let match: RegExpExecArray | null
const replacements: Array<{
match: string
index: number
variableName: string
variableValue: unknown
}> = []
while ((match = regex.exec(code)) !== null) {
const variableName = match[1].trim()
const foundVariable = Object.entries(workflowVariables).find(
([_, variable]) => normalizeName(variable.name || '') === variableName
)
if (!foundVariable) {
const availableVars = Object.values(workflowVariables)
.map((v) => v.name)
.filter(Boolean)
throw new Error(
`Variable "${variableName}" doesn't exist.` +
(availableVars.length > 0 ? ` Available: ${availableVars.join(', ')}` : '')
)
}
const variable = foundVariable[1]
let variableValue: unknown = variable.value
if (variable.value !== undefined && variable.value !== null) {
const type = variable.type === 'string' ? 'plain' : variable.type
if (type === 'number') {
variableValue = Number(variableValue)
} else if (type === 'boolean') {
if (typeof variableValue === 'boolean') {
// Already a boolean, keep as-is
} else {
const normalized = String(variableValue).toLowerCase().trim()
variableValue = normalized === 'true'
}
} else if (type === 'json' && typeof variableValue === 'string') {
try {
variableValue = JSON.parse(variableValue)
} catch {
// Keep as-is
}
}
}
replacements.push({
match: match[0],
index: match.index,
variableName,
variableValue,
})
}
for (let i = replacements.length - 1; i >= 0; i--) {
const { match: matchStr, index, variableName, variableValue } = replacements[i]
const safeVarName = `__variable_${variableName.replace(/[^a-zA-Z0-9_]/g, '_')}`
contextVariables[safeVarName] = variableValue
resolvedCode =
resolvedCode.slice(0, index) + safeVarName + resolvedCode.slice(index + matchStr.length)
}
return resolvedCode
}
function resolveEnvironmentVariables(
code: string,
params: Record<string, any>,
envVars: Record<string, string>,
contextVariables: Record<string, any>
): string {
let resolvedCode = code
const regex = createEnvVarPattern()
let match: RegExpExecArray | null
const replacements: Array<{ match: string; index: number; varName: string; varValue: string }> =
[]
const resolverVars: Record<string, string> = {}
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
resolverVars[key] = String(value)
}
})
Object.entries(envVars).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
resolverVars[key] = value
}
})
while ((match = regex.exec(code)) !== null) {
const varName = match[1].trim()
if (!(varName in resolverVars)) {
continue
}
replacements.push({
match: match[0],
index: match.index,
varName,
varValue: resolverVars[varName],
})
}
for (let i = replacements.length - 1; i >= 0; i--) {
const { match: matchStr, index, varName, varValue } = replacements[i]
const safeVarName = `__var_${varName.replace(/[^a-zA-Z0-9_]/g, '_')}`
contextVariables[safeVarName] = varValue
resolvedCode =
resolvedCode.slice(0, index) + safeVarName + resolvedCode.slice(index + matchStr.length)
}
return resolvedCode
}
function resolveTagVariables(
code: string,
blockData: Record<string, unknown>,
blockNameMapping: Record<string, string>,
blockOutputSchemas: Record<string, OutputSchema>,
contextVariables: Record<string, unknown>,
language = 'javascript'
): string {
let resolvedCode = code
const undefinedLiteral = language === 'python' ? 'None' : 'undefined'
const tagMatches = resolvedCode.match(TAG_PATTERN) || []
for (const match of tagMatches) {
const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim()
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
const blockName = pathParts[0]
const fieldPath = pathParts.slice(1)
const result = resolveBlockReference(blockName, fieldPath, {
blockNameMapping,
blockData,
blockOutputSchemas,
})
if (!result) {
continue
}
let tagValue = result.value
if (tagValue === undefined) {
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), undefinedLiteral)
continue
}
if (typeof tagValue === 'string') {
const trimmed = tagValue.trimStart()
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
tagValue = JSON.parse(tagValue)
} catch {
// Keep as string if not valid JSON
}
}
}
const safeVarName = `__tag_${tagName.replace(/_/g, '_1').replace(/\./g, '_0')}`
contextVariables[safeVarName] = tagValue
resolvedCode = resolvedCode.replace(new RegExp(escapeRegExp(match), 'g'), safeVarName)
}
return resolvedCode
}
/**
* Resolves environment variables and tags in code
* @param code - Code with variables
* @param params - Parameters that may contain variable values
* @param envVars - Environment variables from the workflow
* @returns Resolved code
*/
function resolveCodeVariables(
code: string,
params: Record<string, unknown>,
envVars: Record<string, string> = {},
blockData: Record<string, unknown> = {},
blockNameMapping: Record<string, string> = {},
blockOutputSchemas: Record<string, OutputSchema> = {},
workflowVariables: Record<string, unknown> = {},
language = 'javascript'
): { resolvedCode: string; contextVariables: Record<string, unknown> } {
let resolvedCode = code
const contextVariables: Record<string, unknown> = {}
resolvedCode = resolveWorkflowVariables(resolvedCode, workflowVariables, contextVariables)
resolvedCode = resolveEnvironmentVariables(resolvedCode, params, envVars, contextVariables)
resolvedCode = resolveTagVariables(
resolvedCode,
blockData,
blockNameMapping,
blockOutputSchemas,
contextVariables,
language
)
return { resolvedCode, contextVariables }
}
/**
* Remove one trailing newline from stdout
* This handles the common case where print() or console.log() adds a trailing \n
* that users don't expect to see in the output
*/
/**
* Heuristic: did the sandbox die from an infrastructure failure (OOM kill,
* timeout, lost connection) rather than a normal code error? Python/JS code
* exceptions surface via execution.error; an OOM kill instead makes runCode
* throw, often with an empty or cryptic message.
*/
function isLikelySandboxKill(error: any): boolean {
const msg = `${error?.name ?? ''} ${error?.message ?? ''} ${error?.code ?? ''}`
.toLowerCase()
.trim()
if (!msg) return true
return [
'out of memory',
'oom',
'killed',
'sigkill',
'code 137',
'signal 9',
'terminated',
'econnreset',
'epipe',
'socket hang up',
'connection closed',
'connection reset',
'websocket',
'timed out',
'timeout',
'deadline',
].some((s) => msg.includes(s))
}
function cleanStdout(stdout: string): string {
if (stdout.endsWith('\n')) {
return stdout.slice(0, -1)
}
return stdout
}
/**
* Serializes a value for use as a shell environment variable. Strings pass through
* unchanged; primitives are coerced via `String`; objects, arrays, and other complex
* values are JSON-stringified so that referencing them via `$VAR` yields a useful
* representation instead of `[object Object]`. `null`/`undefined` become an empty
* string to match POSIX env semantics.
*/
function serializeForShellEnv(value: unknown, nullValue = ''): string {
if (value === null || value === undefined) return nullValue
if (typeof value === 'string') return value
if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
return String(value)
}
try {
return JSON.stringify(value) ?? ''
} catch {
return String(value)
}
}
interface FunctionRouteExecutionContext {
workflowId?: string
workspaceId?: string
executionId?: string
largeValueExecutionIds?: string[]
largeValueKeys?: string[]
fileKeys?: string[]
allowLargeValueWorkflowScope?: boolean
userId?: string
requestId: string
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value)
? (value as Record<string, unknown>)
: {}
}
function getPositiveNumber(value: unknown): number | undefined {
if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
return undefined
}
return value
}
function clampInlineBytes(value: unknown, limit = MAX_FUNCTION_INLINE_BYTES): number {
const requested = getPositiveNumber(value)
return Math.min(requested ?? limit, limit)
}
function getBrokerFileArgs(args: unknown): {
file: unknown
maxBytes: number
offset?: number
length?: number
} {
const record = asRecord(args)
const options = asRecord(record.options)
return {
file: record.file,
maxBytes: clampInlineBytes(options.maxBytes),
offset: getPositiveNumber(options.offset),
length: getPositiveNumber(options.length),
}
}
function createFunctionRuntimeBrokers(
context: FunctionRouteExecutionContext
): Record<string, IsolatedVMBrokerHandler> {
context.largeValueKeys ??= []
context.fileKeys ??= []
const largeValueKeys = context.largeValueKeys
const fileKeys = context.fileKeys
const base = {
requestId: context.requestId,
workflowId: context.workflowId,
workspaceId: context.workspaceId,
executionId: context.executionId,
largeValueExecutionIds: context.largeValueExecutionIds,
largeValueKeys,
fileKeys,
allowLargeValueWorkflowScope: context.allowLargeValueWorkflowScope,
userId: context.userId,
logger,
}
const recordMaterializedKeys = (value: unknown) =>
recordMaterializedAccessKeys({ largeValueKeys, fileKeys }, value)
const readFile = async (args: unknown, encoding: 'base64' | 'text', chunked = false) => {
const fileArgs = getBrokerFileArgs(args)
return readUserFileContent(fileArgs.file, {
...base,
encoding,
maxBytes: fileArgs.maxBytes,
chunked,
offset: chunked ? fileArgs.offset : undefined,
length: chunked ? fileArgs.length : undefined,
})
}
return {
'sim.files.readBase64': (args) => readFile(args, 'base64'),
'sim.files.readText': (args) => readFile(args, 'text'),
'sim.files.readBase64Chunk': (args) => readFile(args, 'base64', true),
'sim.files.readTextChunk': (args) => readFile(args, 'text', true),
'sim.values.read': async (args) => {
const record = asRecord(args)
const options = asRecord(record.options)
const ref = record.ref
if (!isLargeValueRef(ref)) {
throw new Error('Expected a large execution value reference.')
}
if (!context.executionId) {
throw new Error('Large execution values require an execution context.')
}
const value = await materializeLargeValueRef(ref, {
...base,
maxBytes: clampInlineBytes(options.maxBytes, MAX_INLINE_MATERIALIZATION_BYTES),
})
if (value === undefined) {
throw unavailableLargeValueError(ref)
}
recordMaterializedKeys(value)
return value
},
'sim.values.readArray': async (args) => {
const record = asRecord(args)
const options = asRecord(record.options)
const manifest = record.ref
if (!isLargeArrayManifest(manifest)) {
throw new Error('Expected a large array manifest.')
}
if (!context.executionId) {
throw new Error('Large array manifests require an execution context.')
}
const value = await materializeLargeArrayManifest(manifest, {
...base,
maxBytes: clampInlineBytes(options.maxBytes, MAX_INLINE_MATERIALIZATION_BYTES),
})
recordMaterializedKeys(value)
return value
},
}
}
async function compactFunctionRouteBody<T>(
body: T,
context: FunctionRouteExecutionContext
): Promise<T> {
return compactExecutionPayload(body, {
workflowId: context.workflowId,
workspaceId: context.workspaceId,
executionId: context.executionId,
userId: context.userId,
preserveRoot: true,
requireDurable: Boolean(context.workspaceId && context.workflowId && context.executionId),
})
}
async function functionJsonResponse<T>(
body: T,
context: FunctionRouteExecutionContext,
init?: ResponseInit
) {
return NextResponse.json(
await compactFunctionRouteBody(
{
...body,
largeValueKeys: context.largeValueKeys,
fileKeys: context.fileKeys,
},
context
),
init
)
}
async function maybeExportSandboxFileToWorkspace(args: {
authUserId: string
workflowId?: string
workspaceId?: string
outputPath?: string
outputFormat?: string
outputMimeType?: string
outputSandboxPath?: string
overwriteFileId?: string
outputMode?: 'create' | 'overwrite'
exportedFileContent?: string
stdout: string
executionTime: number
}) {
const {
authUserId,
workflowId,
workspaceId,
outputPath,
outputFormat,
outputMimeType,
outputSandboxPath,
overwriteFileId,
outputMode,
exportedFileContent,
stdout,
executionTime,
} = args
if (!outputSandboxPath) return null
if (!outputPath) {
return NextResponse.json(
{
success: false,
error:
'outputSandboxPath requires outputPath. Set outputPath to the destination workspace file, e.g. "files/result.csv".',
output: { result: null, stdout: cleanStdout(stdout), executionTime },
},
{ status: 400 }
)
}
const resolvedWorkspaceId =
workspaceId || (workflowId ? (await getWorkflowById(workflowId))?.workspaceId : undefined)
if (!resolvedWorkspaceId) {
return NextResponse.json(
{
success: false,
error: 'Workspace context required to save sandbox file to workspace',
output: { result: null, stdout: cleanStdout(stdout), executionTime },
},
{ status: 400 }
)
}
if (exportedFileContent === undefined) {
return NextResponse.json(
{
success: false,
error: `Sandbox file "${outputSandboxPath}" was not found or could not be read`,
output: { result: null, stdout: cleanStdout(stdout), executionTime },
},
{ status: 500 }
)
}
const fileName = normalizeOutputWorkspaceFileName(outputPath)
const TEXT_MIMES = new Set(Object.values(FORMAT_TO_CONTENT_TYPE))
const resolvedMimeType =
outputMimeType ||
FORMAT_TO_CONTENT_TYPE[resolveOutputFormat(fileName, outputFormat)] ||
'application/octet-stream'
const isBinary = !TEXT_MIMES.has(resolvedMimeType)
const fileBuffer = isBinary
? Buffer.from(exportedFileContent, 'base64')
: Buffer.from(exportedFileContent, 'utf-8')
const targetPath = overwriteFileId || outputPath
const mode = outputMode ?? (overwriteFileId ? 'overwrite' : 'create')
try {
const written = await writeWorkspaceFileByPath({
workspaceId: resolvedWorkspaceId,
userId: authUserId,
target: {
path: targetPath,
mode,
mimeType: outputMimeType,
},
buffer: fileBuffer,
inferredMimeType: resolvedMimeType,
})
logger.info('Sandbox file exported to workspace', {
fileId: written.id,
vfsPath: written.vfsPath,
sandboxPath: outputSandboxPath,
mode,
mimeType: resolvedMimeType,
size: fileBuffer.length,
})
return NextResponse.json({
success: true,
output: {
result: {
message: `Sandbox file exported to ${written.vfsPath}`,
fileId: written.id,
fileName: written.name,
vfsPath: written.vfsPath,
downloadUrl: written.downloadUrl,
sandboxPath: outputSandboxPath,
},
stdout: cleanStdout(stdout),
executionTime,
},
resources: [{ type: 'file', id: written.id, title: written.name, path: written.vfsPath }],
})
} catch (error) {
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Failed to export sandbox file'),
output: { result: null, stdout: cleanStdout(stdout), executionTime },
},
{ status: 400 }
)
}
}
async function maybeExportSandboxFilesToWorkspace(args: {
authUserId: string
workflowId?: string
workspaceId?: string
outputFiles: OutputFileDeclaration[]
exportedFiles?: Record<string, string>
exportedFileContent?: string
stdout: string
executionTime: number
}) {
const sandboxFiles = args.outputFiles.filter((file) => file.sandboxPath)
if (sandboxFiles.length === 0) return null
if (sandboxFiles.length > MAX_SANDBOX_OUTPUT_FILES) {
return NextResponse.json(
{
success: false,
error: `Too many sandbox output files requested (${sandboxFiles.length}). Maximum is ${MAX_SANDBOX_OUTPUT_FILES}.`,
output: {
result: null,
stdout: cleanStdout(args.stdout),
executionTime: args.executionTime,
},
},
{ status: 400 }
)
}
if (sandboxFiles.length === 1) {
const file = sandboxFiles[0]
return maybeExportSandboxFileToWorkspace({
authUserId: args.authUserId,
workflowId: args.workflowId,
workspaceId: args.workspaceId,
outputPath: file.formatPath ?? file.path,
outputFormat: file.format,
outputMimeType: file.mimeType,
outputSandboxPath: file.sandboxPath,
outputMode: file.mode,
exportedFileContent:
(file.sandboxPath ? args.exportedFiles?.[file.sandboxPath] : undefined) ??
args.exportedFileContent,
stdout: args.stdout,
executionTime: args.executionTime,
})
}
const resolvedWorkspaceId =
args.workspaceId ||
(args.workflowId ? (await getWorkflowById(args.workflowId))?.workspaceId : undefined)
if (!resolvedWorkspaceId) {
return NextResponse.json(
{
success: false,
error: 'Workspace context required to save sandbox files to workspace',
output: {
result: null,
stdout: cleanStdout(args.stdout),
executionTime: args.executionTime,
},
},
{ status: 400 }
)
}
const preparedFiles = []
let totalOutputBytes = 0
for (const file of sandboxFiles) {
const sandboxPath = file.sandboxPath!
const content = args.exportedFiles?.[sandboxPath]
if (content === undefined) {
return NextResponse.json(
{
success: false,
error: `Sandbox file "${sandboxPath}" was not found or could not be read`,
output: {
result: null,
stdout: cleanStdout(args.stdout),
executionTime: args.executionTime,
},
},
{ status: 500 }
)
}
const fileName = normalizeOutputWorkspaceFileName(file.formatPath ?? file.path)
const resolvedMimeType =
file.mimeType ||
FORMAT_TO_CONTENT_TYPE[resolveOutputFormat(fileName, file.format)] ||
'application/octet-stream'
const isBinary = !new Set(Object.values(FORMAT_TO_CONTENT_TYPE)).has(resolvedMimeType)
const size = Buffer.byteLength(content, isBinary ? 'base64' : 'utf-8')
totalOutputBytes += size
if (totalOutputBytes > MAX_SANDBOX_OUTPUT_BYTES) {
return NextResponse.json(
{
success: false,
error: `Sandbox output files exceed ${MAX_SANDBOX_OUTPUT_BYTES} bytes total`,
output: {
result: null,
stdout: cleanStdout(args.stdout),
executionTime: args.executionTime,
},
},
{ status: 400 }
)
}
preparedFiles.push({
file,
sandboxPath,
content,
resolvedMimeType,
isBinary,
size,
target: {
path: file.path,
mode: file.mode ?? 'create',
mimeType: file.mimeType,
},
})
}
let validationPaths: string[]
try {
const validations = await Promise.all(
preparedFiles.map((prepared) =>
validateWorkspaceFileWriteTarget({
workspaceId: resolvedWorkspaceId,
userId: args.authUserId,
target: prepared.target,
})
)
)
validationPaths = validations.map((validation) => validation.vfsPath)
} catch (error) {
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Invalid sandbox output destination'),
output: {
result: null,
stdout: cleanStdout(args.stdout),
executionTime: args.executionTime,
},
},
{ status: 400 }
)
}
const duplicateDestination = validationPaths.find(
(vfsPath, index) => validationPaths.indexOf(vfsPath) !== index
)
if (duplicateDestination) {
return NextResponse.json(
{
success: false,
error: `Duplicate sandbox output destination: ${duplicateDestination}`,
output: {
result: null,
stdout: cleanStdout(args.stdout),
executionTime: args.executionTime,
},
},
{ status: 400 }
)
}
const writtenFiles = []
try {
for (const prepared of preparedFiles) {
const buffer = prepared.isBinary
? Buffer.from(prepared.content, 'base64')
: Buffer.from(prepared.content, 'utf-8')
const written = await writeWorkspaceFileByPath({
workspaceId: resolvedWorkspaceId,
userId: args.authUserId,
target: prepared.target,
buffer,
inferredMimeType: prepared.resolvedMimeType,
})
logger.info('Sandbox file exported to workspace', {
fileId: written.id,
vfsPath: written.vfsPath,
sandboxPath: prepared.sandboxPath,
mode: prepared.file.mode ?? 'create',
mimeType: prepared.resolvedMimeType,
size: prepared.size,
})
writtenFiles.push({ ...written, sandboxPath: prepared.sandboxPath })
}
} catch (error) {
return NextResponse.json(
{
success: false,
error: getErrorMessage(error, 'Failed to export sandbox files'),
output: {
result: null,
stdout: cleanStdout(args.stdout),
executionTime: args.executionTime,
},
},
{ status: 400 }
)
}
return NextResponse.json({
success: true,
output: {
result: {
message: `Exported ${writtenFiles.length} sandbox files`,
files: writtenFiles.map((file) => ({
fileId: file.id,
fileName: file.name,
vfsPath: file.vfsPath,
backingVfsPath: file.backingVfsPath,
downloadUrl: file.downloadUrl,
sandboxPath: file.sandboxPath,
})),
},
stdout: cleanStdout(args.stdout),
executionTime: args.executionTime,
},
resources: writtenFiles.map((file) => ({
type: 'file',
id: file.id,
title: file.name,
path: file.vfsPath,
})),
})
}
export const POST = withRouteHandler(async (req: NextRequest) => {
const requestId = generateRequestId()
const startTime = Date.now()
let stdout = ''
let userCodeStartLine = 3 // Default value for error reporting
let resolvedCode = '' // Store resolved code for error reporting
let sourceCodeForErrors: string | undefined
let routeContext: FunctionRouteExecutionContext | undefined
try {
const auth = await checkInternalAuth(req)
if (!auth.success || !auth.userId) {
logger.warn(`[${requestId}] Unauthorized function execution attempt`)
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
const parsed = await parseRequest(functionExecuteContract, req, {})
if (!parsed.success) return parsed.response
const { body } = parsed.data
const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants')
const {
code,
sourceCode,
params = {},
timeout = DEFAULT_EXECUTION_TIMEOUT_MS,
language = DEFAULT_CODE_LANGUAGE,
outputPath,
outputFormat,
outputMimeType,
outputSandboxPath,
overwriteFileId,
outputs,
envVars = {},
blockData = {},
blockNameMapping = {},
blockOutputSchemas = {},
workflowVariables = {},
contextVariables: preResolvedContextVariables = {},
workflowId,
executionId,
largeValueExecutionIds,
largeValueKeys,
fileKeys,
allowLargeValueWorkflowScope = false,
workspaceId,
isCustomTool = false,
_sandboxFiles,
} = body
sourceCodeForErrors = sourceCode
const outputFiles = getOutputFileDeclarations({
outputs,
outputPath,
outputFormat,
outputMimeType,
outputSandboxPath,
overwriteFileId,
})
const outputSandboxPaths = outputFiles
.map((file) => file.sandboxPath)
.filter((path): path is string => Boolean(path))
if (outputSandboxPaths.length > MAX_SANDBOX_OUTPUT_FILES) {
return NextResponse.json(
{
success: false,
error: `Too many sandbox output files requested (${outputSandboxPaths.length}). Maximum is ${MAX_SANDBOX_OUTPUT_FILES}.`,
},
{ status: 400 }
)
}
const executionParams = { ...params }
executionParams._context = undefined
logger.info(`[${requestId}] Function execution request`, {
hasCode: !!code,
paramsCount: Object.keys(executionParams).length,
timeout,
workflowId,
executionId,
isCustomTool,
})
routeContext = {
workflowId,
workspaceId,
executionId,
largeValueExecutionIds,
largeValueKeys,
fileKeys,
allowLargeValueWorkflowScope,
userId: auth.userId,
requestId,
}
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
let contextVariables: Record<string, unknown> = {}
if (lang === CodeLanguage.Shell) {
// For shell, env vars are injected as OS env vars via shellEnvs.
// Replace {{VAR}} placeholders with $VAR so the shell can access them natively.
resolvedCode = code.replace(/\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}/g, '$$$1')
// Carry pre-resolved block output variables (e.g. __blockRef_N) so they can be
// injected as shell env vars below. The executor replaces block references in the
// code with these names, so the values must be present at runtime.
contextVariables = { ...preResolvedContextVariables }
} else {
const codeResolution = resolveCodeVariables(
code,
executionParams,
envVars,
blockData,
blockNameMapping,
blockOutputSchemas,
workflowVariables,
lang
)
resolvedCode = codeResolution.resolvedCode
// Merge pre-resolved block output variables from the executor. These take precedence
// because they were produced by the resolver using full execution-state context
// (including loop/parallel scope) and should not be overwritten.
contextVariables = { ...codeResolution.contextVariables, ...preResolvedContextVariables }
}
if (lang === CodeLanguage.Shell && containsLargeValueRef(contextVariables)) {
throw new Error(
'Large execution values require the JavaScript isolated-vm runtime. Select a nested field or read the value in a JavaScript function.'
)
}
let jsImports = ''
let jsRemainingCode = resolvedCode
let hasImports = false
if (lang === CodeLanguage.JavaScript) {
const extractionResult = await extractJavaScriptImports(resolvedCode)
jsImports = extractionResult.imports
jsRemainingCode = extractionResult.remainingCode
const hasRequireStatements = /require\s*\(\s*['"`]/.test(resolvedCode)
hasImports = jsImports.trim().length > 0 || hasRequireStatements
}
if (lang === CodeLanguage.Shell) {
if (!isE2bEnabled) {
throw new Error(
'Shell execution requires E2B to be enabled. Please contact your administrator to enable E2B.'
)
}
const shellEnvs: Record<string, string> = {}
for (const [k, v] of Object.entries(envVars)) {
shellEnvs[k] = serializeForShellEnv(v)
}
for (const [k, v] of Object.entries(contextVariables)) {
shellEnvs[k] = serializeForShellEnv(v, 'null')
}
logger.info(`[${requestId}] E2B shell execution`, {
enabled: isE2bEnabled,
hasApiKey: Boolean(process.env.E2B_API_KEY),
envVarCount: Object.keys(shellEnvs).length,
})
const execStart = Date.now()
const {
result: shellResult,
stdout: shellStdout,
sandboxId,
error: shellError,
exportedFileContent,
exportedFiles,
} = await executeShellInE2B({
code: resolvedCode,
envs: shellEnvs,
timeoutMs: timeout,
sandboxFiles: _sandboxFiles,
outputSandboxPath,
outputSandboxPaths,
})
const executionTime = Date.now() - execStart
logger.info(`[${requestId}] E2B shell sandbox`, {
sandboxId,
stdoutPreview: shellStdout?.slice(0, 200),
error: shellError,
executionTime,
})
if (shellError) {
return functionJsonResponse(
{
success: false,
error: shellError,
output: { result: null, stdout: cleanStdout(shellStdout), executionTime },
},
routeContext,
{ status: 422 }
)
}
if (outputSandboxPaths.length > 0 || outputSandboxPath) {
const fileExportResponse = await maybeExportSandboxFilesToWorkspace({
authUserId: auth.userId,
workflowId,
workspaceId,
outputFiles,
exportedFiles,
exportedFileContent,
stdout: shellStdout,
executionTime,
})
if (fileExportResponse) return fileExportResponse
}
return functionJsonResponse(
{
success: true,
output: { result: shellResult ?? null, stdout: cleanStdout(shellStdout), executionTime },
},
routeContext
)
}
if (lang === CodeLanguage.Python && !isE2bEnabled) {
throw new Error(
'Python execution requires E2B to be enabled. Please contact your administrator to enable E2B, or use JavaScript instead.'
)
}
if (lang === CodeLanguage.JavaScript && hasImports && !isE2bEnabled) {
throw new Error(
'JavaScript code with import statements requires E2B to be enabled. Please remove the import statements, or contact your administrator to enable E2B.'
)
}
const useE2B =
isE2bEnabled &&
!isCustomTool &&
(lang === CodeLanguage.Python || (lang === CodeLanguage.JavaScript && hasImports))
if (useE2B && containsLargeValueRef(contextVariables)) {
throw new Error(
'Large execution values require the JavaScript isolated-vm runtime. Remove imports, select a nested field, or read the value in a JavaScript function without E2B.'
)
}
if (useE2B) {
logger.info(`[${requestId}] E2B status`, {
enabled: isE2bEnabled,
hasApiKey: Boolean(process.env.E2B_API_KEY),
language: lang,
})
let prologue = ''
if (lang === CodeLanguage.JavaScript) {
let prologueLineCount = 0
const imports = jsImports
const remainingCode = jsRemainingCode
const importSection: string = imports ? `${imports}\n` : ''
const importLineCount = imports ? imports.split('\n').length : 0
const codeBody = remainingCode
resolvedCode = importSection ? `${imports}\n\n${codeBody}` : codeBody
prologue += `const params = JSON.parse(${JSON.stringify(JSON.stringify(executionParams))});\n`
prologueLineCount++
prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
prologue += `globalThis[${JSON.stringify(k)}] = ${formatLiteralForCode(v, 'javascript')};\n`
prologue += `const ${k} = globalThis[${JSON.stringify(k)}];\n`
prologueLineCount++
prologueLineCount++
}
const wrapped = [
';(async () => {',
' try {',
' const __sim_result = await (async () => {',
` ${codeBody.split('\n').join('\n ')}`,
' })();',
" console.log('__SIM_RESULT__=' + JSON.stringify(__sim_result));",
' } catch (error) {',
' console.log(String((error && (error.stack || error.message)) || error));',
' throw error;',
' }',
'})();',
].join('\n')
const codeForE2B = importSection + prologue + wrapped
const execStart = Date.now()
const {
result: e2bResult,
stdout: e2bStdout,
sandboxId,
error: e2bError,
exportedFileContent,
exportedFiles,
} = await executeInE2B({
code: codeForE2B,
language: CodeLanguage.JavaScript,
timeoutMs: timeout,
sandboxFiles: _sandboxFiles,
outputSandboxPath,
outputSandboxPaths,
})
const executionTime = Date.now() - execStart
stdout += e2bStdout
logger.info(`[${requestId}] E2B JS sandbox`, {
sandboxId,
stdoutPreview: e2bStdout?.slice(0, 200),
error: e2bError,
})
if (e2bError) {
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const { formattedError, cleanedOutput } = formatE2BError(
getErrorDisplayMessage(e2bError, sourceCodeForErrors, resolvedCode),
e2bStdout,
lang,
errorDisplayCode,
prologueLineCount + importLineCount
)
return functionJsonResponse(
{
success: false,
error: formattedError,
output: { result: null, stdout: cleanedOutput, executionTime },
},
routeContext,
{ status: 422 }
)
}
if (outputSandboxPaths.length > 0 || outputSandboxPath) {
const fileExportResponse = await maybeExportSandboxFilesToWorkspace({
authUserId: auth.userId,
workflowId,
workspaceId,
outputFiles,
exportedFiles,
exportedFileContent,
stdout,
executionTime,
})
if (fileExportResponse) return fileExportResponse
}
return functionJsonResponse(
{
success: true,
output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime },
},
routeContext
)
}
let prologueLineCount = 0
prologue += 'import json\n'
prologueLineCount++
prologue += `params = json.loads(${JSON.stringify(JSON.stringify(executionParams))})\n`
prologueLineCount++
prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n`
prologueLineCount++
for (const [k, v] of Object.entries(contextVariables)) {
prologue += `${k} = ${formatLiteralForCode(v, 'python')}\n`
prologueLineCount++
}
const wrapped = [
'def __sim_main__():',
...resolvedCode.split('\n').map((l) => ` ${l}`),
'__sim_result__ = __sim_main__()',
"print('__SIM_RESULT__=' + json.dumps(__sim_result__))",
].join('\n')
const codeForE2B = prologue + wrapped
const execStart = Date.now()
const {
result: e2bResult,
stdout: e2bStdout,
sandboxId,
error: e2bError,
exportedFileContent,
exportedFiles,
} = await executeInE2B({
code: codeForE2B,
language: CodeLanguage.Python,
timeoutMs: timeout,
sandboxFiles: _sandboxFiles,
outputSandboxPath,
outputSandboxPaths,
})
const executionTime = Date.now() - execStart
stdout += e2bStdout
logger.info(`[${requestId}] E2B Py sandbox`, {
sandboxId,
stdoutPreview: e2bStdout?.slice(0, 200),
error: e2bError,
})
if (e2bError) {
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const { formattedError, cleanedOutput } = formatE2BError(
getErrorDisplayMessage(e2bError, sourceCodeForErrors, resolvedCode),
e2bStdout,
lang,
errorDisplayCode,
prologueLineCount
)
return functionJsonResponse(
{
success: false,
error: formattedError,
output: { result: null, stdout: cleanedOutput, executionTime },
},
routeContext,
{ status: 422 }
)
}
if (outputSandboxPaths.length > 0 || outputSandboxPath) {
const fileExportResponse = await maybeExportSandboxFilesToWorkspace({
authUserId: auth.userId,
workflowId,
workspaceId,
outputFiles,
exportedFiles,
exportedFileContent,
stdout,
executionTime,
})
if (fileExportResponse) return fileExportResponse
}
return functionJsonResponse(
{
success: true,
output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime },
},
routeContext
)
}
const executionMethod = 'isolated-vm'
const isSafeParamKey = (key: string) => SAFE_IDENTIFIER.test(key) && !JS_RESERVED_WORDS.has(key)
const wrapperLines = ['(async () => {', ' try {']
if (isCustomTool) {
Object.keys(executionParams).forEach((key) => {
if (isSafeParamKey(key)) {
wrapperLines.push(` const ${key} = params.${key};`)
} else {
logger.warn('Skipping param key — not a safe JS identifier', { key, requestId })
}
})
}
userCodeStartLine = wrapperLines.length + 1
let codeToExecute = resolvedCode
let prependedLineCount = 0
if (isCustomTool) {
const paramKeys = Object.keys(executionParams).filter(isSafeParamKey)
const paramDestructuring = paramKeys.map((key) => `const ${key} = params.${key};`).join('\n')
codeToExecute = `${paramDestructuring}\n${resolvedCode}`
prependedLineCount = paramKeys.length
}
const isolatedResult = await executeInIsolatedVM(
{
code: codeToExecute,
params: executionParams,
envVars,
contextVariables,
timeoutMs: timeout,
requestId,
ownerKey: `user:${auth.userId}`,
ownerWeight: 1,
},
{ brokers: createFunctionRuntimeBrokers(routeContext) }
)
const executionTime = Date.now() - startTime
if (isolatedResult.error) {
const isSystemError = isolatedResult.error.isSystemError === true
const logFn = isSystemError ? logger.error.bind(logger) : logger.warn.bind(logger)
logFn(`[${requestId}] Function execution failed in isolated-vm`, {
error: isolatedResult.error,
executionTime,
isSystemError,
})
const ivmError = isolatedResult.error
let adjustedLine = ivmError.line
let adjustedLineContent = ivmError.lineContent
if (prependedLineCount > 0 && ivmError.line !== undefined) {
adjustedLine = Math.max(1, ivmError.line - prependedLineCount)
}
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const displayMessage = getErrorDisplayMessage(
ivmError.message,
sourceCodeForErrors,
resolvedCode
)
adjustedLineContent = getLineContent(errorDisplayCode, adjustedLine) ?? adjustedLineContent
const enhancedError: EnhancedError = {
message: displayMessage,
name: ivmError.name,
stack: ivmError.stack,
originalError: ivmError,
line: adjustedLine,
column: ivmError.column,
lineContent: adjustedLineContent,
}
const userFriendlyErrorMessage = createUserFriendlyErrorMessage(
enhancedError,
requestId,
errorDisplayCode
)
const detailLogFn = isSystemError ? logger.error.bind(logger) : logger.warn.bind(logger)
detailLogFn(`[${requestId}] Enhanced error details`, {
originalMessage: ivmError.message,
enhancedMessage: userFriendlyErrorMessage,
line: enhancedError.line,
column: enhancedError.column,
lineContent: enhancedError.lineContent,
errorType: enhancedError.name,
})
return functionJsonResponse(
{
success: false,
error: userFriendlyErrorMessage,
output: {
result: null,
stdout: cleanStdout(isolatedResult.stdout),
executionTime,
},
debug: {
line: enhancedError.line,
column: enhancedError.column,
errorType: enhancedError.name,
lineContent: enhancedError.lineContent,
stack: enhancedError.stack,
},
},
routeContext,
{ status: isSystemError ? 500 : 422 }
)
}
stdout = isolatedResult.stdout
logger.info(`[${requestId}] Function executed successfully using ${executionMethod}`, {
executionTime,
})
return functionJsonResponse(
{
success: true,
output: { result: isolatedResult.result, stdout: cleanStdout(stdout), executionTime },
},
routeContext
)
} catch (error: any) {
const executionTime = Date.now() - startTime
if (isExecutionResourceLimitError(error)) {
logger.warn(`[${requestId}] Function execution exceeded resource limits`, {
resource: error.resource,
attemptedBytes: error.attemptedBytes,
limitBytes: error.limitBytes,
executionTime,
})
if (routeContext) {
return functionJsonResponse(
{
success: false,
error: error.message,
output: {
result: null,
stdout: cleanStdout(stdout),
executionTime,
},
},
routeContext,
{ status: error.statusCode }
)
}
return NextResponse.json(
{
success: false,
error: error.message,
output: {
result: null,
stdout: cleanStdout(stdout),
executionTime,
},
},
{ status: error.statusCode }
)
}
if (isLikelySandboxKill(error)) {
const underlying = (error?.message || String(error)).slice(0, 300)
logger.warn(`[${requestId}] Sandbox terminated before completion (likely OOM or timeout)`, {
executionTime,
underlying,
})
const killResponse = {
success: false,
error:
'The sandbox was terminated before finishing — most likely it ran out of memory or hit the time limit while processing large or combined inputs. Mount and process fewer/smaller files at once (e.g. one file at a time), or stream and aggregate incrementally instead of loading everything into memory. ' +
`(underlying: ${underlying || 'no detail; sandbox died'})`,
output: { result: null, stdout: cleanStdout(stdout), executionTime },
}
return routeContext
? functionJsonResponse(killResponse, routeContext, { status: 500 })
: NextResponse.json(killResponse, { status: 500 })
}
logger.error(`[${requestId}] Function execution failed`, {
error: error.message || 'Unknown error',
stack: error.stack,
executionTime,
})
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const enhancedError = extractEnhancedError(error, userCodeStartLine, errorDisplayCode)
const userFriendlyErrorMessage = createUserFriendlyErrorMessage(
enhancedError,
requestId,
errorDisplayCode
)
logger.error(`[${requestId}] Enhanced error details`, {
originalMessage: error.message,
enhancedMessage: userFriendlyErrorMessage,
line: enhancedError.line,
column: enhancedError.column,
lineContent: enhancedError.lineContent,
errorType: enhancedError.name,
userCodeStartLine,
})
const errorResponse = {
success: false,
error: userFriendlyErrorMessage,
output: {
result: null,
stdout: cleanStdout(stdout),
executionTime,
},
debug: {
line: enhancedError.line,
column: enhancedError.column,
errorType: enhancedError.name,
lineContent: enhancedError.lineContent,
stack: enhancedError.stack,
},
}
if (routeContext) {
return functionJsonResponse(errorResponse, routeContext, { status: 500 })
}
return NextResponse.json(errorResponse, { status: 500 })
}
})