Skip to content
Open
54 changes: 27 additions & 27 deletions apps/sim/app/api/function/execute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { NextRequest } from 'next/server'
import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest'

const {
mockExecuteInE2B,
mockExecuteInSandbox,
mockExecuteInIsolatedVM,
mockFetchWorkspaceFileBuffer,
mockGetWorkspaceFile,
Expand All @@ -24,7 +24,7 @@ const {
mockValidateWorkspaceFileWriteTarget,
mockWriteWorkspaceFileByPath,
} = vi.hoisted(() => ({
mockExecuteInE2B: vi.fn(),
mockExecuteInSandbox: vi.fn(),
mockExecuteInIsolatedVM: vi.fn(),
mockFetchWorkspaceFileBuffer: vi.fn(),
mockGetWorkspaceFile: vi.fn(),
Expand All @@ -39,9 +39,9 @@ vi.mock('@/lib/execution/isolated-vm', () => ({
executeInIsolatedVM: mockExecuteInIsolatedVM,
}))

vi.mock('@/lib/execution/e2b', () => ({
executeInE2B: mockExecuteInE2B,
executeShellInE2B: vi.fn(),
vi.mock('@/lib/execution/remote-sandbox', () => ({
executeInSandbox: mockExecuteInSandbox,
executeShellInSandbox: vi.fn(),
SIM_RESULT_PREFIX: '__SIM_RESULT__=',
}))

Expand Down Expand Up @@ -113,7 +113,7 @@ afterAll(resetEnvFlagsMock)
describe('Function Execute API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
envFlagsMock.isE2bEnabled = false
envFlagsMock.isRemoteSandboxEnabled = false

hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
success: true,
Expand All @@ -125,7 +125,7 @@ describe('Function Execute API Route', () => {
mockUploadFile.mockImplementation(async ({ customKey }) => ({ key: customKey }))
clearLargeValueCacheForTests()

mockExecuteInE2B.mockResolvedValue({
mockExecuteInSandbox.mockResolvedValue({
result: 'e2b success',
stdout: 'e2b output',
sandboxId: 'test-sandbox-id',
Expand Down Expand Up @@ -351,8 +351,8 @@ describe('Function Execute API Route', () => {
})

it('exports multiple declared sandbox output files', async () => {
envFlagsMock.isE2bEnabled = true
mockExecuteInE2B.mockResolvedValueOnce({
envFlagsMock.isRemoteSandboxEnabled = true
mockExecuteInSandbox.mockResolvedValueOnce({
result: 'done',
stdout: 'ok',
sandboxId: 'sandbox-123',
Expand Down Expand Up @@ -389,7 +389,7 @@ describe('Function Execute API Route', () => {

expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(mockExecuteInE2B).toHaveBeenCalledWith(
expect(mockExecuteInSandbox).toHaveBeenCalledWith(
expect.objectContaining({
outputSandboxPaths: ['/home/user/chart.png', '/home/user/summary.json'],
})
Expand Down Expand Up @@ -419,8 +419,8 @@ describe('Function Execute API Route', () => {
})

it('prevalidates all sandbox output destinations before writing any files', async () => {
envFlagsMock.isE2bEnabled = true
mockExecuteInE2B.mockResolvedValueOnce({
envFlagsMock.isRemoteSandboxEnabled = true
mockExecuteInSandbox.mockResolvedValueOnce({
result: 'done',
stdout: 'ok',
sandboxId: 'sandbox-123',
Expand Down Expand Up @@ -463,8 +463,8 @@ describe('Function Execute API Route', () => {
})

it('rejects duplicate sandbox output destinations before writing files', async () => {
envFlagsMock.isE2bEnabled = true
mockExecuteInE2B.mockResolvedValueOnce({
envFlagsMock.isRemoteSandboxEnabled = true
mockExecuteInSandbox.mockResolvedValueOnce({
result: 'done',
stdout: 'ok',
sandboxId: 'sandbox-123',
Expand Down Expand Up @@ -508,8 +508,8 @@ describe('Function Execute API Route', () => {
})

it('returns a targeted error when a declared sandbox output is missing', async () => {
envFlagsMock.isE2bEnabled = true
mockExecuteInE2B.mockResolvedValueOnce({
envFlagsMock.isRemoteSandboxEnabled = true
mockExecuteInSandbox.mockResolvedValueOnce({
result: 'done',
stdout: 'ok',
sandboxId: 'sandbox-123',
Expand Down Expand Up @@ -541,7 +541,7 @@ describe('Function Execute API Route', () => {
})

it('rejects sandboxPath outputs when the call would run in isolated-vm (E2B enabled, JS without imports)', async () => {
envFlagsMock.isE2bEnabled = true
envFlagsMock.isRemoteSandboxEnabled = true

const req = createMockRequest('POST', {
code: 'return "content"',
Expand All @@ -565,7 +565,7 @@ describe('Function Execute API Route', () => {
expect(data.success).toBe(false)
expect(data.error).toContain('no sandbox filesystem')
expect(mockExecuteInIsolatedVM).not.toHaveBeenCalled()
expect(mockExecuteInE2B).not.toHaveBeenCalled()
expect(mockExecuteInSandbox).not.toHaveBeenCalled()
expect(mockWriteWorkspaceFileByPath).not.toHaveBeenCalled()
})

Expand All @@ -582,16 +582,16 @@ describe('Function Execute API Route', () => {

expect(response.status).toBe(422)
expect(data.success).toBe(false)
// E2B is disabled in this test, so the remediation must name that cause
// instead of suggesting python (which would also fail without E2B).
expect(data.error).toContain('E2B is not enabled')
// No remote sandbox is enabled in this test, so the remediation must name
// that cause instead of suggesting python (which would also fail without one).
expect(data.error).toContain('No remote code sandbox is enabled')
expect(mockExecuteInIsolatedVM).not.toHaveBeenCalled()
})

it('flags an overwrite export whose bytes are identical to the current file content as unchanged', async () => {
envFlagsMock.isE2bEnabled = true
envFlagsMock.isRemoteSandboxEnabled = true
const staleContent = '# doc\nunchanged mounted content\n'
mockExecuteInE2B.mockResolvedValueOnce({
mockExecuteInSandbox.mockResolvedValueOnce({
result: 'done',
stdout: 'ok',
sandboxId: 'sandbox-123',
Expand Down Expand Up @@ -636,9 +636,9 @@ describe('Function Execute API Route', () => {
})

it('reports size, previousSize, and sha256 receipts on a successful overwrite export', async () => {
envFlagsMock.isE2bEnabled = true
envFlagsMock.isRemoteSandboxEnabled = true
const newContent = '# doc\nnew content\n'
mockExecuteInE2B.mockResolvedValueOnce({
mockExecuteInSandbox.mockResolvedValueOnce({
result: 'done',
stdout: 'ok',
sandboxId: 'sandbox-123',
Expand Down Expand Up @@ -682,7 +682,7 @@ describe('Function Execute API Route', () => {
expect(data.output.result.message).toContain('sha256:')
// The python wrapper prints the marker with a leading \n so it always
// starts a fresh line even after non-newline-terminated user output.
const e2bCode = mockExecuteInE2B.mock.calls[0][0].code as string
const e2bCode = mockExecuteInSandbox.mock.calls[0][0].code as string
expect(e2bCode).toContain("print('\\n__SIM_RESULT__=' + json.dumps(__sim_result__))")
})

Expand Down Expand Up @@ -727,7 +727,7 @@ describe('Function Execute API Route', () => {
})

it('rejects large refs in runtimes without ref-native helpers', async () => {
envFlagsMock.isE2bEnabled = true
envFlagsMock.isRemoteSandboxEnabled = true
const req = createMockRequest('POST', {
code: 'echo "$__blockRef_0"',
language: 'shell',
Expand Down
57 changes: 32 additions & 25 deletions apps/sim/app/api/function/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@ import {
validateWorkspaceFileWriteTarget,
writeWorkspaceFileByPath,
} from '@/lib/copilot/vfs/resource-writer'
import { isE2bEnabled } from '@/lib/core/config/env-flags'
import { isRemoteSandboxEnabled } 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, SIM_RESULT_PREFIX } 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'
Expand All @@ -36,6 +35,11 @@ import {
} from '@/lib/execution/payloads/materialization.server'
import { compactExecutionPayload } from '@/lib/execution/payloads/serializer'
import { materializeLargeValueRef } from '@/lib/execution/payloads/store'
import {
executeInSandbox,
executeShellInSandbox,
SIM_RESULT_PREFIX,
} from '@/lib/execution/remote-sandbox'
import { isExecutionResourceLimitError } from '@/lib/execution/resource-errors'
import {
fetchWorkspaceFileBuffer,
Expand Down Expand Up @@ -1509,9 +1513,9 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
}

if (lang === CodeLanguage.Shell) {
if (!isE2bEnabled) {
if (!isRemoteSandboxEnabled) {
throw new Error(
'Shell execution requires E2B to be enabled. Please contact your administrator to enable E2B.'
'Shell execution requires a remote code sandbox to be enabled. Please contact your administrator to enable it.'
)
}

Expand All @@ -1524,7 +1528,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
}

logger.info(`[${requestId}] E2B shell execution`, {
enabled: isE2bEnabled,
enabled: isRemoteSandboxEnabled,
hasApiKey: Boolean(process.env.E2B_API_KEY),
envVarCount: Object.keys(shellEnvs).length,
})
Expand All @@ -1537,7 +1541,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
error: shellError,
exportedFileContent,
exportedFiles,
} = await executeShellInE2B({
} = await executeShellInSandbox({
Comment thread
TheodoreSpeaks marked this conversation as resolved.
code: resolvedCode,
envs: shellEnvs,
timeoutMs: timeout,
Expand Down Expand Up @@ -1589,41 +1593,44 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
)
}

if (lang === CodeLanguage.Python && !isE2bEnabled) {
if (lang === CodeLanguage.Python && !isRemoteSandboxEnabled) {
throw new Error(
'Python execution requires E2B to be enabled. Please contact your administrator to enable E2B, or use JavaScript instead.'
'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 && !isE2bEnabled) {
if (lang === CodeLanguage.JavaScript && hasImports && !isRemoteSandboxEnabled) {
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.'
'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 useE2B =
isE2bEnabled &&
const useRemoteSandbox =
isRemoteSandboxEnabled &&
!isCustomTool &&
(lang === CodeLanguage.Python || (lang === CodeLanguage.JavaScript && hasImports))

if (useE2B && containsLargeValueRef(contextVariables)) {
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 E2B.'
'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 E2B
// runtime; isolated-vm has no filesystem. Silently dropping a declared
// 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 E2B is disabled or the call is a custom tool.
if (!useE2B && (outputSandboxPaths.length > 0 || outputSandboxPath || _sandboxFiles?.length)) {
const remediation = !isE2bEnabled
? "E2B is not 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)."
// when no remote sandbox is enabled or the call is a custom tool.
if (
!useRemoteSandbox &&
(outputSandboxPaths.length > 0 || outputSandboxPath || _sandboxFiles?.length)
) {
const remediation = !isRemoteSandboxEnabled
? "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)."
Comment thread
TheodoreSpeaks marked this conversation as resolved.
: 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 E2B sandbox, or drop sandboxPath and return the file content as the code\'s return value with outputs.files[].path.'
: '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,
Expand All @@ -1635,9 +1642,9 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
)
}

if (useE2B) {
if (useRemoteSandbox) {
logger.info(`[${requestId}] E2B status`, {
enabled: isE2bEnabled,
enabled: isRemoteSandboxEnabled,
hasApiKey: Boolean(process.env.E2B_API_KEY),
language: lang,
})
Expand Down Expand Up @@ -1693,7 +1700,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
error: e2bError,
exportedFileContent,
exportedFiles,
} = await executeInE2B({
} = await executeInSandbox({
code: codeForE2B,
language: CodeLanguage.JavaScript,
timeoutMs: timeout,
Expand Down Expand Up @@ -1781,7 +1788,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
error: e2bError,
exportedFileContent,
exportedFiles,
} = await executeInE2B({
} = await executeInSandbox({
code: codeForE2B,
language: CodeLanguage.Python,
timeoutMs: timeout,
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/mothership/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { buildSelectedMcpToolSchemas, buildTaggedMcpToolSchemas } from '@/lib/co
import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless'
import { requestExplicitStreamAbort } from '@/lib/copilot/request/session/explicit-abort'
import type { StreamEvent } from '@/lib/copilot/request/types'
import { isE2BDocEnabled } from '@/lib/core/config/env-flags'
import { isDocSandboxEnabled } from '@/lib/core/config/env-flags'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import {
assertActiveWorkspaceAccess,
Expand Down Expand Up @@ -199,7 +199,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
messageId,
isHosted: true,
workspaceContext,
...(isE2BDocEnabled ? { docCompiler: 'python' } : {}),
...(isDocSandboxEnabled ? { docCompiler: 'python' } : {}),
...(userMetadata ? { userMetadata } : {}),
...(fileAttachments && fileAttachments.length > 0 ? { fileAttachments } : {}),
...(agentContexts.length > 0 || mothershipTools.length > 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { getE2BDocFormat } from '@/lib/copilot/tools/server/files/doc-compile'
import { runE2BCompiledCheck } from '@/lib/copilot/tools/server/files/doc-recalc'
import { isE2BDocEnabled } from '@/lib/core/config/env-flags'
import { isDocSandboxEnabled } from '@/lib/core/config/env-flags'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { BINARY_DOC_TASKS, MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants'
import { runSandboxTask, SandboxUserCodeError } from '@/lib/execution/sandbox/run-task'
Expand Down Expand Up @@ -57,7 +57,7 @@ export const GET = withRouteHandler(
// In the E2B regime ALL four formats compile in the doc sandbox (Node for
// pptx/docx, Python for pdf/xlsx). Gate on the flag (not the stored MIME) so
// a stale file can't trigger an E2B compile when the sandbox is disabled.
const e2bFmt = isE2BDocEnabled ? await getE2BDocFormat(fileRecord.name) : null
const e2bFmt = isDocSandboxEnabled ? await getE2BDocFormat(fileRecord.name) : null
const taskId = BINARY_DOC_TASKS[ext]
const isMermaidFile = ext === 'mmd' || ext === 'mermaid'
if (!e2bFmt && !taskId && !isMermaidFile) {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/executor/handlers/pi/cloud-backend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const { mockRun, mockReadFile, mockWriteFile, mockExecuteTool, mockProviderEnvVa
})
)

vi.mock('@/lib/execution/e2b', () => ({
vi.mock('@/lib/execution/remote-sandbox', () => ({
withPiSandbox: (fn: (runner: unknown) => unknown) =>
fn({ run: mockRun, readFile: mockReadFile, writeFile: mockWriteFile }),
}))
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/executor/handlers/pi/cloud-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import { createLogger } from '@sim/logger'
import { generateShortId } from '@sim/utils/id'
import { truncate } from '@sim/utils/string'
import { withPiSandbox } from '@/lib/execution/e2b'
import { withPiSandbox } from '@/lib/execution/remote-sandbox'
import type { PiBackendRun, PiCloudRunParams } from '@/executor/handlers/pi/backend'
import {
CLONE_TIMEOUT_MS,
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/executor/handlers/pi/cloud-review-backend.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const mockModelRuntime = {
removeRuntimeApiKey: mockRemoveRuntimeApiKey,
}

vi.mock('@/lib/execution/e2b', () => ({
vi.mock('@/lib/execution/remote-sandbox', () => ({
withPiSandbox: (fn: (runner: unknown) => unknown) =>
fn({ run: mockRun, writeFile: mockWriteFile }),
}))
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/executor/handlers/pi/cloud-review-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createLogger } from '@sim/logger'
import { truncate } from '@sim/utils/string'
import { withPiSandbox } from '@/lib/execution/e2b'
import { withPiSandbox } from '@/lib/execution/remote-sandbox'
import type { PiBackendRun, PiCloudReviewRunParams } from '@/executor/handlers/pi/backend'
import {
CLOUD_REVIEW_TOOL_NAMES,
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/executor/handlers/pi/cloud-review-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { join } from 'node:path'
import { promisify } from 'node:util'
import * as sdk from '@earendil-works/pi-coding-agent'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { PiSandboxRunner } from '@/lib/execution/e2b'
import type { PiSandboxRunner } from '@/lib/execution/remote-sandbox'
import {
CLOUD_REVIEW_TOOL_NAMES,
createCloudReviewTools,
Expand Down
Loading
Loading