Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions apps/sim/app/api/function/execute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'

const {
mockExecuteInE2B,
mockExecuteInSandbox,
mockExecuteInIsolatedVM,
mockFetchWorkspaceFileBuffer,
mockGetWorkspaceFile,
Expand All @@ -23,7 +23,7 @@ const {
mockValidateWorkspaceFileWriteTarget,
mockWriteWorkspaceFileByPath,
} = vi.hoisted(() => ({
mockExecuteInE2B: vi.fn(),
mockExecuteInSandbox: vi.fn(),
mockExecuteInIsolatedVM: vi.fn(),
mockFetchWorkspaceFileBuffer: vi.fn(),
mockGetWorkspaceFile: vi.fn(),
Expand All @@ -38,9 +38,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 @@ -124,7 +124,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,7 +351,7 @@ describe('Function Execute API Route', () => {

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

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

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

it('returns a targeted error when a declared sandbox output is missing', async () => {
envFlagsMock.isE2bEnabled = true
mockExecuteInE2B.mockResolvedValueOnce({
mockExecuteInSandbox.mockResolvedValueOnce({
result: 'done',
stdout: 'ok',
sandboxId: 'sandbox-123',
Expand Down Expand Up @@ -564,7 +564,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 @@ -590,7 +590,7 @@ describe('Function Execute API Route', () => {
it('flags an overwrite export whose bytes are identical to the current file content as unchanged', async () => {
envFlagsMock.isE2bEnabled = true
const staleContent = '# doc\nunchanged mounted content\n'
mockExecuteInE2B.mockResolvedValueOnce({
mockExecuteInSandbox.mockResolvedValueOnce({
result: 'done',
stdout: 'ok',
sandboxId: 'sandbox-123',
Expand Down Expand Up @@ -637,7 +637,7 @@ describe('Function Execute API Route', () => {
it('reports size, previousSize, and sha256 receipts on a successful overwrite export', async () => {
envFlagsMock.isE2bEnabled = true
const newContent = '# doc\nnew content\n'
mockExecuteInE2B.mockResolvedValueOnce({
mockExecuteInSandbox.mockResolvedValueOnce({
result: 'done',
stdout: 'ok',
sandboxId: 'sandbox-123',
Expand Down Expand Up @@ -681,7 +681,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
12 changes: 8 additions & 4 deletions apps/sim/app/api/function/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
import { isE2bEnabled } from '@/lib/core/config/env-flags'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { executeInE2B, executeShellInE2B, 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 @@ -1537,7 +1541,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
error: shellError,
exportedFileContent,
exportedFiles,
} = await executeShellInE2B({
} = await executeShellInSandbox({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Legacy Gate Blocks Daytona Execution

When Daytona is configured but E2B_ENABLED is false, the existing shell, Python, and JavaScript gates reject the request before this provider-neutral call can resolve Daytona. A Daytona-only failover configuration therefore cannot run function blocks; entry into the remote-sandbox path must depend on the selected provider's availability.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

code: resolvedCode,
envs: shellEnvs,
timeoutMs: timeout,
Expand Down Expand Up @@ -1693,7 +1697,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 +1785,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
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 @@ -62,7 +62,7 @@ const mockModelRuntime = {
vi.mock('@sim/logger', () => ({
createLogger: () => ({ info: vi.fn(), warn: mockLoggerWarn }),
}))
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
2 changes: 1 addition & 1 deletion apps/sim/executor/handlers/pi/cloud-review-tools.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ToolDefinition } from '@earendil-works/pi-coding-agent'
import { Type } from 'typebox'
import type { PiSandboxRunner } from '@/lib/execution/e2b'
import type { PiSandboxRunner } from '@/lib/execution/remote-sandbox'
import { REVIEW_TOOLS_SCRIPT } from '@/executor/handlers/pi/cloud-review-tools-script'
import { raceAbort } from '@/executor/handlers/pi/cloud-shared'
import type { PiSdk } from '@/executor/handlers/pi/pi-sdk'
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/lib/copilot/tools/server/files/doc-compile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
*/
import { describe, expect, it, vi } from 'vitest'

vi.mock('@/lib/execution/e2b', () => ({
executeInE2B: vi.fn(),
executeShellInE2B: vi.fn(),
vi.mock('@/lib/execution/remote-sandbox', () => ({
executeInSandbox: vi.fn(),
executeShellInSandbox: vi.fn(),
}))
vi.mock('@/lib/execution/languages', () => ({
CodeLanguage: { javascript: 'javascript', python: 'python' },
Expand Down
10 changes: 7 additions & 3 deletions apps/sim/lib/copilot/tools/server/files/doc-compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import { sha256Hex } from '@sim/security/hash'
import { getErrorMessage } from '@sim/utils/errors'
import { isE2BDocEnabled } from '@/lib/core/config/env-flags'
import { isFeatureEnabled } from '@/lib/core/config/feature-flags'
import { executeInE2B, executeShellInE2B, type SandboxFile } from '@/lib/execution/e2b'
import { CodeLanguage } from '@/lib/execution/languages'
import {
executeInSandbox,
executeShellInSandbox,
type SandboxFile,
} from '@/lib/execution/remote-sandbox'
import { runSandboxTask } from '@/lib/execution/sandbox/run-task'
import {
fetchWorkspaceFileBuffer,
Expand Down Expand Up @@ -259,7 +263,7 @@ async function compileDocViaE2BPython(
// unaffected. Runs only after the user's script succeeds.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 E2B Gate Hides Daytona Documents

Document compilation still enters this path through isE2BDocEnabled, which requires the E2B API key and E2B document template. With Daytona selected and DAYTONA_DOC_SNAPSHOT_ID configured, removing the unavailable E2B configuration marks the compiler unavailable before this call runs, so document generation fails during the intended failover.

const code = fmt.ext === 'xlsx' ? `${source}\n${XLSX_RECALC_SNIPPET}` : source

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

const result = await executeShellInE2B({
const result = await executeShellInSandbox({
code: 'NODE_PATH=$(npm root -g) node /home/user/script.js',
envs: {},
timeoutMs: DOC_COMPILE_TIMEOUT_MS,
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/copilot/tools/server/files/doc-extract.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { executeInE2B } from '@/lib/execution/e2b'
import { CodeLanguage } from '@/lib/execution/languages'
import { executeInSandbox } from '@/lib/execution/remote-sandbox'

const EXTRACT_TIMEOUT_MS = 120_000
// Bound the text handed back to the agent so a huge document can't blow the
Expand Down Expand Up @@ -87,7 +87,7 @@ text = "\\n".join(out)[:${MAX_EXTRACT_CHARS + 20000}]
print("__SIM_RESULT__=" + json.dumps({"text": text}))
`.trim()

const result = await executeInE2B({
const result = await executeInSandbox({
code: script,
language: CodeLanguage.Python,
timeoutMs: EXTRACT_TIMEOUT_MS,
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/copilot/tools/server/files/doc-recalc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createLogger } from '@sim/logger'
import { executeInE2B } from '@/lib/execution/e2b'
import { CodeLanguage } from '@/lib/execution/languages'
import { executeInSandbox } from '@/lib/execution/remote-sandbox'
import { compileDoc, DocCompileUserError } from './doc-compile'

const logger = createLogger('CopilotDocRecalc')
Expand Down Expand Up @@ -50,7 +50,7 @@ for ws in wb.worksheets:
print("__SIM_RESULT__=" + json.dumps({"ok": len(errors) == 0, "errors": errors[:${MAX_REPORTED_ERRORS}]}))
`.trim()

const result = await executeInE2B({
const result = await executeInSandbox({
code: script,
language: CodeLanguage.Python,
timeoutMs: RECALC_TIMEOUT_MS,
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/copilot/tools/server/files/doc-render.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { executeInE2B } from '@/lib/execution/e2b'
import { CodeLanguage } from '@/lib/execution/languages'
import { executeInSandbox } from '@/lib/execution/remote-sandbox'

const RENDER_TIMEOUT_MS = 150_000
// Bound the visual-QA cost: cap pages and rasterization DPI so the JPEGs the
Expand Down Expand Up @@ -84,7 +84,7 @@ else:
print("__SIM_RESULT__=" + json.dumps({"grid": base64.b64encode(f.read()).decode(), "pageCount": n}))
`.trim()

const result = await executeInE2B({
const result = await executeInSandbox({
code: script,
language: CodeLanguage.Python,
timeoutMs: RENDER_TIMEOUT_MS,
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ const { e2bFlag, betaFlag, mockLoadCompiledDoc, mockRunSandboxTask } = vi.hoiste
mockRunSandboxTask: vi.fn(),
}))

vi.mock('@/lib/execution/e2b', () => ({
executeInE2B: vi.fn(),
executeShellInE2B: vi.fn(),
vi.mock('@/lib/execution/remote-sandbox', () => ({
executeInSandbox: vi.fn(),
executeShellInSandbox: vi.fn(),
}))
vi.mock('@/lib/execution/languages', () => ({
CodeLanguage: { javascript: 'javascript', python: 'python' },
Expand Down
7 changes: 7 additions & 0 deletions apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,13 @@ export const env = createEnv({
MOTHERSHIP_E2B_DOC_TEMPLATE_ID: z.string().optional(), // Dedicated E2B template with python-pptx/docx/openpyxl/reportlab for document generation; when set (and E2B enabled), docs compile via Python instead of the JS isolated-vm path
E2B_PI_TEMPLATE_ID: z.string().optional(), // E2B template ID/alias with the Pi CLI + git baked in (Create PR and Review Code)

// Daytona Remote Code Execution (manual failover for E2B; selected by the sandbox-provider-daytona flag)
DAYTONA_API_KEY: z.string().optional(), // Daytona API key; needs write:snapshots to build images, write:sandboxes to run them
DAYTONA_SHELL_SNAPSHOT_ID: z.string().optional(), // Daytona snapshot mirroring mothership-shell (must carry an explicit tag; latest is rejected)
DAYTONA_DOC_SNAPSHOT_ID: z.string().optional(), // Daytona snapshot mirroring mothership-docs
DAYTONA_PI_SNAPSHOT_ID: z.string().optional(), // Daytona snapshot mirroring the Pi template (Create PR and Review Code)
SANDBOX_PROVIDER_DAYTONA: z.string().optional(), // Fallback for the sandbox-provider-daytona flag when AppConfig is not the source of truth

// Access Control (Permission Groups) - for self-hosted deployments
ACCESS_CONTROL_ENABLED: z.boolean().optional(), // Enable access control on self-hosted (bypasses plan requirements)

Expand Down
10 changes: 10 additions & 0 deletions apps/sim/lib/core/config/feature-flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ const FEATURE_FLAGS = {
'self-hosted/local behaviour. Fallback mirrors FORKING_ENABLED for off-AppConfig reads.',
fallback: 'FORKING_ENABLED',
},
'sandbox-provider-daytona': {
description:
'Route remote sandbox execution (function blocks, shell, doc generation, Pi cloud agent) to ' +
'Daytona instead of E2B — the manual failover for an E2B outage. Global on/off only: ' +
'resolved without user/org context at every sandbox entry point so the whole deployment ' +
'switches together, and resolved ONCE before the sandbox is created so a run is never ' +
'migrated mid-execution (user code has side effects). Requires the DAYTONA_* snapshot ids; ' +
'each sandbox kind fails closed when its snapshot is unset.',
fallback: 'SANDBOX_PROVIDER_DAYTONA',
},
'deploy-as-block': {
description:
'Publish a deployed workflow as a reusable, org-wide custom block (custom name/SVG icon/' +
Expand Down
Loading
Loading