Skip to content

Commit e201d6c

Browse files
feat(sandbox): add Daytona as a manual-flip failover for E2B
E2B was a hard single point of failure: lib/execution/e2b.ts had no retry and no fallback, so a failed Sandbox.create() killed Python function blocks, JS-with-imports, shell, doc generation and the Pi cloud agent outright. Extract a SandboxRunner boundary (lib/execution/remote-sandbox) with an E2B runner and a Daytona runner, selected once per execution by the sandbox-provider-daytona AppConfig flag. Everything above the provider boundary — marker parsing, mount materialization, file export, corruption handling — is unchanged. Selection resolves before create() and never mid-execution, since user code has side effects. Each sandbox kind fails closed when its snapshot id is unset. Notes on the Daytona adapter: - language binds at create(), not per call: Daytona applies it as a sandbox label and silently runs JS through Python if passed to codeRun - Python routes via CodeInterpreter for its {name,value,traceback} error shape, which matches E2B's and keeps formatE2BError's line offsets correct - timeouts convert ms to seconds - the streaming path delivers env via the filesystem API, as SessionExecuteRequest has no env field and secrets must not reach a command line Drops the dead E2BExecutionResult.images field (populated, never consumed).
1 parent 49d3804 commit e201d6c

30 files changed

Lines changed: 1823 additions & 870 deletions

apps/sim/app/api/function/execute/route.test.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { NextRequest } from 'next/server'
1313
import { beforeEach, describe, expect, it, vi } from 'vitest'
1414

