Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

2795 lines
93 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { sha256Hex } from '@sim/security/hash'
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 { isMothershipSandboxEnabled, isRemoteSandboxEnabled } from '@/lib/core/config/env-flags'
import {
createTimeoutAbortController,
isTimeoutAbortReason,
type TimeoutAbortController,
} from '@/lib/core/execution-limits'
import { encryptSecret } from '@/lib/core/security/encryption'
import { setRecordValue } from '@/lib/core/utils/records'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import {
CodePlaceholderCompileError,
type CodePlaceholderPrivateInput,
type CodePlaceholderRuntimeBinding,
compileCodePlaceholders,
} from '@/lib/execution/code-placeholders'
import { parseExecutionDeadlineHeader } from '@/lib/execution/execution-deadline-header'
import { executeInIsolatedVM, type IsolatedVMBrokerHandler } from '@/lib/execution/isolated-vm'
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
import {
inspectPrivateSecretProvenanceRequest,
isPrivateSecretProvenanceBundleV1,
} from '@/lib/execution/model-input-provenance'
import {
createMountedFileSecretProvenanceScanner,
type MountedFileSecretProvenanceScanner,
} from '@/lib/execution/mounted-file-secret-provenance'
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,
} from '@/lib/execution/payloads/limits'
import {
readUserFileContent,
unavailableLargeValueError,
} from '@/lib/execution/payloads/materialization.server'
import { compactExecutionPayload } from '@/lib/execution/payloads/serializer'
import { materializeLargeValueRef } from '@/lib/execution/payloads/store'
import {
MOUNTED_WORKSPACE_FILES_PROVENANCE_KEY,
PRIVATE_TOOL_METADATA_RESPONSE_HEADER,
RESOLVED_SECRET_NAMES_DURABLE_FILES_METADATA_V2,
RESOLVED_SECRET_NAMES_FIELD,
RESOLVED_SECRET_NAMES_METADATA_V1,
requestsPrivateToolMetadata,
} from '@/lib/execution/private-tool-metadata'
import {
executeInSandbox,
executeShellInSandbox,
SIM_RESULT_PREFIX,
} from '@/lib/execution/remote-sandbox'
import {
isSandboxOutputFileError,
isSandboxOutputLimitError,
MAX_SANDBOX_OUTPUT_BYTES,
} from '@/lib/execution/remote-sandbox/output-limits'
import { isExecutionResourceLimitError } from '@/lib/execution/resource-errors'
import {
fetchWorkspaceFileBuffer,
resolveWorkspaceFileReference,
} from '@/lib/uploads/contexts/workspace/workspace-file-manager'
import {
EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE,
mergeWorkspaceFileSecretProvenance,
type WorkspaceFileSecretProvenance,
} from '@/lib/uploads/contexts/workspace/workspace-file-secret-provenance'
import { getWorkflowById } from '@/lib/workflows/utils'
import {
checkWorkspaceAccess,
resolveWorkspaceAccess,
type WorkspaceAccess,
} from '@/lib/workspaces/permissions/utils'
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference'
import {
createReferencePattern,
createWorkflowVariablePattern,
} from '@/executor/utils/reference-validation'
import {
createResolvedSecretMatcher,
projectResolvedSecretContent,
type ResolvedSecretMatcher,
scanResolvedSecretString,
} from '@/executor/utils/resolved-secret-content-projection'
import type { ResolvedSecretTraceProvenanceV1 } from '@/executor/utils/resolved-secret-trace-registry'
export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'
/** Static host ceiling; the trusted workflow deadline applies the smaller per-call budget. */
export const maxDuration = 604800
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_PRIVATE_FILE_SECRET_MATCH_EVENTS = 1_000_000
const SANDBOX_RUNTIME_PAYLOAD_PATH_ENV = '__SIM_RUNTIME_PAYLOAD_PATH'
interface SandboxRuntimePayload {
params: Record<string, unknown>
environmentVariables: Record<string, string>
contextVariables: SandboxRuntimeContextVariable[]
}
type SandboxRuntimeContextVariable =
| { name: string; kind: 'json'; value: unknown }
| { name: string; kind: 'undefined' }
| { name: string; kind: 'non-finite-number'; value: 'nan' | 'positive' | 'negative' }
function encodeSandboxRuntimeContextVariables(
contextVariables: Record<string, unknown>
): SandboxRuntimeContextVariable[] {
return Object.entries(contextVariables).map(([name, value]) => {
if (value === undefined) return { name, kind: 'undefined' }
if (typeof value === 'number' && !Number.isFinite(value)) {
return {
name,
kind: 'non-finite-number',
value: Number.isNaN(value) ? 'nan' : value > 0 ? 'positive' : 'negative',
}
}
return { name, kind: 'json', value }
})
}
function createRuntimeIdentifier(
code: string,
reservedNames: Set<string>,
label: string,
options: { occupiedIdentifiers?: ReadonlySet<string>; suffix?: string } = {}
): string {
for (let index = 0; ; index += 1) {
const candidate = `__sim_runtime_${label}_${index}${options.suffix ?? ''}`
if (
!code.includes(candidate) &&
!reservedNames.has(candidate) &&
!options.occupiedIdentifiers?.has(candidate)
) {
reservedNames.add(candidate)
return candidate
}
}
}
function createSandboxRuntimePrivateInput(
payload: SandboxRuntimePayload
): CodePlaceholderPrivateInput {
return {
environmentVariable: SANDBOX_RUNTIME_PAYLOAD_PATH_ENV,
content: JSON.stringify(payload),
}
}
function buildJavaScriptSandboxRuntime(
code: string,
contextVariableNames: string[],
runtimeBindings: CodePlaceholderRuntimeBinding[],
occupiedIdentifiers: ReadonlySet<string>
): { importSource: string; prologue: string; internalIdentifiers: string[]; lineCount: number } {
const reservedNames = new Set([
...contextVariableNames,
...runtimeBindings.map((binding) => binding.name),
])
const identifierOptions = { occupiedIdentifiers }
const readFile = createRuntimeIdentifier(code, reservedNames, 'read', identifierOptions)
const unlink = createRuntimeIdentifier(code, reservedNames, 'unlink', identifierOptions)
const payloadPath = createRuntimeIdentifier(code, reservedNames, 'path', identifierOptions)
const payload = createRuntimeIdentifier(code, reservedNames, 'payload', identifierOptions)
const entry = createRuntimeIdentifier(code, reservedNames, 'entry', identifierOptions)
const value = createRuntimeIdentifier(code, reservedNames, 'value', identifierOptions)
const importSource = `import { readFileSync as ${readFile}, unlinkSync as ${unlink} } from 'node:fs';\n`
const lines = [
`const ${payloadPath} = process.env[${JSON.stringify(SANDBOX_RUNTIME_PAYLOAD_PATH_ENV)}];`,
`if (!${payloadPath}) throw new Error('Function runtime payload is unavailable');`,
`const ${payload} = JSON.parse(${readFile}(${payloadPath}, 'utf8'));`,
`${unlink}(${payloadPath});`,
`delete process.env[${JSON.stringify(SANDBOX_RUNTIME_PAYLOAD_PATH_ENV)}];`,
`const params = ${payload}.params;`,
`const environmentVariables = ${payload}.environmentVariables;`,
`for (const ${entry} of ${payload}.contextVariables) {`,
` let ${value};`,
` if (${entry}.kind === 'json') ${value} = ${entry}.value;`,
` else if (${entry}.kind === 'undefined') ${value} = undefined;`,
` else if (${entry}.kind === 'non-finite-number') ${value} = ${entry}.value === 'nan' ? NaN : ${entry}.value === 'positive' ? Infinity : -Infinity;`,
` else throw new Error('Function runtime context value is invalid');`,
` globalThis[${entry}.name] = ${value};`,
'}',
]
for (const name of contextVariableNames) {
if (SAFE_IDENTIFIER.test(name) && !JS_RESERVED_WORDS.has(name)) {
lines.push(`const ${name} = globalThis[${JSON.stringify(name)}];`)
}
}
return {
importSource,
prologue: `${lines.join('\n')}\n`,
internalIdentifiers: [readFile, unlink, payloadPath, payload, entry, value],
lineCount: lines.length + 1,
}
}
function buildPythonSandboxRuntime(
code: string,
contextVariableNames: string[]
): { prologue: string; internalIdentifiers: string[]; lineCount: number } {
const reservedNames = new Set(contextVariableNames)
const identifierOptions = { suffix: '__' }
const payloadPath = createRuntimeIdentifier(code, reservedNames, 'path', identifierOptions)
const payloadFile = createRuntimeIdentifier(code, reservedNames, 'file', identifierOptions)
const payload = createRuntimeIdentifier(code, reservedNames, 'payload', identifierOptions)
const entry = createRuntimeIdentifier(code, reservedNames, 'entry', identifierOptions)
const value = createRuntimeIdentifier(code, reservedNames, 'value', identifierOptions)
const lines = [
'import json',
'import os',
`${payloadPath} = os.environ.pop(${JSON.stringify(SANDBOX_RUNTIME_PAYLOAD_PATH_ENV)}, None)`,
`if ${payloadPath} is None: raise RuntimeError('Function runtime payload is unavailable')`,
`with open(${payloadPath}, 'r', encoding='utf-8') as ${payloadFile}:`,
` ${payload} = json.load(${payloadFile})`,
`os.unlink(${payloadPath})`,
`params = ${payload}['params']`,
`environmentVariables = ${payload}['environmentVariables']`,
`for ${entry} in ${payload}['contextVariables']:`,
` if ${entry}['kind'] == 'json': ${value} = ${entry}['value']`,
` elif ${entry}['kind'] == 'undefined': ${value} = None`,
` elif ${entry}['kind'] == 'non-finite-number': ${value} = float('nan') if ${entry}['value'] == 'nan' else (float('inf') if ${entry}['value'] == 'positive' else float('-inf'))`,
` else: raise RuntimeError('Function runtime context value is invalid')`,
` globals()[${entry}['name']] = ${value}`,
]
return {
prologue: `${lines.join('\n')}\n`,
internalIdentifiers: [payloadPath, payloadFile, payload, entry, value],
lineCount: lines.length,
}
}
/**
* Runs syntactically valid Python modules as modules while retaining the legacy
* Function-body contract for snippets whose top-level `return` only compiles
* after being wrapped in a function.
*/
function buildPythonSandboxWrapper(source: string): string {
return [
`__sim_source__ = ${JSON.stringify(source)}`,
'__sim_exec_globals__ = dict(globals())',
'__sim_exec_globals__["__name__"] = "__main__"',
'try:',
' __sim_compiled__ = compile(__sim_source__, "<sim-function-module>", "exec")',
'except SyntaxError as __sim_compile_error__:',
' if "return" not in str(__sim_compile_error__) or "outside function" not in str(__sim_compile_error__):',
' raise',
' __sim_wrapped_source__ = "def __sim_main__():\\n" + "\\n".join(" " + line for line in __sim_source__.split("\\n"))',
' exec(compile(__sim_wrapped_source__, "<sim-function-body>", "exec"), __sim_exec_globals__, __sim_exec_globals__)',
' __sim_result__ = __sim_exec_globals__["__sim_main__"]()',
'else:',
' exec(__sim_compiled__, __sim_exec_globals__, __sim_exec_globals__)',
' __sim_result__ = __sim_exec_globals__.get("__sim_result__", None)',
`print('\\n${SIM_RESULT_PREFIX}' + json.dumps(__sim_result__))`,
].join('\n')
}
/** 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
hasRequireCalls: boolean
identifierNames: ReadonlySet<string>
}> {
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 }> = []
const identifierNames = new Set<string>()
let hasRequireCalls = false
const visit = (node: import('@typescript/typescript6').Node): void => {
if (tsModule.isIdentifier(node)) identifierNames.add(node.text)
if (
tsModule.isCallExpression(node) &&
tsModule.isIdentifier(node.expression) &&
node.expression.text === 'require'
) {
hasRequireCalls = true
}
tsModule.forEachChild(node, visit)
}
visit(sourceFile)
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, hasRequireCalls, identifierNames }
}
importSegments.sort((a, b) => a.start - b.start)
const imports = importSegments.map((segment) => segment.text).join('\n')
let cursor = 0
const parts: string[] = []
for (const segment of importSegments) {
if (segment.start > cursor) {
parts.push(code.slice(cursor, segment.start))
}
const removedSegment = code.slice(segment.start, segment.end)
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, hasRequireCalls, identifierNames }
} catch {
logger.error('Failed to extract JavaScript imports')
return {
imports: '',
remainingCode: code,
hasRequireCalls: false,
identifierNames: new Set(),
}
}
}
/**
* Enhanced error information interface
*/
interface EnhancedError {
message: string
line?: number
column?: number
stack?: string
name: string
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',
}
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 moduleMatch = errorOutput.match(/<sim-function-module>[^\n]*line (\d+)/)
const bodyMatch = errorOutput.match(/<sim-function-body>[^\n]*line (\d+)/)
const cellMatch = errorOutput.match(/Cell In\[\d+\], line (\d+)/)
if (moduleMatch) {
userLine = Number.parseInt(moduleMatch[1], 10)
} else if (bodyMatch) {
userLine = Number.parseInt(bodyMatch[1], 10) - 1
} else 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, 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 scrubInternalIdentifiers(message: string, identifiers: readonly string[]): string {
let scrubbed = message
for (const identifier of identifiers) {
if (identifier) scrubbed = scrubbed.split(identifier).join('[runtime binding]')
}
return scrubbed
}
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
}
/**
* Narrows the secrets an execution can see, per the block's stored scope.
*
* | Stored value | Behavior |
* |-----------------------------|-------------------------------------------------|
* | unset (every block today) | all secrets — the regression-safe default |
* | `'all'` | all secrets, resolved now so later additions land |
* | `'selected'` + names | only those |
* | `'selected'` + empty list | none — an explicit deny |
*
* Unset and `'all'` must both inject everything: agent-authored code already
* reads `{{MY_SECRET}}` and `environmentVariables['MY_SECRET']` today, so a
* default-deny would silently break prompts that work right now.
*/
function scopeEnvironmentVariables(
envVars: Record<string, string>,
scope: 'all' | 'selected' | undefined,
mountedSecrets: string[] | undefined
): Record<string, string> {
if (scope !== 'selected') return envVars
const allowed = new Set(mountedSecrets ?? [])
const scoped: Record<string, string> = {}
const missing: string[] = []
for (const name of allowed) {
if (Object.hasOwn(envVars, name)) setRecordValue(scoped, name, envVars[name])
else missing.push(name)
}
if (missing.length > 0) {
// A secret that was renamed or deleted since the block was configured. Drop
// it rather than failing: the code's own error is clearer than ours.
logger.warn('Mounted secrets no longer exist in this workspace', { missing })
}
return scoped
}
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 non-environment references before the shared placeholder compiler runs.
* @param code - Code with variables
* @returns Resolved code
*/
function resolveCodeVariables(
code: 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 = 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
resolvedSecretNames: Set<string>
includePrivateResolvedSecretNames: boolean
privateResolvedSecretNamesMetadataType?: ResolvedSecretNamesMetadataType
outputSecretMatcher?: ResolvedSecretMatcher
outputSecretNamesByScanLiteral: Map<string, string[]>
outputSecretPlaintextsByName: Map<string, string>
mountedFileSecretProvenanceScanner?: MountedFileSecretProvenanceScanner
}
type ResolvedSecretNamesMetadataType =
| typeof RESOLVED_SECRET_NAMES_METADATA_V1
| typeof RESOLVED_SECRET_NAMES_DURABLE_FILES_METADATA_V2
type MountedWorkspaceFileProvenanceInspection =
| { status: 'none' }
| { status: 'verified'; provenance: ResolvedSecretTraceProvenanceV1 }
| { status: 'invalid' }
function inspectMountedWorkspaceFileProvenance(
headers: Headers,
body: unknown
): MountedWorkspaceFileProvenanceInspection {
const inspection = inspectPrivateSecretProvenanceRequest(headers, body)
if (inspection.status === 'unsupported') return { status: 'none' }
if (inspection.status !== 'verified' || !isPrivateSecretProvenanceBundleV1(inspection.value)) {
return { status: 'invalid' }
}
if (!inspection.value.complete) {
return {
status: 'verified',
provenance: { version: 1, complete: false, entries: [] },
}
}
if (
inspection.value.selections.length !== 1 ||
inspection.value.selections[0]?.key !== MOUNTED_WORKSPACE_FILES_PROVENANCE_KEY
) {
return { status: 'invalid' }
}
return {
status: 'verified',
provenance: inspection.value.selections[0].provenance,
}
}
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
) {
const responseBody = {
...body,
largeValueKeys: context.largeValueKeys,
fileKeys: context.fileKeys,
}
if (context.includePrivateResolvedSecretNames) {
activateOutputSecretProvenance(getFunctionResultProvenanceSurface(body), context)
}
const response = NextResponse.json(await compactFunctionRouteBody(responseBody, context), init)
return appendPrivateResolvedSecretNames(
response,
context.includePrivateResolvedSecretNames ? getPrivateResolvedSecretNames(context) : null,
context.privateResolvedSecretNamesMetadataType
)
}
function getFunctionResultProvenanceSurface(body: unknown): unknown {
const record = asRecord(body)
const output = asRecord(record.output)
const debug = asRecord(record.debug)
return [
Object.hasOwn(record, 'error') ? record.error : undefined,
Object.hasOwn(output, 'result') ? output.result : undefined,
Object.hasOwn(output, 'stdout') ? output.stdout : undefined,
Object.hasOwn(debug, 'lineContent') ? debug.lineContent : undefined,
Object.hasOwn(debug, 'stack') ? debug.stack : undefined,
]
}
function activateOutputSecretProvenance(
body: unknown,
context: FunctionRouteExecutionContext
): void {
if (!context.outputSecretMatcher) {
activateCompiledSecretProvenance(context)
return
}
const matchedPlaintexts = new Set<string>()
const projection = projectResolvedSecretContent(
body,
context.outputSecretMatcher,
MAX_SANDBOX_OUTPUT_BYTES,
{
onMatch: (plaintext) => matchedPlaintexts.add(plaintext),
}
)
if (!projection.safe) {
activateCompiledSecretProvenance(context)
return
}
for (const plaintext of matchedPlaintexts) {
for (const name of context.outputSecretNamesByScanLiteral.get(plaintext) ?? []) {
context.resolvedSecretNames.add(name)
}
}
}
/**
* Conservatively activates only secrets whose placeholders were compiled for this invocation.
* This fallback is used when the bounded output classifier cannot inspect a result; it never
* considers configured-but-unused environment values and never mutates the functional result.
*/
function activateCompiledSecretProvenance(context: FunctionRouteExecutionContext): void {
for (const name of context.outputSecretPlaintextsByName.keys()) {
context.resolvedSecretNames.add(name)
}
}
/**
* True when this execution compiled a secret placeholder or received a mounted file with verified
* secret provenance. Ordinary mounts without a provenance envelope are user data, not evidence that
* a Sim secret was resolved in this call.
*/
function hasSecretMaterialInScope(context: FunctionRouteExecutionContext): boolean {
if (context.outputSecretPlaintextsByName.size > 0) return true
return context.mountedFileSecretProvenanceScanner?.hasSecrets ?? false
}
/**
* Classifies the secret provenance of one exported sandbox file.
*
* Text exports are scanned for the exact resolved-secret plaintexts in scope. Binary exports cannot
* be scanned soundly — re-encoding can carry a secret without leaving a literal substring — so they
* are classified only when no secret material was in scope at all; with nothing available to embed,
* the bytes are provably secret-free. Otherwise they stay unknown, which fails closed at every
* model and runtime boundary that later reads the file.
*/
async function getOutputFileSecretProvenance(
buffer: Buffer,
isBinary: boolean,
context: FunctionRouteExecutionContext,
scope: { userId: string; workspaceId: string }
): Promise<WorkspaceFileSecretProvenance> {
if (isBinary) {
return hasSecretMaterialInScope(context)
? { status: 'unknown' }
: EXACT_EMPTY_WORKSPACE_FILE_SECRET_PROVENANCE
}
const mountedFileProvenance = context.mountedFileSecretProvenanceScanner?.scan(buffer) ?? {
status: 'exact' as const,
entries: [],
}
if (context.outputSecretPlaintextsByName.size === 0) {
return mountedFileProvenance
}
if (!context.outputSecretMatcher) return { status: 'unknown' }
const matchedNames = new Set<string>()
try {
scanResolvedSecretString(
buffer.toString('utf8'),
context.outputSecretMatcher,
(scanLiteral) => {
for (const name of context.outputSecretNamesByScanLiteral.get(scanLiteral) ?? []) {
matchedNames.add(name)
}
},
MAX_PRIVATE_FILE_SECRET_MATCH_EVENTS
)
} catch {
return { status: 'unknown' }
}
try {
const entries = await Promise.all(
[...matchedNames].sort().map(async (name) => {
const plaintext = context.outputSecretPlaintextsByName.get(name)
if (plaintext === undefined) {
throw new Error('Resolved secret provenance name is outside the scoped catalog')
}
return {
name,
encryptedValue: (await encryptSecret(plaintext)).encrypted,
sourceUserId: scope.userId,
sourceWorkspaceId: scope.workspaceId,
}
})
)
return mergeWorkspaceFileSecretProvenance({ status: 'exact', entries }, mountedFileProvenance)
} catch {
return { status: 'unknown' }
}
}
function getPrivateResolvedSecretNames(context: FunctionRouteExecutionContext): string[] {
return Array.from(context.resolvedSecretNames).sort()
}
async function appendResolvedSecretNames(
response: NextResponse,
context: FunctionRouteExecutionContext,
provenanceValue: unknown
): Promise<NextResponse> {
if (!context.includePrivateResolvedSecretNames) return response
activateOutputSecretProvenance(provenanceValue, context)
return appendPrivateResolvedSecretNames(
response,
getPrivateResolvedSecretNames(context),
context.privateResolvedSecretNamesMetadataType
)
}
async function appendPrivateResolvedSecretNames(
response: NextResponse,
names: string[] | null,
metadataType?: ResolvedSecretNamesMetadataType
): Promise<NextResponse> {
if (!names || !metadataType) return response
const body = (await response.json()) as Record<string, unknown>
const headers = new Headers(response.headers)
headers.delete('content-length')
headers.set(PRIVATE_TOOL_METADATA_RESPONSE_HEADER, metadataType)
return NextResponse.json(
{
...body,
[RESOLVED_SECRET_NAMES_FIELD]: names,
},
{ status: response.status, statusText: response.statusText, headers }
)
}
/**
* Compares an about-to-be-exported buffer against the overwrite target's
* current content. `identical: true` means the export is a byte-for-byte no-op:
* either a legitimately idempotent regeneration, or the incident signature of
* code that never wrote to the declared sandboxPath (the file still holds the
* mounted input). Only the model can tell those apart, so callers surface the
* fact loudly in the receipt instead of failing the write. Comparison failures
* never block the write; the current content is only downloaded when the sizes
* already match.
*/
async function checkOverwriteTarget(
workspaceId: string,
targetPath: string,
buffer: Buffer
): Promise<{ previousSize?: number; identical: boolean }> {
try {
const existing = await resolveWorkspaceFileReference(workspaceId, targetPath)
if (!existing) return { identical: false }
if (existing.size !== buffer.length) {
return { previousSize: existing.size, identical: false }
}
const current = await fetchWorkspaceFileBuffer(existing)
return { previousSize: existing.size, identical: current.equals(buffer) }
} catch {
return { identical: false }
}
}
function formatExportReceipt(bytes: number, previousSize: number | undefined, sha256: string) {
return `(${bytes} bytes${
previousSize !== undefined ? `, replaced ${previousSize} bytes` : ''
}, sha256:${sha256.slice(0, 16)})`
}
function exportUnchangedNote(sandboxPath?: string): string {
return (
'WARNING: content is byte-identical to the previous version — nothing changed.' +
(sandboxPath
? ` If you expected new content, your code did not modify the sandbox file at "${sandboxPath}" (it still holds the mounted input); write the new content to exactly that path and export again.`
: ' If you expected new content, the code returned the same bytes as before.')
)
}
function exportFailure(
error: string,
status: number,
stdout: string,
executionTime: number
): NextResponse {
return NextResponse.json(
{ success: false, error, output: { result: null, stdout: cleanStdout(stdout), executionTime } },
{ status }
)
}
/**
* Both `workspaceId` and `workflowId` arrive in the request body, so the workspace an export
* resolves to is caller-controlled either way. Returns null when the acting user cannot write to
* it, gating the secret-provenance scan and overwrite probe that run before the write itself.
*/
async function authorizeExportWorkspace(
workspaceId: string,
authUserId: string,
provided?: WorkspaceAccess
): Promise<WorkspaceAccess | null> {
const access = await resolveWorkspaceAccess(workspaceId, authUserId, provided)
if (access.exists && access.canWrite) return access
logger.warn('Sandbox file export denied for workspace', { workspaceId, userId: authUserId })
return null
}
async function maybeExportSandboxFileToWorkspace(args: {
routeContext: FunctionRouteExecutionContext
authUserId: string
workflowId?: string
workspaceId?: string
workspaceAccess?: WorkspaceAccess
outputPath?: string
outputFormat?: string
outputMimeType?: string
outputSandboxPath?: string
overwriteFileId?: string
outputMode?: 'create' | 'overwrite'
exportedFileContent?: string
stdout: string
executionTime: number
}) {
const {
routeContext,
authUserId,
workflowId,
workspaceId,
workspaceAccess,
outputPath,
outputFormat,
outputMimeType,
outputSandboxPath,
overwriteFileId,
outputMode,
exportedFileContent,
stdout,
executionTime,
} = args
if (!outputSandboxPath) return null
if (!outputPath) {
return exportFailure(
'outputSandboxPath requires outputPath. Set outputPath to the destination workspace file, e.g. "files/result.csv".',
400,
stdout,
executionTime
)
}
const resolvedWorkspaceId =
workspaceId || (workflowId ? (await getWorkflowById(workflowId))?.workspaceId : undefined)
if (!resolvedWorkspaceId) {
return exportFailure(
'Workspace context required to save sandbox file to workspace',
400,
stdout,
executionTime
)
}
const access = await authorizeExportWorkspace(resolvedWorkspaceId, authUserId, workspaceAccess)
if (!access) return exportFailure('Workspace access denied', 403, stdout, executionTime)
if (exportedFileContent === undefined) {
return exportFailure(
`Sandbox file "${outputSandboxPath}" was not found or could not be read`,
500,
stdout,
executionTime
)
}
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 outputBytes = Buffer.byteLength(exportedFileContent, isBinary ? 'base64' : 'utf-8')
if (outputBytes > MAX_SANDBOX_OUTPUT_BYTES) {
return exportFailure(
`Sandbox output files exceed ${MAX_SANDBOX_OUTPUT_BYTES} bytes total`,
400,
stdout,
executionTime
)
}
const fileBuffer = isBinary
? Buffer.from(exportedFileContent, 'base64')
: Buffer.from(exportedFileContent, 'utf-8')
const secretProvenance = await getOutputFileSecretProvenance(fileBuffer, isBinary, routeContext, {
userId: authUserId,
workspaceId: resolvedWorkspaceId,
})
const mode = outputMode ?? (overwriteFileId ? 'overwrite' : 'create')
const targetPath = mode === 'create' ? outputPath : overwriteFileId || outputPath
let previousSize: number | undefined
let unchanged = false
if (mode === 'overwrite') {
const check = await checkOverwriteTarget(resolvedWorkspaceId, targetPath, fileBuffer)
previousSize = check.previousSize
unchanged = check.identical
}
try {
const sha256 = sha256Hex(fileBuffer)
const written = await writeWorkspaceFileByPath({
workspaceId: resolvedWorkspaceId,
userId: authUserId,
workspaceAccess: access,
target: {
path: targetPath,
mode,
mimeType: outputMimeType,
},
buffer: fileBuffer,
inferredMimeType: resolvedMimeType,
secretProvenance,
})
logger.info('Sandbox file exported to workspace', {
fileId: written.id,
vfsPath: written.vfsPath,
sandboxPath: outputSandboxPath,
mode,
mimeType: resolvedMimeType,
size: fileBuffer.length,
previousSize,
sha256,
unchanged,
})
return NextResponse.json({
success: true,
output: {
result: {
message: `Sandbox file exported to ${written.vfsPath} ${formatExportReceipt(
fileBuffer.length,
previousSize,
sha256
)}${unchanged ? ` — ${exportUnchangedNote(outputSandboxPath)}` : ''}`,
fileId: written.id,
fileName: written.name,
vfsPath: written.vfsPath,
downloadUrl: written.downloadUrl,
sandboxPath: outputSandboxPath,
size: fileBuffer.length,
previousSize,
sha256,
unchanged,
},
stdout: cleanStdout(stdout),
executionTime,
},
resources: [{ type: 'file', id: written.id, title: written.name, path: written.vfsPath }],
})
} catch (error) {
return exportFailure(
getErrorMessage(error, 'Failed to export sandbox file'),
400,
stdout,
executionTime
)
}
}
async function maybeExportSandboxFilesToWorkspace(args: {
routeContext: FunctionRouteExecutionContext
authUserId: string
workflowId?: string
workspaceId?: string
workspaceAccess?: WorkspaceAccess
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 exportFailure(
`Too many sandbox output files requested (${sandboxFiles.length}). Maximum is ${MAX_SANDBOX_OUTPUT_FILES}.`,
400,
args.stdout,
args.executionTime
)
}
if (sandboxFiles.length === 1) {
const file = sandboxFiles[0]
return maybeExportSandboxFileToWorkspace({
routeContext: args.routeContext,
authUserId: args.authUserId,
workflowId: args.workflowId,
workspaceId: args.workspaceId,
workspaceAccess: args.workspaceAccess,
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 exportFailure(
'Workspace context required to save sandbox files to workspace',
400,
args.stdout,
args.executionTime
)
}
const access = await authorizeExportWorkspace(
resolvedWorkspaceId,
args.authUserId,
args.workspaceAccess
)
if (!access) {
return exportFailure('Workspace access denied', 403, args.stdout, args.executionTime)
}
const preparedFiles = []
let totalOutputBytes = 0
for (const file of sandboxFiles) {
const sandboxPath = file.sandboxPath!
const content = args.exportedFiles?.[sandboxPath]
if (content === undefined) {
return exportFailure(
`Sandbox file "${sandboxPath}" was not found or could not be read`,
500,
args.stdout,
args.executionTime
)
}
const outputPath = file.formatPath ?? file.path
const fileName = normalizeOutputWorkspaceFileName(outputPath)
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 exportFailure(
`Sandbox output files exceed ${MAX_SANDBOX_OUTPUT_BYTES} bytes total`,
400,
args.stdout,
args.executionTime
)
}
const scanBuffer = isBinary ? Buffer.from(content, 'base64') : Buffer.from(content, 'utf-8')
const secretProvenance = await getOutputFileSecretProvenance(
scanBuffer,
isBinary,
args.routeContext,
{ userId: args.authUserId, workspaceId: resolvedWorkspaceId }
)
preparedFiles.push({
file,
sandboxPath,
content,
resolvedMimeType,
isBinary,
size,
secretProvenance,
target: {
path: (file.mode ?? 'create') === 'create' ? outputPath : 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,
workspaceAccess: access,
target: prepared.target,
})
)
)
validationPaths = validations.map((validation) => validation.vfsPath)
} catch (error) {
return exportFailure(
getErrorMessage(error, 'Invalid sandbox output destination'),
400,
args.stdout,
args.executionTime
)
}
const duplicateDestination = validationPaths.find(
(vfsPath, index) => validationPaths.indexOf(vfsPath) !== index
)
if (duplicateDestination) {
return exportFailure(
`Duplicate sandbox output destination: ${duplicateDestination}`,
400,
args.stdout,
args.executionTime
)
}
const writtenFiles = []
try {
for (const prepared of preparedFiles) {
const buffer = prepared.isBinary
? Buffer.from(prepared.content, 'base64')
: Buffer.from(prepared.content, 'utf-8')
let previousSize: number | undefined
let unchanged = false
if (prepared.target.mode === 'overwrite') {
const check = await checkOverwriteTarget(resolvedWorkspaceId, prepared.target.path, buffer)
previousSize = check.previousSize
unchanged = check.identical
}
const sha256 = sha256Hex(buffer)
const written = await writeWorkspaceFileByPath({
workspaceId: resolvedWorkspaceId,
userId: args.authUserId,
workspaceAccess: access,
target: prepared.target,
buffer,
inferredMimeType: prepared.resolvedMimeType,
secretProvenance: prepared.secretProvenance,
})
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,
previousSize,
sha256,
unchanged,
})
writtenFiles.push({
...written,
sandboxPath: prepared.sandboxPath,
exportedBytes: buffer.length,
previousSize,
sha256,
unchanged,
})
}
} catch (error) {
return exportFailure(
getErrorMessage(error, 'Failed to export sandbox files'),
400,
args.stdout,
args.executionTime
)
}
const unchangedFiles = writtenFiles.filter((file) => file.unchanged)
return NextResponse.json({
success: true,
output: {
result: {
message: `Exported ${writtenFiles.length} sandbox files: ${writtenFiles
.map(
(file) =>
`${file.vfsPath} ${formatExportReceipt(
file.exportedBytes,
file.previousSize,
file.sha256
)}${file.unchanged ? ' [UNCHANGED]' : ''}`
)
.join('; ')}${
unchangedFiles.length > 0
? ` — WARNING: ${unchangedFiles.map((file) => file.vfsPath).join(', ')} ${
unchangedFiles.length === 1 ? 'is' : 'are'
} byte-identical to the previous version (nothing changed). If you expected new content there, your code did not modify the corresponding sandbox file.`
: ''
}`,
files: writtenFiles.map((file) => ({
fileId: file.id,
fileName: file.name,
vfsPath: file.vfsPath,
downloadUrl: file.downloadUrl,
sandboxPath: file.sandboxPath,
size: file.exportedBytes,
previousSize: file.previousSize,
sha256: file.sha256,
unchanged: file.unchanged,
})),
},
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 compilerInternalIdentifiers: string[] = []
let compilerPrivateInputs: CodePlaceholderPrivateInput[] = []
let compilerRuntimeBindings: CodePlaceholderRuntimeBinding[] = []
let routeContext: FunctionRouteExecutionContext | undefined
let includePrivateResolvedSecretNames = false
let privateResolvedSecretNamesMetadataType: ResolvedSecretNamesMetadataType | undefined
let timeoutForError: number | undefined
let executionDeadlineAt: number | undefined
let executionDeadlineController: TimeoutAbortController | undefined
let executionSignal = req.signal
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 usesMothershipSandbox = auth.sandboxProfile === 'mothership'
executionDeadlineAt = parseExecutionDeadlineHeader(req.headers)
privateResolvedSecretNamesMetadataType = requestsPrivateToolMetadata(
req.headers,
RESOLVED_SECRET_NAMES_DURABLE_FILES_METADATA_V2
)
? RESOLVED_SECRET_NAMES_DURABLE_FILES_METADATA_V2
: requestsPrivateToolMetadata(req.headers, RESOLVED_SECRET_NAMES_METADATA_V1)
? RESOLVED_SECRET_NAMES_METADATA_V1
: undefined
includePrivateResolvedSecretNames = privateResolvedSecretNamesMetadataType !== undefined
const parsed = await parseRequest(functionExecuteContract, req, {})
if (!parsed.success) {
return appendPrivateResolvedSecretNames(
parsed.response,
includePrivateResolvedSecretNames ? [] : null,
privateResolvedSecretNamesMetadataType
)
}
const { body } = parsed.data
const mountedWorkspaceFileProvenance = inspectMountedWorkspaceFileProvenance(req.headers, body)
if (mountedWorkspaceFileProvenance.status === 'invalid') {
return appendPrivateResolvedSecretNames(
NextResponse.json(
{ success: false, error: 'Mounted file secret provenance is invalid' },
{ status: 400 }
),
includePrivateResolvedSecretNames ? [] : null,
privateResolvedSecretNamesMetadataType
)
}
const mountedFileSecretProvenanceScanner =
mountedWorkspaceFileProvenance.status === 'verified'
? await createMountedFileSecretProvenanceScanner(mountedWorkspaceFileProvenance.provenance)
: undefined
if (
mountedWorkspaceFileProvenance.status === 'verified' &&
!mountedFileSecretProvenanceScanner
) {
return appendPrivateResolvedSecretNames(
NextResponse.json(
{ success: false, error: 'Mounted file secret provenance is unavailable' },
{ status: 400 }
),
includePrivateResolvedSecretNames ? [] : null,
privateResolvedSecretNamesMetadataType
)
}
const { DEFAULT_EXECUTION_TIMEOUT_MS } = await import('@/lib/execution/constants')
const {
code,
sourceCode,
params = {},
timeout: requestedTimeout,
language = DEFAULT_CODE_LANGUAGE,
outputPath,
outputFormat,
outputMimeType,
outputSandboxPath,
overwriteFileId,
outputs,
envVars: rawEnvVars = {},
secretScope,
mountedSecrets,
sandboxId: selectedSandboxId,
blockData = {},
blockNameMapping = {},
blockOutputSchemas = {},
workflowVariables = {},
contextVariables: preResolvedContextVariables = {},
workflowId,
executionId,
largeValueExecutionIds,
largeValueKeys,
fileKeys,
allowLargeValueWorkflowScope = false,
workspaceId,
isCustomTool = false,
_sandboxFiles,
} = body
// The internal JWT carries no workspace scope, so a body-supplied workspaceId would
// otherwise be the sole authorization input for sandbox selection and file exports.
// Denial is returned rather than thrown: this handler's catch-all would turn a thrown
// WorkspaceAccessDeniedError into a 500 before withRouteHandler could map it.
const workspaceAccess = workspaceId
? await checkWorkspaceAccess(workspaceId, auth.userId)
: undefined
if (workspaceAccess && (!workspaceAccess.exists || !workspaceAccess.hasAccess)) {
logger.warn(`[${requestId}] Function execution denied for workspace`, {
workspaceId,
userId: auth.userId,
})
return NextResponse.json(
{ success: false, error: 'Workspace access denied' },
{ status: 403 }
)
}
if (selectedSandboxId && !isRemoteSandboxEnabled) {
return NextResponse.json(
{ success: false, error: 'The Function code sandbox is not configured' },
{ status: 503 }
)
}
if (usesMothershipSandbox && !selectedSandboxId && !isMothershipSandboxEnabled) {
return NextResponse.json(
{ success: false, error: 'Mothership code sandbox is not configured' },
{ status: 503 }
)
}
// A selected Sim sandbox is layered on the Function base, even for a
// trusted Mothership call. Only an unselected Mothership call uses the
// separately built Mothership image.
const remoteSandboxEnabled = selectedSandboxId
? isRemoteSandboxEnabled
: usesMothershipSandbox
? isMothershipSandboxEnabled
: isRemoteSandboxEnabled
const remainingExecutionMs =
executionDeadlineAt === undefined ? undefined : Math.max(1, executionDeadlineAt - Date.now())
const timeout =
remainingExecutionMs === undefined
? (requestedTimeout ?? DEFAULT_EXECUTION_TIMEOUT_MS)
: Math.max(1, Math.min(requestedTimeout ?? remainingExecutionMs, remainingExecutionMs))
executionDeadlineController = createTimeoutAbortController(timeout, req.signal)
executionSignal = executionDeadlineController.signal
timeoutForError = timeout
// Scoped before {{VAR}} resolution so the `{{NAME}}` path and the
// `environmentVariables[...]` dict narrow together — filtering only the dict
// would leave `{{OTHER_SECRET}}` resolving, which is a hole, not a scope.
const envVars = scopeEnvironmentVariables(rawEnvVars, secretScope, mountedSecrets)
sourceCodeForErrors = sourceCode ?? code
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 appendPrivateResolvedSecretNames(
NextResponse.json(
{
success: false,
error: `Too many sandbox output files requested (${outputSandboxPaths.length}). Maximum is ${MAX_SANDBOX_OUTPUT_FILES}.`,
},
{ status: 400 }
),
includePrivateResolvedSecretNames ? [] : null,
privateResolvedSecretNamesMetadataType
)
}
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,
resolvedSecretNames: new Set<string>(),
includePrivateResolvedSecretNames,
privateResolvedSecretNamesMetadataType,
outputSecretNamesByScanLiteral: new Map(),
outputSecretPlaintextsByName: new Map(),
mountedFileSecretProvenanceScanner,
}
const lang = isValidCodeLanguage(language) ? language : DEFAULT_CODE_LANGUAGE
const codeResolution = resolveCodeVariables(
code,
blockData,
blockNameMapping,
blockOutputSchemas,
workflowVariables,
lang
)
/**
* Pre-resolved block outputs take precedence because the executor produced them with the
* complete loop/parallel scope. Environment placeholders remain untouched until this point,
* so Custom Tools and visual Function blocks share exactly one compiler.
*/
const contextVariables: Record<string, unknown> = {
...codeResolution.contextVariables,
...preResolvedContextVariables,
}
const compilation = await compileCodePlaceholders({
code: codeResolution.resolvedCode,
language: lang,
params: executionParams,
environmentVariables: envVars,
reservedNames: Object.keys(contextVariables),
})
for (const name of compilation.resolvedSecretNames) {
if (!Object.hasOwn(envVars, name)) continue
const plaintext = envVars[name]
if (!plaintext) continue
routeContext.outputSecretPlaintextsByName.set(name, plaintext)
const scanLiterals = new Set([plaintext, JSON.stringify(plaintext).slice(1, -1)])
for (const scanLiteral of scanLiterals) {
const names = routeContext.outputSecretNamesByScanLiteral.get(scanLiteral) ?? []
names.push(name)
routeContext.outputSecretNamesByScanLiteral.set(scanLiteral, names)
}
}
if (routeContext.outputSecretNamesByScanLiteral.size > 0) {
try {
routeContext.outputSecretMatcher = createResolvedSecretMatcher(
[...routeContext.outputSecretNamesByScanLiteral].map(([plaintext, names]) => ({
plaintext,
replacement: `{{${[...names].sort()[0]}}}`,
}))
)
} catch {
activateCompiledSecretProvenance(routeContext)
}
}
resolvedCode = compilation.code
compilerInternalIdentifiers = [...compilation.internalIdentifiers]
compilerPrivateInputs = [...compilation.privateInputs]
compilerRuntimeBindings = [...compilation.runtimeBindings]
for (const binding of compilation.bindings) {
setRecordValue(contextVariables, binding.name, binding.value)
}
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 jsIdentifierNames: ReadonlySet<string> = new Set()
let hasImports = false
if (lang === CodeLanguage.JavaScript) {
const extractionResult = await extractJavaScriptImports(resolvedCode)
jsImports = extractionResult.imports
jsRemainingCode = extractionResult.remainingCode
jsIdentifierNames = extractionResult.identifierNames
hasImports = jsImports.trim().length > 0 || extractionResult.hasRequireCalls
}
if (lang === CodeLanguage.Shell) {
if (!remoteSandboxEnabled) {
throw new Error(
'Shell execution requires a remote code sandbox to be enabled. Please contact your administrator to enable it.'
)
}
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: remoteSandboxEnabled,
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 executeShellInSandbox({
code: resolvedCode,
envs: shellEnvs,
timeoutMs: timeout,
sandboxFiles: _sandboxFiles,
privateInputs: compilerPrivateInputs,
outputSandboxPath,
outputSandboxPaths,
workspaceId,
sandboxId: selectedSandboxId,
...(usesMothershipSandbox && !selectedSandboxId
? { sandboxKind: 'mothership' as const }
: {}),
signal: executionSignal,
})
const executionTime = Date.now() - execStart
logger.info(`[${requestId}] E2B shell sandbox`, {
sandboxId,
succeeded: !shellError,
executionTime,
})
if (shellError) {
return functionJsonResponse(
{
success: false,
error: scrubInternalIdentifiers(shellError, compilerInternalIdentifiers),
output: { result: null, stdout: cleanStdout(shellStdout), executionTime },
},
routeContext,
{ status: 422 }
)
}
if (outputSandboxPaths.length > 0 || outputSandboxPath) {
const fileExportResponse = await maybeExportSandboxFilesToWorkspace({
routeContext,
authUserId: auth.userId,
workflowId,
workspaceId,
workspaceAccess,
outputFiles,
exportedFiles,
exportedFileContent,
stdout: shellStdout,
executionTime,
})
if (fileExportResponse) {
return appendResolvedSecretNames(
fileExportResponse,
routeContext,
cleanStdout(shellStdout)
)
}
}
return functionJsonResponse(
{
success: true,
output: { result: shellResult ?? null, stdout: cleanStdout(shellStdout), executionTime },
},
routeContext
)
}
if (lang === CodeLanguage.Python && !remoteSandboxEnabled) {
throw new Error(
'Python execution requires a remote code sandbox to be enabled. Please contact your administrator to enable it, or use JavaScript instead.'
)
}
if (lang === CodeLanguage.JavaScript && hasImports && !remoteSandboxEnabled) {
throw new Error(
'JavaScript code with import statements requires a remote code sandbox to be enabled. Please remove the import statements, or contact your administrator to enable it.'
)
}
const useRemoteSandbox =
usesMothershipSandbox ||
(remoteSandboxEnabled &&
!isCustomTool &&
(lang === CodeLanguage.Python ||
(lang === CodeLanguage.JavaScript && (hasImports || Boolean(selectedSandboxId)))))
if (useRemoteSandbox && 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 a remote sandbox.'
)
}
// Sandbox file mounts and sandboxPath exports only exist in the remote
// sandbox runtime; isolated-vm has no filesystem. Silently dropping a declared
// sandbox input/output here produced "export succeeded" responses with
// zero bytes written, so refuse the call instead. The remediation depends
// on WHY this call runs in isolated-vm — "switch to python" is a dead end
// when no remote sandbox is enabled or the call is a custom tool.
if (
!useRemoteSandbox &&
(outputSandboxPaths.length > 0 || outputSandboxPath || _sandboxFiles?.length)
) {
const remediation = !remoteSandboxEnabled
? "No remote code sandbox is enabled on this deployment, so there is no sandbox filesystem for any language. Pass input data via params and return output as the code's return value with outputs.files[].path (no sandboxPath)."
: isCustomTool
? "custom tools always run in the isolated JavaScript VM, which has no sandbox filesystem. Pass input data via params and return output as the code's return value."
: 'plain JavaScript runs in the isolated VM, which has no sandbox filesystem. Use language "python" so the code runs in the remote sandbox, or drop sandboxPath and return the file content as the code\'s return value with outputs.files[].path.'
return functionJsonResponse(
{
success: false,
error: `Sandbox file inputs/outputs are unavailable for this call: ${remediation}`,
output: { result: null, stdout: '', executionTime: Date.now() - startTime },
},
routeContext,
{ status: 422 }
)
}
if (useRemoteSandbox) {
logger.info(`[${requestId}] E2B status`, {
enabled: remoteSandboxEnabled,
hasApiKey: Boolean(process.env.E2B_API_KEY),
language: lang,
})
if (lang === CodeLanguage.JavaScript) {
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
const runtime = buildJavaScriptSandboxRuntime(
resolvedCode,
Object.keys(contextVariables),
compilerRuntimeBindings,
jsIdentifierNames
)
compilerInternalIdentifiers.push(...runtime.internalIdentifiers)
const runtimePrivateInput = createSandboxRuntimePrivateInput({
params: executionParams,
environmentVariables: envVars,
contextVariables: encodeSandboxRuntimeContextVariables(contextVariables),
})
const wrapped = [
';(async () => {',
' try {',
' const __sim_result = await (async () => {',
` ${codeBody.split('\n').join('\n ')}`,
' })();',
// Leading \n guarantees the marker starts a fresh line even when user
// code's last stdout write was not newline-terminated (chunks are
// concatenated verbatim on the parse side, so a glued marker would
// otherwise be missed silently).
` console.log('\\n${SIM_RESULT_PREFIX}' + JSON.stringify(__sim_result));`,
' } catch (error) {',
' console.log(String((error && (error.stack || error.message)) || error));',
' throw error;',
' }',
'})();',
].join('\n')
const codeForE2B = runtime.importSource + importSection + runtime.prologue + wrapped
const execStart = Date.now()
const {
result: e2bResult,
stdout: e2bStdout,
sandboxId,
error: e2bError,
exportedFileContent,
exportedFiles,
} = await executeInSandbox({
code: codeForE2B,
language: CodeLanguage.JavaScript,
timeoutMs: timeout,
sandboxFiles: _sandboxFiles,
privateInputs: [...compilerPrivateInputs, runtimePrivateInput],
runtimeBindings: compilerRuntimeBindings,
outputSandboxPath,
outputSandboxPaths,
workspaceId,
sandboxId: selectedSandboxId,
...(usesMothershipSandbox && !selectedSandboxId
? { sandboxKind: 'mothership' as const }
: {}),
signal: executionSignal,
})
const executionTime = Date.now() - execStart
stdout += e2bStdout
logger.info(`[${requestId}] E2B JS sandbox`, {
sandboxId,
succeeded: !e2bError,
})
if (e2bError) {
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const { formattedError, cleanedOutput } = formatE2BError(
scrubInternalIdentifiers(
getErrorDisplayMessage(e2bError, sourceCodeForErrors, resolvedCode),
compilerInternalIdentifiers
),
e2bStdout,
lang,
errorDisplayCode,
runtime.lineCount + 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({
routeContext,
authUserId: auth.userId,
workflowId,
workspaceId,
workspaceAccess,
outputFiles,
exportedFiles,
exportedFileContent,
stdout,
executionTime,
})
if (fileExportResponse) {
return appendResolvedSecretNames(fileExportResponse, routeContext, cleanStdout(stdout))
}
}
return functionJsonResponse(
{
success: true,
output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime },
},
routeContext
)
}
const runtime = buildPythonSandboxRuntime(resolvedCode, Object.keys(contextVariables))
compilerInternalIdentifiers.push(...runtime.internalIdentifiers)
const runtimePrivateInput = createSandboxRuntimePrivateInput({
params: executionParams,
environmentVariables: envVars,
contextVariables: encodeSandboxRuntimeContextVariables(contextVariables),
})
const wrapped = buildPythonSandboxWrapper(resolvedCode)
const codeForE2B = runtime.prologue + wrapped
const execStart = Date.now()
const {
result: e2bResult,
stdout: e2bStdout,
sandboxId,
error: e2bError,
exportedFileContent,
exportedFiles,
} = await executeInSandbox({
code: codeForE2B,
language: CodeLanguage.Python,
timeoutMs: timeout,
sandboxFiles: _sandboxFiles,
privateInputs: [...compilerPrivateInputs, runtimePrivateInput],
outputSandboxPath,
outputSandboxPaths,
workspaceId,
sandboxId: selectedSandboxId,
...(usesMothershipSandbox && !selectedSandboxId
? { sandboxKind: 'mothership' as const }
: {}),
signal: executionSignal,
})
const executionTime = Date.now() - execStart
stdout += e2bStdout
logger.info(`[${requestId}] E2B Py sandbox`, {
sandboxId,
succeeded: !e2bError,
})
if (e2bError) {
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const { formattedError, cleanedOutput } = formatE2BError(
scrubInternalIdentifiers(
getErrorDisplayMessage(e2bError, sourceCodeForErrors, resolvedCode),
compilerInternalIdentifiers
),
e2bStdout,
lang,
errorDisplayCode,
runtime.lineCount
)
return functionJsonResponse(
{
success: false,
error: formattedError,
output: { result: null, stdout: cleanedOutput, executionTime },
},
routeContext,
{ status: 422 }
)
}
if (outputSandboxPaths.length > 0 || outputSandboxPath) {
const fileExportResponse = await maybeExportSandboxFilesToWorkspace({
routeContext,
authUserId: auth.userId,
workflowId,
workspaceId,
workspaceAccess,
outputFiles,
exportedFiles,
exportedFileContent,
stdout,
executionTime,
})
if (fileExportResponse) {
return appendResolvedSecretNames(fileExportResponse, routeContext, cleanStdout(stdout))
}
}
return functionJsonResponse(
{
success: true,
output: { result: e2bResult ?? null, stdout: cleanStdout(stdout), executionTime },
},
routeContext
)
}
const executionMethod = 'isolated-vm'
const isSafeParamKey = (key: string) =>
key !== 'params' && SAFE_IDENTIFIER.test(key) && !JS_RESERVED_WORDS.has(key)
const customToolParamKeys = isCustomTool
? Object.keys(executionParams).filter((key) => {
const safe = isSafeParamKey(key)
if (!safe)
logger.warn('Skipping param key — not a safe JS identifier', { key, requestId })
return safe
})
: []
userCodeStartLine = customToolParamKeys.length + 3
let codeToExecute = resolvedCode
const prependedLineCount = customToolParamKeys.length
if (customToolParamKeys.length > 0) {
const paramDestructuring = customToolParamKeys
.map((key) => `const ${key} = params.${key};`)
.join('\n')
codeToExecute = `${paramDestructuring}\n${resolvedCode}`
}
const isolatedResult = await executeInIsolatedVM(
{
code: codeToExecute,
params: executionParams,
envVars,
contextVariables,
runtimeBindings: compilerRuntimeBindings,
timeoutMs: timeout,
requestId,
ownerKey: `user:${auth.userId}`,
ownerWeight: 1,
},
{ brokers: createFunctionRuntimeBrokers(routeContext), signal: executionSignal }
)
const executionTime = Date.now() - startTime
stdout = isolatedResult.stdout
if (isolatedResult.error) {
if (isolatedResult.termination === 'timeout') {
throw new DOMException('timeout', 'AbortError')
}
if (isolatedResult.termination === 'cancelled') {
throw executionSignal.reason instanceof Error
? executionSignal.reason
: new DOMException('user', 'AbortError')
}
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`, {
executionTime,
isSystemError,
hasStack: Boolean(isolatedResult.error.stack),
})
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 = scrubInternalIdentifiers(
getErrorDisplayMessage(ivmError.message, sourceCodeForErrors, resolvedCode),
compilerInternalIdentifiers
)
adjustedLineContent = getLineContent(errorDisplayCode, adjustedLine) ?? adjustedLineContent
const enhancedError: EnhancedError = {
message: displayMessage,
name: ivmError.name,
stack: ivmError.stack
? scrubInternalIdentifiers(ivmError.stack, compilerInternalIdentifiers)
: undefined,
line: adjustedLine,
column: ivmError.column,
lineContent: adjustedLineContent,
}
const userFriendlyErrorMessage = createUserFriendlyErrorMessage(
enhancedError,
errorDisplayCode
)
const detailLogFn = isSystemError ? logger.error.bind(logger) : logger.warn.bind(logger)
detailLogFn(`[${requestId}] Enhanced error details`, {
line: enhancedError.line,
column: enhancedError.column,
})
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 }
)
}
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 (executionSignal.aborted || (error instanceof Error && error.name === 'AbortError')) {
const timedOut =
executionDeadlineController?.isTimedOut() === true ||
isTimeoutAbortReason(executionSignal.reason) ||
isTimeoutAbortReason(req.signal.reason) ||
isTimeoutAbortReason(error?.cause ?? error) ||
(executionDeadlineAt !== undefined && Date.now() >= executionDeadlineAt)
const abortResponse = {
success: false,
error: timedOut
? `Function execution timed out${timeoutForError ? ` after ${timeoutForError}ms` : ''}`
: 'Function execution was cancelled',
output: { result: null, stdout: cleanStdout(stdout), executionTime },
}
logger.warn(`[${requestId}] Function execution ${timedOut ? 'timed out' : 'was cancelled'}`, {
executionTime,
})
return routeContext
? functionJsonResponse(abortResponse, routeContext, { status: timedOut ? 408 : 499 })
: appendPrivateResolvedSecretNames(
NextResponse.json(abortResponse, { status: timedOut ? 408 : 499 }),
includePrivateResolvedSecretNames ? [] : null,
privateResolvedSecretNamesMetadataType
)
}
if (error instanceof CodePlaceholderCompileError) {
const compilerResponse = {
success: false,
error: scrubInternalIdentifiers(error.message, compilerInternalIdentifiers),
output: { result: null, stdout: cleanStdout(stdout), executionTime },
debug: {
line: error.line,
column: error.column,
errorType: error.name,
lineContent: getLineContent(sourceCodeForErrors ?? '', error.line),
},
}
return routeContext
? functionJsonResponse(compilerResponse, routeContext, { status: 422 })
: appendPrivateResolvedSecretNames(
NextResponse.json(compilerResponse, { status: 422 }),
includePrivateResolvedSecretNames ? [] : null,
privateResolvedSecretNamesMetadataType
)
}
if (isSandboxOutputLimitError(error) || isSandboxOutputFileError(error)) {
const outputLimitResponse = {
success: false,
error: error.message,
output: { result: null, stdout: cleanStdout(stdout), executionTime },
}
return routeContext
? functionJsonResponse(outputLimitResponse, routeContext, { status: 400 })
: appendPrivateResolvedSecretNames(
NextResponse.json(outputLimitResponse, { status: 400 }),
includePrivateResolvedSecretNames ? [] : null,
privateResolvedSecretNamesMetadataType
)
}
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 appendPrivateResolvedSecretNames(
NextResponse.json(
{
success: false,
error: error.message,
output: {
result: null,
stdout: cleanStdout(stdout),
executionTime,
},
},
{ status: error.statusCode }
),
includePrivateResolvedSecretNames ? [] : null,
privateResolvedSecretNamesMetadataType
)
}
if (isLikelySandboxKill(error)) {
const underlying = scrubInternalIdentifiers(
(error?.message || String(error)).slice(0, 300),
compilerInternalIdentifiers
)
logger.warn(`[${requestId}] Sandbox terminated before completion (likely OOM or timeout)`, {
executionTime,
})
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 })
: appendPrivateResolvedSecretNames(
NextResponse.json(killResponse, { status: 500 }),
includePrivateResolvedSecretNames ? [] : null,
privateResolvedSecretNamesMetadataType
)
}
logger.error(`[${requestId}] Function execution failed`, {
executionTime,
hasStack: Boolean(error.stack),
})
const errorDisplayCode = getErrorDisplayCode(sourceCodeForErrors, resolvedCode)
const enhancedError = extractEnhancedError(error, userCodeStartLine, errorDisplayCode)
const userFriendlyErrorMessage = scrubInternalIdentifiers(
createUserFriendlyErrorMessage(enhancedError, errorDisplayCode),
compilerInternalIdentifiers
)
logger.error(`[${requestId}] Enhanced error details`, {
line: enhancedError.line,
column: enhancedError.column,
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
? scrubInternalIdentifiers(enhancedError.lineContent, compilerInternalIdentifiers)
: undefined,
stack: enhancedError.stack
? scrubInternalIdentifiers(enhancedError.stack, compilerInternalIdentifiers)
: undefined,
},
}
if (routeContext) {
return functionJsonResponse(errorResponse, routeContext, { status: 500 })
}
return appendPrivateResolvedSecretNames(
NextResponse.json(errorResponse, { status: 500 }),
includePrivateResolvedSecretNames ? [] : null,
privateResolvedSecretNamesMetadataType
)
} finally {
executionDeadlineController?.cleanup()
}
})