1515
const {
16-
mockExecuteInE2B,
16+
mockExecuteInSandbox,
1717
mockExecuteInIsolatedVM,
1818
mockFetchWorkspaceFileBuffer,
1919
mockGetWorkspaceFile,
@@ -23,7 +23,7 @@ const {
2323
mockValidateWorkspaceFileWriteTarget,
2424
mockWriteWorkspaceFileByPath,
2525
} = vi.hoisted(() => ({
26-
mockExecuteInE2B: vi.fn(),
26+
mockExecuteInSandbox: vi.fn(),
2727
mockExecuteInIsolatedVM: vi.fn(),
2828
mockFetchWorkspaceFileBuffer: vi.fn(),
2929
mockGetWorkspaceFile: vi.fn(),
@@ -38,9 +38,9 @@ vi.mock('@/lib/execution/isolated-vm', () => ({
3838
executeInIsolatedVM: mockExecuteInIsolatedVM,
3939
}))
4040

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

@@ -124,7 +124,7 @@ describe('Function Execute API Route', () => {
124124
mockUploadFile.mockImplementation(async ({ customKey }) => ({ key: customKey }))
125125
clearLargeValueCacheForTests()
126126

127-
mockExecuteInE2B.mockResolvedValue({
127+
mockExecuteInSandbox.mockResolvedValue({
128128
result: 'e2b success',
129129
stdout: 'e2b output',
130130
sandboxId: 'test-sandbox-id',
@@ -351,7 +351,7 @@ describe('Function Execute API Route', () => {
351351

352352
it('exports multiple declared sandbox output files', async () => {
353353
envFlagsMock.isE2bEnabled = true
354-
mockExecuteInE2B.mockResolvedValueOnce({
354+
mockExecuteInSandbox.mockResolvedValueOnce({
355355
result: 'done',
356356
stdout: 'ok',
357357
sandboxId: 'sandbox-123',
@@ -388,7 +388,7 @@ describe('Function Execute API Route', () => {
388388

389389
expect(response.status).toBe(200)
390390
expect(data.success).toBe(true)
391-
expect(mockExecuteInE2B).toHaveBeenCalledWith(
391+
expect(mockExecuteInSandbox).toHaveBeenCalledWith(
392392
expect.objectContaining({
393393
outputSandboxPaths: ['/home/user/chart.png', '/home/user/summary.json'],
394394
})
@@ -419,7 +419,7 @@ describe('Function Execute API Route', () => {
419419

420420
it('prevalidates all sandbox output destinations before writing any files', async () => {
421421
envFlagsMock.isE2bEnabled = true
422-
mockExecuteInE2B.mockResolvedValueOnce({
422+
mockExecuteInSandbox.mockResolvedValueOnce({
423423
result: 'done',
424424
stdout: 'ok',
425425
sandboxId: 'sandbox-123',
@@ -463,7 +463,7 @@ describe('Function Execute API Route', () => {
463463

464464
it('rejects duplicate sandbox output destinations before writing files', async () => {
465465
envFlagsMock.isE2bEnabled = true
466-
mockExecuteInE2B.mockResolvedValueOnce({
466+
mockExecuteInSandbox.mockResolvedValueOnce({
467467
result: 'done',
468468
stdout: 'ok',
469469
sandboxId: 'sandbox-123',
@@ -508,7 +508,7 @@ describe('Function Execute API Route', () => {
508508

509509
it('returns a targeted error when a declared sandbox output is missing', async () => {
510510
envFlagsMock.isE2bEnabled = true
511-
mockExecuteInE2B.mockResolvedValueOnce({
511+
mockExecuteInSandbox.mockResolvedValueOnce({
512512
result: 'done',
513513
stdout: 'ok',
514514
sandboxId: 'sandbox-123',
@@ -564,7 +564,7 @@ describe('Function Execute API Route', () => {
564564
expect(data.success).toBe(false)
565565
expect(data.error).toContain('no sandbox filesystem')
566566
expect(mockExecuteInIsolatedVM).not.toHaveBeenCalled()
567-
expect(mockExecuteInE2B).not.toHaveBeenCalled()
567+
expect(mockExecuteInSandbox).not.toHaveBeenCalled()
568568
expect(mockWriteWorkspaceFileByPath).not.toHaveBeenCalled()
569569
})
570570

@@ -590,7 +590,7 @@ describe('Function Execute API Route', () => {
590590
it('flags an overwrite export whose bytes are identical to the current file content as unchanged', async () => {
591591
envFlagsMock.isE2bEnabled = true
592592
const staleContent = '# doc\nunchanged mounted content\n'
593-
mockExecuteInE2B.mockResolvedValueOnce({
593+
mockExecuteInSandbox.mockResolvedValueOnce({
594594
result: 'done',
595595
stdout: 'ok',
596596
sandboxId: 'sandbox-123',
@@ -637,7 +637,7 @@ describe('Function Execute API Route', () => {
637637
it('reports size, previousSize, and sha256 receipts on a successful overwrite export', async () => {
638638
envFlagsMock.isE2bEnabled = true
639639
const newContent = '# doc\nnew content\n'
640-
mockExecuteInE2B.mockResolvedValueOnce({
640+
mockExecuteInSandbox.mockResolvedValueOnce({
641641
result: 'done',
642642
stdout: 'ok',
643643
sandboxId: 'sandbox-123',
@@ -681,7 +681,7 @@ describe('Function Execute API Route', () => {
681681
expect(data.output.result.message).toContain('sha256:')
682682
// The python wrapper prints the marker with a leading \n so it always
683683
// starts a fresh line even after non-newline-terminated user output.
684-
const e2bCode = mockExecuteInE2B.mock.calls[0][0].code as string
684+
const e2bCode = mockExecuteInSandbox.mock.calls[0][0].code as string
685685
expect(e2bCode).toContain("print('\\n__SIM_RESULT__=' + json.dumps(__sim_result__))")
686686
})
687687

apps/sim/app/api/function/execute/route.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import {
1919
import { isE2bEnabled } from '@/lib/core/config/env-flags'
2020
import { generateRequestId } from '@/lib/core/utils/request'
2121
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
22-
import { executeInE2B, executeShellInE2B, SIM_RESULT_PREFIX } from '@/lib/execution/e2b'
2322
import { executeInIsolatedVM, type IsolatedVMBrokerHandler } from '@/lib/execution/isolated-vm'
2423
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
2524
import { recordMaterializedAccessKeys } from '@/lib/execution/payloads/access-keys'
@@ -36,6 +35,11 @@ import {
3635
} from '@/lib/execution/payloads/materialization.server'
3736
import { compactExecutionPayload } from '@/lib/execution/payloads/serializer'
3837
import { materializeLargeValueRef } from '@/lib/execution/payloads/store'
38+
import {
39+
executeInSandbox,
40+
executeShellInSandbox,
41+
SIM_RESULT_PREFIX,
42+
} from '@/lib/execution/remote-sandbox'
3943
import { isExecutionResourceLimitError } from '@/lib/execution/resource-errors'
4044
import {
4145
fetchWorkspaceFileBuffer,
@@ -1537,7 +1541,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
15371541
error: shellError,
15381542
exportedFileContent,
15391543
exportedFiles,
1540-
} = await executeShellInE2B({
1544+
} = await executeShellInSandbox({
15411545
code: resolvedCode,
15421546
envs: shellEnvs,
15431547
timeoutMs: timeout,
@@ -1693,7 +1697,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
16931697
error: e2bError,
16941698
exportedFileContent,
16951699
exportedFiles,
1696-
} = await executeInE2B({
1700+
} = await executeInSandbox({
16971701
code: codeForE2B,
16981702
language: CodeLanguage.JavaScript,
16991703
timeoutMs: timeout,
@@ -1781,7 +1785,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
17811785
error: e2bError,
17821786
exportedFileContent,
17831787
exportedFiles,
1784-
} = await executeInE2B({
1788+
} = await executeInSandbox({
17851789
code: codeForE2B,
17861790
language: CodeLanguage.Python,
17871791
timeoutMs: timeout,

apps/sim/executor/handlers/pi/cloud-backend.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const { mockRun, mockReadFile, mockWriteFile, mockExecuteTool, mockProviderEnvVa
1313
})
1414
)
1515

16-
vi.mock('@/lib/execution/e2b', () => ({
16+
vi.mock('@/lib/execution/remote-sandbox', () => ({
1717
withPiSandbox: (fn: (runner: unknown) => unknown) =>
1818
fn({ run: mockRun, readFile: mockReadFile, writeFile: mockWriteFile }),
1919
}))

apps/sim/executor/handlers/pi/cloud-backend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import { createLogger } from '@sim/logger'
1616
import { generateShortId } from '@sim/utils/id'
1717
import { truncate } from '@sim/utils/string'
18-
import { withPiSandbox } from '@/lib/execution/e2b'
18+
import { withPiSandbox } from '@/lib/execution/remote-sandbox'
1919
import type { PiBackendRun, PiCloudRunParams } from '@/executor/handlers/pi/backend'
2020
import {
2121
CLONE_TIMEOUT_MS,

apps/sim/executor/handlers/pi/cloud-review-backend.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const mockModelRuntime = {
6262
vi.mock('@sim/logger', () => ({
6363
createLogger: () => ({ info: vi.fn(), warn: mockLoggerWarn }),
6464
}))
65-
vi.mock('@/lib/execution/e2b', () => ({
65+
vi.mock('@/lib/execution/remote-sandbox', () => ({
6666
withPiSandbox: (fn: (runner: unknown) => unknown) =>
6767
fn({ run: mockRun, writeFile: mockWriteFile }),
6868
}))

apps/sim/executor/handlers/pi/cloud-review-backend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { tmpdir } from 'node:os'
99
import { join } from 'node:path'
1010
import { createLogger } from '@sim/logger'
1111
import { truncate } from '@sim/utils/string'
12-
import { withPiSandbox } from '@/lib/execution/e2b'
12+
import { withPiSandbox } from '@/lib/execution/remote-sandbox'
1313
import type { PiBackendRun, PiCloudReviewRunParams } from '@/executor/handlers/pi/backend'
1414
import {
1515
CLOUD_REVIEW_TOOL_NAMES,

apps/sim/executor/handlers/pi/cloud-review-tools.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { join } from 'node:path'
88
import { promisify } from 'node:util'
99
import * as sdk from '@earendil-works/pi-coding-agent'
1010
import { beforeEach, describe, expect, it, vi } from 'vitest'
11-
import type { PiSandboxRunner } from '@/lib/execution/e2b'
11+
import type { PiSandboxRunner } from '@/lib/execution/remote-sandbox'
1212
import {
1313
CLOUD_REVIEW_TOOL_NAMES,
1414
createCloudReviewTools,

apps/sim/executor/handlers/pi/cloud-review-tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ToolDefinition } from '@earendil-works/pi-coding-agent'
22
import { Type } from 'typebox'
3-
import type { PiSandboxRunner } from '@/lib/execution/e2b'
3+
import type { PiSandboxRunner } from '@/lib/execution/remote-sandbox'
44
import { REVIEW_TOOLS_SCRIPT } from '@/executor/handlers/pi/cloud-review-tools-script'
55
import { raceAbort } from '@/executor/handlers/pi/cloud-shared'
66
import type { PiSdk } from '@/executor/handlers/pi/pi-sdk'

apps/sim/lib/copilot/tools/server/files/doc-compile.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
*/
44
import { describe, expect, it, vi } from 'vitest'
55

6-
vi.mock('@/lib/execution/e2b', () => ({
7-
executeInE2B: vi.fn(),
8-
executeShellInE2B: vi.fn(),
6+
vi.mock('@/lib/execution/remote-sandbox', () => ({
7+
executeInSandbox: vi.fn(),
8+
executeShellInSandbox: vi.fn(),
99
}))
1010
vi.mock('@/lib/execution/languages', () => ({
1111
CodeLanguage: { javascript: 'javascript', python: 'python' },

apps/sim/lib/copilot/tools/server/files/doc-compile.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import { sha256Hex } from '@sim/security/hash'
33
import { getErrorMessage } from '@sim/utils/errors'
44
import { isE2BDocEnabled } from '@/lib/core/config/env-flags'
55
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
6-
import { executeInE2B, executeShellInE2B, type SandboxFile } from '@/lib/execution/e2b'
76
import { CodeLanguage } from '@/lib/execution/languages'
7+
import {
8+
executeInSandbox,
9+
executeShellInSandbox,
10+
type SandboxFile,
11+
} from '@/lib/execution/remote-sandbox'
812
import { runSandboxTask } from '@/lib/execution/sandbox/run-task'
913
import {
1014
fetchWorkspaceFileBuffer,
@@ -259,7 +263,7 @@ async function compileDocViaE2BPython(
259263
// unaffected. Runs only after the user's script succeeds.
260264
const code = fmt.ext === 'xlsx' ? `${source}\n${XLSX_RECALC_SNIPPET}` : source
261265

262-
const result = await executeInE2B({
266+
const result = await executeInSandbox({
263267
code,
264268
language: CodeLanguage.Python,
265269
timeoutMs: DOC_COMPILE_TIMEOUT_MS,
@@ -342,7 +346,7 @@ ${finalize}
342346
})().then(() => console.log('__DOC_OK__')).catch((e) => { console.error('__DOC_ERR__' + (e && e.message ? e.message : String(e))); process.exit(1); });
343347
`
344348

345-
const result = await executeShellInE2B({
349+
const result = await executeShellInSandbox({
346350
code: 'NODE_PATH=$(npm root -g) node /home/user/script.js',
347351
envs: {},
348352
timeoutMs: DOC_COMPILE_TIMEOUT_MS,

0 commit comments

Comments
 (0)