From 78f6bb33429a61573d49ae79d90f23f7dd1ee5e2 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 22 Jul 2026 14:33:15 -0700 Subject: [PATCH 1/8] feat(sandbox): add Daytona as a manual-flip failover for E2B MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- .../app/api/function/execute/route.test.ts | 30 +- apps/sim/app/api/function/execute/route.ts | 12 +- .../handlers/pi/cloud-backend.test.ts | 2 +- .../sim/executor/handlers/pi/cloud-backend.ts | 2 +- .../handlers/pi/cloud-review-backend.test.ts | 2 +- .../handlers/pi/cloud-review-backend.ts | 2 +- .../handlers/pi/cloud-review-tools.test.ts | 2 +- .../handlers/pi/cloud-review-tools.ts | 2 +- .../tools/server/files/doc-compile.test.ts | 6 +- .../copilot/tools/server/files/doc-compile.ts | 10 +- .../copilot/tools/server/files/doc-extract.ts | 4 +- .../copilot/tools/server/files/doc-recalc.ts | 4 +- .../copilot/tools/server/files/doc-render.ts | 4 +- .../tools/server/files/doc-servable.test.ts | 6 +- apps/sim/lib/core/config/env.ts | 7 + apps/sim/lib/core/config/feature-flags.ts | 10 + apps/sim/lib/execution/e2b.test.ts | 305 ----------- apps/sim/lib/execution/e2b.ts | 516 ------------------ .../remote-sandbox/conformance.test.ts | 342 ++++++++++++ .../lib/execution/remote-sandbox/daytona.ts | 210 +++++++ apps/sim/lib/execution/remote-sandbox/e2b.ts | 131 +++++ .../sim/lib/execution/remote-sandbox/index.ts | 423 ++++++++++++++ .../sim/lib/execution/remote-sandbox/types.ts | 129 +++++ apps/sim/next.config.ts | 1 + apps/sim/package.json | 1 + apps/sim/scripts/build-pi-daytona-snapshot.ts | 101 ++++ apps/sim/scripts/build-pi-e2b-template.ts | 23 +- apps/sim/scripts/pi-sandbox-packages.ts | 54 ++ apps/sim/scripts/verify-sandbox-parity.ts | 198 +++++++ bun.lock | 161 +++++- 30 files changed, 1823 insertions(+), 877 deletions(-) delete mode 100644 apps/sim/lib/execution/e2b.test.ts delete mode 100644 apps/sim/lib/execution/e2b.ts create mode 100644 apps/sim/lib/execution/remote-sandbox/conformance.test.ts create mode 100644 apps/sim/lib/execution/remote-sandbox/daytona.ts create mode 100644 apps/sim/lib/execution/remote-sandbox/e2b.ts create mode 100644 apps/sim/lib/execution/remote-sandbox/index.ts create mode 100644 apps/sim/lib/execution/remote-sandbox/types.ts create mode 100644 apps/sim/scripts/build-pi-daytona-snapshot.ts create mode 100644 apps/sim/scripts/pi-sandbox-packages.ts create mode 100644 apps/sim/scripts/verify-sandbox-parity.ts diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index 4b5c2194584..0292896f702 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -14,7 +14,7 @@ import { NextRequest } from 'next/server' import { afterAll, beforeEach, describe, expect, it, vi } from 'vitest' const { - mockExecuteInE2B, + mockExecuteInSandbox, mockExecuteInIsolatedVM, mockFetchWorkspaceFileBuffer, mockGetWorkspaceFile, @@ -24,7 +24,7 @@ const { mockValidateWorkspaceFileWriteTarget, mockWriteWorkspaceFileByPath, } = vi.hoisted(() => ({ - mockExecuteInE2B: vi.fn(), + mockExecuteInSandbox: vi.fn(), mockExecuteInIsolatedVM: vi.fn(), mockFetchWorkspaceFileBuffer: vi.fn(), mockGetWorkspaceFile: vi.fn(), @@ -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__=', })) @@ -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', @@ -352,7 +352,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', @@ -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'], }) @@ -420,7 +420,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', @@ -464,7 +464,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', @@ -509,7 +509,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', @@ -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() }) @@ -591,7 +591,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', @@ -638,7 +638,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', @@ -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__))") }) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 20568fd2010..f032abfa943 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -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' @@ -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, @@ -1537,7 +1541,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { error: shellError, exportedFileContent, exportedFiles, - } = await executeShellInE2B({ + } = await executeShellInSandbox({ code: resolvedCode, envs: shellEnvs, timeoutMs: timeout, @@ -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, @@ -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, diff --git a/apps/sim/executor/handlers/pi/cloud-backend.test.ts b/apps/sim/executor/handlers/pi/cloud-backend.test.ts index ada9859cd39..8107c62bcf3 100644 --- a/apps/sim/executor/handlers/pi/cloud-backend.test.ts +++ b/apps/sim/executor/handlers/pi/cloud-backend.test.ts @@ -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 }), })) diff --git a/apps/sim/executor/handlers/pi/cloud-backend.ts b/apps/sim/executor/handlers/pi/cloud-backend.ts index ce91e7c957f..5b46820140d 100644 --- a/apps/sim/executor/handlers/pi/cloud-backend.ts +++ b/apps/sim/executor/handlers/pi/cloud-backend.ts @@ -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, diff --git a/apps/sim/executor/handlers/pi/cloud-review-backend.test.ts b/apps/sim/executor/handlers/pi/cloud-review-backend.test.ts index d5131ec0403..83e9b308b31 100644 --- a/apps/sim/executor/handlers/pi/cloud-review-backend.test.ts +++ b/apps/sim/executor/handlers/pi/cloud-review-backend.test.ts @@ -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 }), })) diff --git a/apps/sim/executor/handlers/pi/cloud-review-backend.ts b/apps/sim/executor/handlers/pi/cloud-review-backend.ts index f051aec4e13..1b7f2bf91e9 100644 --- a/apps/sim/executor/handlers/pi/cloud-review-backend.ts +++ b/apps/sim/executor/handlers/pi/cloud-review-backend.ts @@ -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, diff --git a/apps/sim/executor/handlers/pi/cloud-review-tools.test.ts b/apps/sim/executor/handlers/pi/cloud-review-tools.test.ts index c2aded09d48..4cf18a9f3a2 100644 --- a/apps/sim/executor/handlers/pi/cloud-review-tools.test.ts +++ b/apps/sim/executor/handlers/pi/cloud-review-tools.test.ts @@ -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, diff --git a/apps/sim/executor/handlers/pi/cloud-review-tools.ts b/apps/sim/executor/handlers/pi/cloud-review-tools.ts index eb1e8a15da3..192e049bf6a 100644 --- a/apps/sim/executor/handlers/pi/cloud-review-tools.ts +++ b/apps/sim/executor/handlers/pi/cloud-review-tools.ts @@ -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' diff --git a/apps/sim/lib/copilot/tools/server/files/doc-compile.test.ts b/apps/sim/lib/copilot/tools/server/files/doc-compile.test.ts index f65efd94542..18a53134c53 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-compile.test.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-compile.test.ts @@ -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' }, diff --git a/apps/sim/lib/copilot/tools/server/files/doc-compile.ts b/apps/sim/lib/copilot/tools/server/files/doc-compile.ts index b9a6ea0a260..c136450970c 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-compile.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-compile.ts @@ -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, @@ -259,7 +263,7 @@ async function compileDocViaE2BPython( // unaffected. Runs only after the user's script succeeds. 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, @@ -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, diff --git a/apps/sim/lib/copilot/tools/server/files/doc-extract.ts b/apps/sim/lib/copilot/tools/server/files/doc-extract.ts index 1674b7c7af7..92230ac871a 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-extract.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-extract.ts @@ -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 @@ -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, diff --git a/apps/sim/lib/copilot/tools/server/files/doc-recalc.ts b/apps/sim/lib/copilot/tools/server/files/doc-recalc.ts index dd8c43b3499..c85b9249110 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-recalc.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-recalc.ts @@ -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') @@ -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, diff --git a/apps/sim/lib/copilot/tools/server/files/doc-render.ts b/apps/sim/lib/copilot/tools/server/files/doc-render.ts index 70b3a2a2d97..24262b48825 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-render.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-render.ts @@ -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 @@ -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, diff --git a/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts b/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts index 73a1b4d4892..bdac8e0e129 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts @@ -10,9 +10,9 @@ const { betaFlag, mockLoadCompiledDoc, mockRunSandboxTask } = vi.hoisted(() => ( 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' }, diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 683f4356ac8..7fca56733a5 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -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) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index 558707fa587..7981cea2eed 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -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/' + diff --git a/apps/sim/lib/execution/e2b.test.ts b/apps/sim/lib/execution/e2b.test.ts deleted file mode 100644 index d5d45fbd03a..00000000000 --- a/apps/sim/lib/execution/e2b.test.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** - * @vitest-environment node - */ -import { resetEnvMock, setEnv } from '@sim/testing' -import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' - -beforeAll(() => { - setEnv({ E2B_API_KEY: 'test-key' }) -}) - -afterAll(resetEnvMock) - -import { CodeLanguage } from '@/lib/execution/languages' - -const { mockCreate, mockRunCode, mockCommandsRun, mockFilesWrite, mockKill } = vi.hoisted(() => ({ - mockCreate: vi.fn(), - mockRunCode: vi.fn(), - mockCommandsRun: vi.fn(), - mockFilesWrite: vi.fn(), - mockKill: vi.fn(), -})) - -vi.mock('@e2b/code-interpreter', () => ({ Sandbox: { create: mockCreate } })) - -import { executeInE2B, executeShellInE2B } from '@/lib/execution/e2b' - -describe('e2b sandbox inputs', () => { - beforeEach(() => { - vi.clearAllMocks() - mockCreate.mockResolvedValue({ - sandboxId: 'sb_1', - files: { write: mockFilesWrite }, - commands: { run: mockCommandsRun }, - runCode: mockRunCode, - kill: mockKill, - }) - mockRunCode.mockResolvedValue({ - error: null, - text: '', - logs: { stdout: [], stderr: [] }, - results: [], - }) - // Default: shell code run + any fetch succeed. - mockCommandsRun.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }) - }) - - it('fetches a url entry via curl with URL/DST/DIR passed as envs (no inline write)', async () => { - await executeInE2B({ - code: 'x', - language: CodeLanguage.JavaScript, - timeoutMs: 1000, - sandboxFiles: [ - { type: 'url', path: '/home/user/tables/t.csv', url: 'https://s3.example/p?a=1&b=2' }, - ], - }) - - expect(mockCommandsRun).toHaveBeenCalledTimes(1) - const [cmd, opts] = mockCommandsRun.mock.calls[0] - expect(cmd).toContain('curl') - expect(cmd).toContain('mkdir -p') - // URL/path go through envs, never interpolated into the command string. - expect(cmd).not.toContain('https://s3.example') - expect(opts.envs).toEqual({ - URL: 'https://s3.example/p?a=1&b=2', - DST: '/home/user/tables/t.csv', - DIR: '/home/user/tables', - }) - expect(opts.user).toBeUndefined() // code sandbox runs as default user - expect(mockFilesWrite).not.toHaveBeenCalled() - }) - - it('writes a content entry inline (no fetch)', async () => { - await executeInE2B({ - code: 'x', - language: CodeLanguage.JavaScript, - timeoutMs: 1000, - sandboxFiles: [{ path: '/home/user/f.txt', content: 'hi' }], - }) - - expect(mockFilesWrite).toHaveBeenCalledWith('/home/user/f.txt', 'hi') - expect(mockCommandsRun).not.toHaveBeenCalled() - }) - - it('fetches as root in the shell sandbox', async () => { - await executeShellInE2B({ - code: 'echo hi', - envs: {}, - timeoutMs: 1000, - sandboxFiles: [{ type: 'url', path: '/home/user/tables/t.csv', url: 'https://s3.example/p' }], - }) - - const fetchCall = mockCommandsRun.mock.calls.find((c) => c[1]?.envs?.URL) - expect(fetchCall).toBeDefined() - expect(fetchCall?.[0]).toContain('curl') - expect(fetchCall?.[1].user).toBe('root') - }) - - it('passes the requested timeout to shell execution and returns timeout failures', async () => { - mockCommandsRun.mockRejectedValueOnce( - Object.assign(new Error('Execution timed out'), { - stderr: 'Execution timed out', - exitCode: 1, - }) - ) - - const result = await executeShellInE2B({ - code: 'sleep 60', - envs: {}, - timeoutMs: 7000, - }) - - expect(mockCommandsRun).toHaveBeenCalledWith( - 'sleep 60', - expect.objectContaining({ timeoutMs: 7000 }) - ) - expect(result.error).toContain('Execution timed out') - expect(mockKill).toHaveBeenCalled() - }) - - it('throws a clear error and kills the sandbox when the fetch fails', async () => { - mockCommandsRun.mockRejectedValueOnce(new Error('curl: (22) 403')) - - await expect( - executeInE2B({ - code: 'x', - language: CodeLanguage.JavaScript, - timeoutMs: 1000, - sandboxFiles: [ - { type: 'url', path: '/home/user/tables/t.csv', url: 'https://s3.example/p' }, - ], - }) - ).rejects.toThrow(/Failed to fetch mounted file into sandbox/) - - expect(mockKill).toHaveBeenCalled() - expect(mockRunCode).not.toHaveBeenCalled() - }) -}) - -describe('e2b result marker extraction', () => { - beforeEach(() => { - vi.clearAllMocks() - mockCreate.mockResolvedValue({ - sandboxId: 'sb_1', - files: { write: mockFilesWrite }, - commands: { run: mockCommandsRun }, - runCode: mockRunCode, - kill: mockKill, - }) - mockCommandsRun.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }) - }) - - it('parses the result marker from a single stdout entry', async () => { - mockRunCode.mockResolvedValue({ - error: null, - text: '', - logs: { stdout: ['before\n', `__SIM_RESULT__=${JSON.stringify({ ok: true })}\n`] }, - results: [], - }) - - const res = await executeInE2B({ - code: 'x', - language: CodeLanguage.Python, - timeoutMs: 1000, - }) - - expect(res.error).toBeUndefined() - expect(res.result).toEqual({ ok: true }) - expect(res.stdout).toBe('before') - }) - - it('reassembles a marker line split across stream chunks (large single-line result)', async () => { - const payload = 'x'.repeat(50_000) - const markerLine = `__SIM_RESULT__=${JSON.stringify(payload)}\n` - // The kernel splits one long line across several stream messages; each - // chunk is NOT newline-terminated except the last. - const chunks = [ - 'log line\n', - markerLine.slice(0, 20_000), - markerLine.slice(20_000, 40_000), - markerLine.slice(40_000), - ] - mockRunCode.mockResolvedValue({ - error: null, - text: '', - logs: { stdout: chunks }, - results: [], - }) - - const res = await executeInE2B({ - code: 'x', - language: CodeLanguage.Python, - timeoutMs: 1000, - }) - - expect(res.error).toBeUndefined() - expect(res.result).toBe(payload) - expect(res.stdout).toBe('log line') - }) - - it('returns an error instead of a truncated fragment when the marker payload is corrupted', async () => { - // A genuinely broken payload (e.g. the tail chunk was lost entirely). - mockRunCode.mockResolvedValue({ - error: null, - text: '', - logs: { stdout: [`__SIM_RESULT__="${'x'.repeat(100)}\n`] }, - results: [], - }) - - const res = await executeInE2B({ - code: 'x', - language: CodeLanguage.Python, - timeoutMs: 1000, - }) - - expect(res.error).toMatch(/corrupted in transport/) - expect(res.result).toBeNull() - }) - - it('parses the shell marker and falls back to the raw string for non-JSON payloads', async () => { - mockCommandsRun.mockResolvedValueOnce({ - stdout: `hello\n__SIM_RESULT__=${JSON.stringify([1, 2])}\n`, - stderr: '', - exitCode: 0, - }) - const ok = await executeShellInE2B({ code: 'x', envs: {}, timeoutMs: 1000 }) - expect(ok.error).toBeUndefined() - expect(ok.result).toEqual([1, 2]) - expect(ok.stdout).toBe('hello') - - // Shell markers are user-authored (`echo "__SIM_RESULT__=$STATUS"`), so a - // plain non-JSON value is a string result, never a transport error. - mockCommandsRun.mockResolvedValueOnce({ - stdout: '__SIM_RESULT__=ok\n', - stderr: '', - exitCode: 0, - }) - const plain = await executeShellInE2B({ code: 'x', envs: {}, timeoutMs: 1000 }) - expect(plain.error).toBeUndefined() - expect(plain.result).toBe('ok') - }) - - it('takes the LAST marker line so user-printed marker lines cannot shadow the real result', async () => { - mockRunCode.mockResolvedValue({ - error: null, - text: '', - logs: { - stdout: [ - '__SIM_RESULT__=user-debug-junk\n', - `__SIM_RESULT__=${JSON.stringify({ real: true })}\n`, - ], - }, - results: [], - }) - - const res = await executeInE2B({ - code: 'x', - language: CodeLanguage.Python, - timeoutMs: 1000, - }) - - expect(res.error).toBeUndefined() - expect(res.result).toEqual({ real: true }) - expect(res.stdout).not.toContain('__SIM_RESULT__') - }) - - it('finds a marker that landed on stderr', async () => { - mockRunCode.mockResolvedValue({ - error: null, - text: '', - logs: { - stdout: ['regular output\n'], - stderr: [`__SIM_RESULT__=${JSON.stringify([1])}\n`], - }, - results: [], - }) - - const res = await executeInE2B({ - code: 'x', - language: CodeLanguage.Python, - timeoutMs: 1000, - }) - - expect(res.error).toBeUndefined() - expect(res.result).toEqual([1]) - expect(res.stdout).not.toContain('__SIM_RESULT__') - }) - - it('keeps separate print lines intact (chunks concatenated verbatim, not newline-joined)', async () => { - mockRunCode.mockResolvedValue({ - error: null, - text: '', - logs: { stdout: ['a\n', 'b\n'], stderr: ['warn\n'] }, - results: [], - }) - - const res = await executeInE2B({ - code: 'x', - language: CodeLanguage.Python, - timeoutMs: 1000, - }) - - expect(res.result).toBeNull() - expect(res.stdout).toBe('a\nb\n\nwarn\n') - }) -}) diff --git a/apps/sim/lib/execution/e2b.ts b/apps/sim/lib/execution/e2b.ts deleted file mode 100644 index 0f9f2e3a553..00000000000 --- a/apps/sim/lib/execution/e2b.ts +++ /dev/null @@ -1,516 +0,0 @@ -import type { Sandbox as E2BSandbox } from '@e2b/code-interpreter' -import { createLogger } from '@sim/logger' -import { getErrorMessage } from '@sim/utils/errors' -import { env } from '@/lib/core/config/env' -import { CodeLanguage } from '@/lib/execution/languages' - -/** - * A sandbox input file. `content` entries are written inline; `url` entries are fetched from inside - * the sandbox (so large mounts never pass their bytes through the web process). - */ -export type SandboxFile = - | { type?: 'content'; path: string; content: string; encoding?: 'base64' } - | { type: 'url'; path: string; url: string } - -export interface E2BExecutionRequest { - code: string - language: CodeLanguage - timeoutMs: number - sandboxFiles?: SandboxFile[] - outputSandboxPath?: string - outputSandboxPaths?: string[] - // Which sandbox template to run in. Defaults to 'code' (mothership-shell). - // Document generation passes 'doc' so it runs in the doc template - // (mothership-docs) that has python-pptx/docx/openpyxl/reportlab installed. - sandboxKind?: 'code' | 'doc' -} - -export interface E2BShellExecutionRequest { - code: string - envs: Record - timeoutMs: number - sandboxFiles?: SandboxFile[] - outputSandboxPath?: string - outputSandboxPaths?: string[] - // Which sandbox template to run in. Defaults to 'shell' (mothership-shell). - // The Node document engines (pptxgenjs/docx + react-icons/sharp) pass 'doc' so - // they run in the doc template (mothership-docs). - sandboxKind?: 'shell' | 'doc' -} - -export interface E2BExecutionResult { - result: unknown - stdout: string - sandboxId?: string - error?: string - exportedFileContent?: string - exportedFiles?: Record - /** Base64-encoded PNG images captured from rich outputs (e.g. matplotlib figures). */ - images?: string[] -} - -const logger = createLogger('E2BExecution') - -/** - * Materializes sandbox input files before user code runs. `content` entries are written inline; - * `url` entries are fetched from inside the sandbox via `curl` — their bytes never pass through the - * web process, so the mount size is bounded by sandbox disk, not web heap. The URL and paths are - * passed as env vars (never interpolated into the shell) so a presigned query string can't break or - * inject. A failed fetch throws so user code never runs against a missing mount. `rootUser` matches - * the shell sandbox's root execution context. - */ -async function writeSandboxInputs( - sandbox: E2BSandbox, - files: SandboxFile[] | undefined, - opts: { sandboxId?: string; rootUser?: boolean } -): Promise { - if (!files?.length) return - const fetchedByUrl: string[] = [] - const writtenInline: string[] = [] - for (const file of files) { - if (file.type === 'url') { - const dir = file.path.slice(0, file.path.lastIndexOf('/')) - try { - await sandbox.commands.run( - 'set -e; [ -n "$DIR" ] && mkdir -p "$DIR"; curl -fsS --retry 3 --retry-connrefused --max-time 300 "$URL" -o "$DST"', - { - envs: { URL: file.url, DST: file.path, DIR: dir }, - ...(opts.rootUser ? { user: 'root' } : {}), - } - ) - fetchedByUrl.push(file.path) - } catch (error) { - throw new Error( - `Failed to fetch mounted file into sandbox at ${file.path}: ${getErrorMessage(error)}` - ) - } - } else if (file.encoding === 'base64') { - const buf = Buffer.from(file.content, 'base64') - await sandbox.files.write( - file.path, - buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) - ) - writtenInline.push(file.path) - } else { - await sandbox.files.write(file.path, file.content) - writtenInline.push(file.path) - } - } - // Split counts so it's visible whether a mount was fetched in-sandbox (by presigned URL, no bytes - // through the web process) or written inline. - logger.info('Materialized sandbox inputs', { - sandboxId: opts.sandboxId, - fetchedByUrlCount: fetchedByUrl.length, - writtenInlineCount: writtenInline.length, - fetchedByUrl, - writtenInline, - }) -} - -async function createE2BSandbox(kind: 'code' | 'shell' | 'doc' | 'pi'): Promise { - const apiKey = env.E2B_API_KEY - if (!apiKey) { - throw new Error('E2B_API_KEY is required when E2B is enabled') - } - - // Document generation uses a dedicated template (python-pptx/docx/openpyxl/ - // reportlab + fonts); shell/code execution use the general shell template. - // Doc fails closed: never run LLM-authored Python in E2B's default template - // (which is not vetted for this) just because the doc template id is unset. - if (kind === 'doc' && !env.MOTHERSHIP_E2B_DOC_TEMPLATE_ID) { - throw new Error('Document compiler not configured (MOTHERSHIP_E2B_DOC_TEMPLATE_ID is unset)') - } - // Pi fails closed for the same reason: the coding agent needs the Pi CLI + git - // baked into a vetted template, never E2B's default image. - if (kind === 'pi' && !env.E2B_PI_TEMPLATE_ID) { - throw new Error('Pi cloud agent not configured (E2B_PI_TEMPLATE_ID is unset)') - } - - const templateName = - kind === 'doc' - ? env.MOTHERSHIP_E2B_DOC_TEMPLATE_ID - : kind === 'pi' - ? env.E2B_PI_TEMPLATE_ID - : env.MOTHERSHIP_E2B_TEMPLATE_ID - logger.info('Creating E2B sandbox', { - kind, - template: templateName || '(default)', - }) - const { Sandbox } = await import('@e2b/code-interpreter') - return templateName ? Sandbox.create(templateName, { apiKey }) : Sandbox.create({ apiKey }) -} - -/** - * Marker prefix for the serialized code result printed to stdout. Emitters - * (the wrapper builders in the function-execute route) interpolate this - * constant so producer and parser cannot drift. - */ -export const SIM_RESULT_PREFIX = '__SIM_RESULT__=' - -/** - * Extracts the `__SIM_RESULT__=` marker line from stdout and parses its JSON - * payload. Takes the LAST marker line: the wrapper prints its marker after all - * user output, so an earlier user-printed line with the same prefix (debug - * output, a grepped log) never shadows the real result. `parseFailed` means - * the last marker's payload was not valid JSON — `rawPayload` carries it so - * callers whose markers are user-authored (shell) can fall back to the plain - * string, while wrapper-backed callers treat it as transport corruption. - */ -function extractSimResult(stdout: string): { - result: unknown - cleanedStdout: string - parseFailed: boolean - rawPayload?: string -} { - const lines = stdout.split('\n') - let markerIndex = -1 - for (let i = lines.length - 1; i >= 0; i--) { - if (lines[i].startsWith(SIM_RESULT_PREFIX)) { - markerIndex = i - break - } - } - if (markerIndex === -1) { - return { result: null, cleanedStdout: stdout, parseFailed: false } - } - const rawPayload = lines[markerIndex].slice(SIM_RESULT_PREFIX.length) - let result: unknown = null - let parseFailed = false - try { - result = JSON.parse(rawPayload) - } catch { - parseFailed = true - } - const filteredLines = lines.filter((l) => !l.startsWith(SIM_RESULT_PREFIX)) - if (filteredLines.length > 0 && filteredLines[filteredLines.length - 1] === '') { - filteredLines.pop() - } - return { result, cleanedStdout: filteredLines.join('\n'), parseFailed, rawPayload } -} - -const SIM_RESULT_CORRUPTED_ERROR = - 'Sandbox result was corrupted in transport (the __SIM_RESULT__ line failed to parse). ' + - "Do not trust or persist this call's output. For large results, write the content to a " + - 'file inside the sandbox and export it via outputs.files[].sandboxPath instead of returning it.' - -function shouldReadSandboxPathAsBase64(outputSandboxPath: string): boolean { - const ext = outputSandboxPath.slice(outputSandboxPath.lastIndexOf('.')).toLowerCase() - const binaryExts = new Set([ - '.png', - '.jpg', - '.jpeg', - '.gif', - '.webp', - '.pdf', - '.zip', - '.mp3', - '.mp4', - '.docx', - '.pptx', - '.xlsx', - ]) - return binaryExts.has(ext) -} - -async function readSandboxOutputFile( - sandbox: E2BSandbox, - outputSandboxPath: string, - options?: { user?: string } -): Promise { - try { - if (shouldReadSandboxPathAsBase64(outputSandboxPath)) { - const b64Result = await sandbox.commands.run(`base64 -w0 "${outputSandboxPath}"`, options) - return b64Result.stdout - } - return await sandbox.files.read(outputSandboxPath) - } catch (error) { - logger.warn('Failed to read requested sandbox output file', { - outputSandboxPath, - error: getErrorMessage(error), - }) - return undefined - } -} - -function requestedOutputSandboxPaths(req: { - outputSandboxPath?: string - outputSandboxPaths?: string[] -}): string[] { - const paths = [...(req.outputSandboxPaths ?? [])] - if (req.outputSandboxPath && !paths.includes(req.outputSandboxPath)) { - paths.push(req.outputSandboxPath) - } - return paths -} - -export async function executeInE2B(req: E2BExecutionRequest): Promise { - const { code, language, timeoutMs } = req - - const sandbox = await createE2BSandbox(req.sandboxKind ?? 'code') - const sandboxId = sandbox.sandboxId - - try { - // Inside the try so a failed mount still kills the sandbox via the finally below. - await writeSandboxInputs(sandbox, req.sandboxFiles, { sandboxId }) - - const execution = await sandbox.runCode(code, { - language: language === CodeLanguage.Python ? 'python' : 'javascript', - timeoutMs, - }) - - if (execution.error) { - const errorMessage = `${execution.error.name}: ${execution.error.value}` - logger.error(`E2B execution error`, { - sandboxId, - error: execution.error, - errorMessage, - }) - - const errorOutput = execution.error.traceback || errorMessage - return { - result: null, - stdout: errorOutput, - error: errorMessage, - sandboxId, - } - } - - // Kernel stream entries are chunks, not lines — each already carries its own - // newlines, and one long line can arrive split across several entries. - // Concatenate each stream verbatim: joining chunks with '\n' injected a - // newline at every chunk boundary, which corrupted large single-line - // __SIM_RESULT__ payloads and silently truncated the persisted result. - // Distinct sources (final-expression text, stdout, stderr) still join with - // '\n' so the marker is found no matter which stream carried it. - const streamStdout = (execution.logs?.stdout ?? []).join('') - const streamStderr = (execution.logs?.stderr ?? []).join('') - const combinedOutput = [execution.text, streamStdout, streamStderr].filter(Boolean).join('\n') - - const extraction = extractSimResult(combinedOutput) - const cleanedStdout = extraction.cleanedStdout - - // The wrapper always emits valid single-line JSON, so a marker that fails - // to parse means the payload was mangled in transport — never persist it. - if (extraction.parseFailed) { - logger.error('E2B result marker failed to parse', { - sandboxId, - stdoutEntryCount: execution.logs?.stdout?.length ?? 0, - stdoutLength: streamStdout.length, - }) - return { - result: null, - stdout: cleanedStdout, - error: SIM_RESULT_CORRUPTED_ERROR, - sandboxId, - } - } - const result = extraction.result - - const images: string[] = [] - if (execution.results?.length) { - for (const r of execution.results) { - if (r.png) { - images.push(r.png) - } else if (r.jpeg) { - images.push(r.jpeg) - } - } - } - - const exportedFiles: Record = {} - for (const outputSandboxPath of requestedOutputSandboxPaths(req)) { - const content = await readSandboxOutputFile(sandbox, outputSandboxPath) - if (content !== undefined) { - exportedFiles[outputSandboxPath] = content - } - } - const exportedFileContent = req.outputSandboxPath - ? exportedFiles[req.outputSandboxPath] - : undefined - - return { - result, - stdout: cleanedStdout, - sandboxId, - exportedFileContent, - exportedFiles: Object.keys(exportedFiles).length ? exportedFiles : undefined, - images: images.length ? images : undefined, - } - } finally { - try { - await sandbox.kill() - } catch {} - } -} - -export async function executeShellInE2B( - req: E2BShellExecutionRequest -): Promise { - const { code, envs, timeoutMs } = req - - const sandbox = await createE2BSandbox(req.sandboxKind ?? 'shell') - const sandboxId = sandbox.sandboxId - - try { - // Inside the try so a failed mount still kills the sandbox via the finally below. - await writeSandboxInputs(sandbox, req.sandboxFiles, { sandboxId, rootUser: true }) - - let result: { stdout: string; stderr: string; exitCode: number } - try { - result = await sandbox.commands.run(code, { - envs: { - ...envs, - PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/bin', - }, - timeoutMs, - user: 'root', - }) - } catch (cmdError: any) { - const stderr = cmdError?.stderr || cmdError?.message || String(cmdError) - const stdout = cmdError?.stdout || '' - const exitCode = cmdError?.exitCode ?? 1 - logger.error('E2B shell command error', { - sandboxId, - exitCode, - error: stderr.slice(0, 500), - }) - return { - result: null, - stdout: [stdout, stderr].filter(Boolean).join('\n'), - error: stderr || `Command failed with exit code ${exitCode}`, - sandboxId, - } - } - - const stdout = [result.stdout, result.stderr].filter(Boolean).join('\n') - - if (result.exitCode !== 0) { - const errorMessage = result.stderr || `Process exited with code ${result.exitCode}` - logger.error('E2B shell execution error', { - sandboxId, - exitCode: result.exitCode, - stderr: result.stderr?.slice(0, 500), - }) - return { - result: null, - stdout, - error: errorMessage, - sandboxId, - } - } - - // Shell scripts have no wrapper: any __SIM_RESULT__ line is user-authored - // (e.g. `echo "__SIM_RESULT__=$STATUS"`), so a non-JSON payload is a plain - // string result, not transport corruption. commands.run also accumulates - // output into one string, so the chunk-split corruption cannot occur here. - const extraction = extractSimResult(stdout) - const parsed = extraction.parseFailed ? extraction.rawPayload : extraction.result - const cleanedStdout = extraction.cleanedStdout - - const exportedFiles: Record = {} - for (const outputSandboxPath of requestedOutputSandboxPaths(req)) { - const content = await readSandboxOutputFile(sandbox, outputSandboxPath, { - user: 'root', - }) - if (content !== undefined) { - exportedFiles[outputSandboxPath] = content - } - } - const exportedFileContent = req.outputSandboxPath - ? exportedFiles[req.outputSandboxPath] - : undefined - - return { - result: parsed, - stdout: cleanedStdout, - sandboxId, - exportedFileContent, - exportedFiles: Object.keys(exportedFiles).length ? exportedFiles : undefined, - } - } finally { - try { - await sandbox.kill() - } catch {} - } -} - -const PI_SANDBOX_PATH = - '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/bin' - -/** Result of one command run inside a Pi sandbox. */ -export interface PiSandboxCommandResult { - stdout: string - stderr: string - exitCode: number -} - -/** Runs commands and moves files inside a live Pi sandbox. */ -export interface PiSandboxRunner { - run( - command: string, - options: { - envs?: Record - timeoutMs: number - onStdout?: (chunk: string) => void - onStderr?: (chunk: string) => void - } - ): Promise - readFile(path: string): Promise - /** - * Writes a file via the sandbox filesystem API. Bytes go through the E2B SDK, - * never a shell, so untrusted content (the assembled prompt, a commit message) - * is delivered without any shell parsing — callers reference it by a fixed path. - */ - writeFile(path: string, content: string): Promise -} - -/** - * Creates a Pi sandbox, keeps it alive for the duration of `fn` (so the cloned - * repo persists across the clone -> agent -> push commands), streams command - * output, and always kills the sandbox afterward. Per-command envs are isolated, - * so secrets handed to one command never leak into the next. - */ -export async function withPiSandbox(fn: (runner: PiSandboxRunner) => Promise): Promise { - const sandbox = await createE2BSandbox('pi') - const sandboxId = sandbox.sandboxId - logger.info('Started Pi sandbox', { sandboxId }) - - const runner: PiSandboxRunner = { - run: async (command, options) => { - try { - const result = await sandbox.commands.run(command, { - envs: { ...(options.envs ?? {}), PATH: PI_SANDBOX_PATH }, - timeoutMs: options.timeoutMs, - user: 'root', - onStdout: options.onStdout, - onStderr: options.onStderr, - }) - return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode } - } catch (error) { - const failure = error as { - stdout?: string - stderr?: string - message?: string - exitCode?: number - } - return { - stdout: failure.stdout ?? '', - stderr: failure.stderr ?? failure.message ?? getErrorMessage(error), - exitCode: failure.exitCode ?? 1, - } - } - }, - readFile: (path) => sandbox.files.read(path), - writeFile: async (path, content) => { - await sandbox.files.write(path, content) - }, - } - - try { - return await fn(runner) - } finally { - try { - await sandbox.kill() - } catch {} - } -} diff --git a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts new file mode 100644 index 00000000000..87d3c45d19a --- /dev/null +++ b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts @@ -0,0 +1,342 @@ +/** + * @vitest-environment node + * + * Provider conformance: the same input must produce the same + * `SandboxExecutionResult` on E2B and on Daytona. A divergence here is exactly + * what would surface as a broken failover mid-incident, so every scenario runs + * twice — once per provider — from a single table. + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { CodeLanguage } from '@/lib/execution/languages' + +const { + mockIsFeatureEnabled, + mockE2BCreate, + mockE2BRunCode, + mockE2BCommandsRun, + mockE2BFilesRead, + mockE2BFilesWrite, + mockE2BKill, + mockDaytonaCreate, + mockInterpreterRunCode, + mockProcessCodeRun, + mockExecuteCommand, + mockUploadFile, + mockDownloadFile, + mockDelete, +} = vi.hoisted(() => ({ + mockIsFeatureEnabled: vi.fn(), + mockE2BCreate: vi.fn(), + mockE2BRunCode: vi.fn(), + mockE2BCommandsRun: vi.fn(), + mockE2BFilesRead: vi.fn(), + mockE2BFilesWrite: vi.fn(), + mockE2BKill: vi.fn(), + mockDaytonaCreate: vi.fn(), + mockInterpreterRunCode: vi.fn(), + mockProcessCodeRun: vi.fn(), + mockExecuteCommand: vi.fn(), + mockUploadFile: vi.fn(), + mockDownloadFile: vi.fn(), + mockDelete: vi.fn(), +})) + +vi.mock('@e2b/code-interpreter', () => ({ Sandbox: { create: mockE2BCreate } })) +vi.mock('@daytonaio/sdk', () => ({ + Daytona: class { + create = mockDaytonaCreate + }, +})) +vi.mock('@/lib/core/config/env', () => ({ + env: { + E2B_API_KEY: 'test-key', + MOTHERSHIP_E2B_TEMPLATE_ID: 'mothership-shell', + MOTHERSHIP_E2B_DOC_TEMPLATE_ID: 'mothership-docs', + E2B_PI_TEMPLATE_ID: 'sim-pi', + DAYTONA_API_KEY: 'test-key', + DAYTONA_SHELL_SNAPSHOT_ID: 'mothership-shell:v1', + DAYTONA_DOC_SNAPSHOT_ID: 'mothership-docs:v1', + DAYTONA_PI_SNAPSHOT_ID: 'sim-pi:v1', + }, +})) +vi.mock('@/lib/core/config/feature-flags', () => ({ isFeatureEnabled: mockIsFeatureEnabled })) + +import { + executeInSandbox, + executeShellInSandbox, + SIM_RESULT_PREFIX, +} from '@/lib/execution/remote-sandbox' + +type Provider = 'e2b' | 'daytona' +const PROVIDERS: Provider[] = ['e2b', 'daytona'] + +/** Points the shared layer at one provider and stubs that provider's SDK. */ +function useProvider(provider: Provider) { + mockIsFeatureEnabled.mockResolvedValue(provider === 'daytona') +} + +/** Stubs a code execution that prints `stdout` and emits `result` via the marker. */ +function stubCodeRun(provider: Provider, stdout: string) { + if (provider === 'e2b') { + mockE2BRunCode.mockResolvedValue({ + error: null, + text: '', + logs: { stdout: [stdout], stderr: [] }, + results: [], + }) + } else { + mockInterpreterRunCode.mockResolvedValue({ stdout, stderr: '', error: undefined }) + } +} + +beforeEach(() => { + vi.clearAllMocks() + + mockE2BCreate.mockResolvedValue({ + sandboxId: 'sb_1', + runCode: mockE2BRunCode, + commands: { run: mockE2BCommandsRun }, + files: { read: mockE2BFilesRead, write: mockE2BFilesWrite }, + kill: mockE2BKill, + }) + mockE2BCommandsRun.mockResolvedValue({ stdout: '', stderr: '', exitCode: 0 }) + + mockDaytonaCreate.mockResolvedValue({ + id: 'sb_1', + codeInterpreter: { runCode: mockInterpreterRunCode }, + process: { codeRun: mockProcessCodeRun, executeCommand: mockExecuteCommand }, + fs: { uploadFile: mockUploadFile, downloadFile: mockDownloadFile }, + delete: mockDelete, + }) + mockExecuteCommand.mockResolvedValue({ result: '', exitCode: 0 }) +}) + +describe.each(PROVIDERS)('sandbox conformance [%s]', (provider) => { + beforeEach(() => useProvider(provider)) + + it('parses the __SIM_RESULT__ marker and strips it from stdout', async () => { + stubCodeRun(provider, `hello\n${SIM_RESULT_PREFIX}{"ok":true}`) + + const res = await executeInSandbox({ + code: 'x', + language: CodeLanguage.Python, + timeoutMs: 1000, + }) + + expect(res.result).toEqual({ ok: true }) + expect(res.stdout).toBe('hello') + expect(res.error).toBeUndefined() + }) + + it('takes the LAST marker so user output cannot shadow the real result', async () => { + stubCodeRun( + provider, + `${SIM_RESULT_PREFIX}{"decoy":true}\nnoise\n${SIM_RESULT_PREFIX}{"real":true}` + ) + + const res = await executeInSandbox({ + code: 'x', + language: CodeLanguage.Python, + timeoutMs: 1000, + }) + + expect(res.result).toEqual({ real: true }) + }) + + it('refuses to return a corrupted marker payload', async () => { + stubCodeRun(provider, `${SIM_RESULT_PREFIX}{"truncated":`) + + const res = await executeInSandbox({ + code: 'x', + language: CodeLanguage.Python, + timeoutMs: 1000, + }) + + expect(res.result).toBeNull() + expect(res.error).toContain('corrupted in transport') + }) + + it('survives a large single-line payload without chunk corruption', async () => { + const blob = 'x'.repeat(200_000) + stubCodeRun(provider, `${SIM_RESULT_PREFIX}${JSON.stringify({ blob })}`) + + const res = await executeInSandbox({ + code: 'x', + language: CodeLanguage.Python, + timeoutMs: 1000, + }) + + expect((res.result as { blob: string }).blob).toHaveLength(200_000) + }) + + it('normalizes execution errors to the same shape', async () => { + if (provider === 'e2b') { + mockE2BRunCode.mockResolvedValue({ + error: { name: 'ValueError', value: 'boom', traceback: 'Traceback...\nValueError: boom' }, + text: '', + logs: { stdout: [], stderr: [] }, + }) + } else { + mockInterpreterRunCode.mockResolvedValue({ + stdout: '', + stderr: '', + error: { name: 'ValueError', value: 'boom', traceback: 'Traceback...\nValueError: boom' }, + }) + } + + const res = await executeInSandbox({ + code: 'raise ValueError("boom")', + language: CodeLanguage.Python, + timeoutMs: 1000, + }) + + expect(res.error).toBe('ValueError: boom') + expect(res.stdout).toContain('ValueError: boom') + expect(res.result).toBeNull() + }) + + it('fetches url-mounted files inside the sandbox and never inlines the url', async () => { + stubCodeRun(provider, `${SIM_RESULT_PREFIX}null`) + + await executeInSandbox({ + code: 'x', + language: CodeLanguage.Python, + timeoutMs: 1000, + sandboxFiles: [ + { type: 'url', path: '/mnt/data/in.csv', url: 'https://example/presigned?x=1' }, + ], + }) + + const [command, options] = + provider === 'e2b' ? mockE2BCommandsRun.mock.calls[0] : mockExecuteCommand.mock.calls[0] + expect(command).toContain('curl') + // The presigned URL must travel as an env var, never interpolated into the shell. + expect(command).not.toContain('presigned') + const envs = provider === 'e2b' ? options.envs : mockExecuteCommand.mock.calls[0][2] + expect(envs).toMatchObject({ URL: 'https://example/presigned?x=1', DST: '/mnt/data/in.csv' }) + }) + + it('throws rather than running user code against a missing mount', async () => { + if (provider === 'e2b') { + mockE2BCommandsRun.mockRejectedValue(new Error('curl: (22) 404')) + } else { + mockExecuteCommand.mockResolvedValue({ result: 'curl: (22) 404', exitCode: 22 }) + } + + await expect( + executeInSandbox({ + code: 'x', + language: CodeLanguage.Python, + timeoutMs: 1000, + sandboxFiles: [{ type: 'url', path: '/mnt/data/in.csv', url: 'https://example/f' }], + }) + ).rejects.toThrow(/Failed to fetch mounted file/) + }) + + it('treats a non-JSON shell marker as a plain string, not corruption', async () => { + const stdout = `${SIM_RESULT_PREFIX}PLAIN_STATUS` + if (provider === 'e2b') { + mockE2BCommandsRun.mockResolvedValue({ stdout, stderr: '', exitCode: 0 }) + } else { + mockExecuteCommand.mockResolvedValue({ result: stdout, exitCode: 0 }) + } + + const res = await executeShellInSandbox({ code: 'echo hi', envs: {}, timeoutMs: 1000 }) + + expect(res.result).toBe('PLAIN_STATUS') + expect(res.error).toBeUndefined() + }) + + it('surfaces a non-zero shell exit as an error', async () => { + if (provider === 'e2b') { + mockE2BCommandsRun.mockResolvedValue({ stdout: '', stderr: 'bad', exitCode: 3 }) + } else { + mockExecuteCommand.mockResolvedValue({ result: 'bad', exitCode: 3 }) + } + + const res = await executeShellInSandbox({ code: 'false', envs: {}, timeoutMs: 1000 }) + + expect(res.result).toBeNull() + expect(res.error).toBeTruthy() + }) + + it('exports binary output files as base64', async () => { + stubCodeRun(provider, `${SIM_RESULT_PREFIX}null`) + if (provider === 'e2b') { + mockE2BCommandsRun.mockResolvedValue({ stdout: 'QkFTRTY0', stderr: '', exitCode: 0 }) + } else { + mockExecuteCommand.mockResolvedValue({ result: 'QkFTRTY0', exitCode: 0 }) + } + + const res = await executeInSandbox({ + code: 'x', + language: CodeLanguage.Python, + timeoutMs: 1000, + outputSandboxPath: '/out/report.xlsx', + }) + + expect(res.exportedFileContent).toBe('QkFTRTY0') + expect(res.exportedFiles).toEqual({ '/out/report.xlsx': 'QkFTRTY0' }) + }) + + it('always kills the sandbox, even when execution throws', async () => { + if (provider === 'e2b') { + mockE2BRunCode.mockRejectedValue(new Error('kaboom')) + } else { + mockInterpreterRunCode.mockRejectedValue(new Error('kaboom')) + } + + await expect( + executeInSandbox({ code: 'x', language: CodeLanguage.Python, timeoutMs: 1000 }) + ).rejects.toThrow('kaboom') + + expect(provider === 'e2b' ? mockE2BKill : mockDelete).toHaveBeenCalledTimes(1) + }) +}) + +describe('provider selection', () => { + it('routes to E2B when the flag is off and Daytona when it is on', async () => { + stubCodeRun('e2b', `${SIM_RESULT_PREFIX}null`) + stubCodeRun('daytona', `${SIM_RESULT_PREFIX}null`) + + useProvider('e2b') + await executeInSandbox({ code: 'x', language: CodeLanguage.Python, timeoutMs: 1000 }) + expect(mockE2BCreate).toHaveBeenCalledTimes(1) + expect(mockDaytonaCreate).not.toHaveBeenCalled() + + useProvider('daytona') + await executeInSandbox({ code: 'x', language: CodeLanguage.Python, timeoutMs: 1000 }) + expect(mockDaytonaCreate).toHaveBeenCalledTimes(1) + }) + + it('binds language at create time so JS never runs through the Python toolbox', async () => { + useProvider('daytona') + mockProcessCodeRun.mockResolvedValue({ result: `${SIM_RESULT_PREFIX}null`, exitCode: 0 }) + + await executeInSandbox({ code: 'x', language: CodeLanguage.JavaScript, timeoutMs: 1000 }) + + expect(mockDaytonaCreate).toHaveBeenCalledWith( + expect.objectContaining({ language: 'javascript' }) + ) + // JS must go through process.codeRun — CodeInterpreter is Python-only. + expect(mockProcessCodeRun).toHaveBeenCalledTimes(1) + expect(mockInterpreterRunCode).not.toHaveBeenCalled() + }) + + it('fails closed when a Daytona snapshot id is unset', async () => { + useProvider('daytona') + const { env } = await import('@/lib/core/config/env') + const original = env.DAYTONA_DOC_SNAPSHOT_ID + ;(env as { DAYTONA_DOC_SNAPSHOT_ID?: string }).DAYTONA_DOC_SNAPSHOT_ID = undefined + + await expect( + executeInSandbox({ + code: 'x', + language: CodeLanguage.Python, + timeoutMs: 1000, + sandboxKind: 'doc', + }) + ).rejects.toThrow(/DAYTONA_DOC_SNAPSHOT_ID is unset/) + ;(env as { DAYTONA_DOC_SNAPSHOT_ID?: string }).DAYTONA_DOC_SNAPSHOT_ID = original + }) +}) diff --git a/apps/sim/lib/execution/remote-sandbox/daytona.ts b/apps/sim/lib/execution/remote-sandbox/daytona.ts new file mode 100644 index 00000000000..ae2e6b87f87 --- /dev/null +++ b/apps/sim/lib/execution/remote-sandbox/daytona.ts @@ -0,0 +1,210 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { generateShortId } from '@sim/utils/id' +import { env } from '@/lib/core/config/env' +import { CodeLanguage } from '@/lib/execution/languages' +import type { + CreateSandboxOptions, + RunCommandOptions, + SandboxCodeResult, + SandboxCommandResult, + SandboxHandle, + SandboxKind, + SandboxProvider, +} from '@/lib/execution/remote-sandbox/types' + +const logger = createLogger('DaytonaSandboxProvider') + +/** Daytona expresses every timeout in seconds; the rest of Sim works in milliseconds. */ +function toSeconds(timeoutMs: number): number { + return Math.max(1, Math.ceil(timeoutMs / 1000)) +} + +function snapshotFor(kind: SandboxKind): string { + // Mirrors the E2B provider's fail-closed behaviour: never let LLM-authored code + // run in a provider default image just because a snapshot id is unset. + const snapshot = + kind === 'doc' + ? env.DAYTONA_DOC_SNAPSHOT_ID + : kind === 'pi' + ? env.DAYTONA_PI_SNAPSHOT_ID + : env.DAYTONA_SHELL_SNAPSHOT_ID + if (!snapshot) { + const varName = + kind === 'doc' + ? 'DAYTONA_DOC_SNAPSHOT_ID' + : kind === 'pi' + ? 'DAYTONA_PI_SNAPSHOT_ID' + : 'DAYTONA_SHELL_SNAPSHOT_ID' + throw new Error(`Daytona sandbox not configured (${varName} is unset)`) + } + return snapshot +} + +/** Daytona binds `codeRun`'s language to the sandbox, not the call. */ +function toDaytonaLanguage(language: CodeLanguage): string { + return language === CodeLanguage.Python ? 'python' : 'javascript' +} + +class DaytonaSandboxHandle implements SandboxHandle { + constructor( + private readonly sandbox: any, + private readonly language: CodeLanguage + ) {} + + get sandboxId(): string { + return this.sandbox.id + } + + async runCode(code: string, options: { timeoutMs: number }): Promise { + // Python goes through CodeInterpreter because it reports a structured + // `{ name, value, traceback }` error — the same shape E2B returns, which the + // route's line-offset error formatting depends on. CodeInterpreter is + // Python-only, so JS falls back to `process.codeRun`, whose language comes + // from the label bound at sandbox creation. + if (this.language === CodeLanguage.Python) { + const result = await this.sandbox.codeInterpreter.runCode(code, { + timeout: toSeconds(options.timeoutMs), + }) + return { + text: '', + stdout: result.stdout ?? '', + stderr: result.stderr ?? '', + error: result.error + ? { + name: result.error.name, + value: result.error.value ?? result.error.message ?? '', + traceback: result.error.traceback, + } + : undefined, + } + } + + const result = await this.sandbox.process.codeRun(code, undefined, toSeconds(options.timeoutMs)) + const output: string = result.result ?? '' + if (result.exitCode !== 0) { + // `process.codeRun` has no structured error channel — the interpreter's + // stderr lands in `result`. Surface it as the traceback so the shape stays + // identical to the Python and E2B paths. + return { + text: '', + stdout: '', + stderr: output, + error: { name: 'Error', value: lastNonEmptyLine(output), traceback: output }, + } + } + return { text: '', stdout: output, stderr: '' } + } + + async runCommand(command: string, options: RunCommandOptions): Promise { + // `rootUser` needs no handling: Daytona already executes commands as uid 0. + if (options.onStdout || options.onStderr) { + return this.runStreamingCommand(command, options) + } + try { + const result = await this.sandbox.process.executeCommand( + command, + undefined, + options.envs, + toSeconds(options.timeoutMs) + ) + // Daytona merges the two streams into `result`; splitting them back out is + // not possible, so stdout carries everything and callers that join the two + // (every caller today) are unaffected. + return { stdout: result.result ?? '', stderr: '', exitCode: result.exitCode ?? 0 } + } catch (error) { + return { stdout: '', stderr: getErrorMessage(error), exitCode: 1 } + } + } + + /** + * Streaming path (Pi). `SessionExecuteRequest` carries no `env` field, so the + * environment is delivered as a file written through the filesystem API and + * sourced by the command. Secrets therefore never appear in a command line or + * the sandbox process list, and the file is removed before the command runs — + * preserving the per-command isolation the E2B path provides natively. + */ + private async runStreamingCommand( + command: string, + options: RunCommandOptions + ): Promise { + const sessionId = `sim-${generateShortId(12)}` + await this.sandbox.process.createSession(sessionId) + try { + let script = command + if (options.envs && Object.keys(options.envs).length > 0) { + const envPath = `/tmp/.sim-env-${generateShortId(12)}` + const envFile = Object.entries(options.envs) + .map(([key, value]) => `${key}=${shellQuote(value)}`) + .join('\n') + await this.writeFile(envPath, envFile) + script = `set -a; . ${envPath}; set +a; rm -f ${envPath}; ${command}` + } + + const started = await this.sandbox.process.executeSessionCommand( + sessionId, + { command: script, runAsync: true }, + toSeconds(options.timeoutMs) + ) + const commandId: string = started.cmdId ?? started.commandId + await this.sandbox.process.getSessionCommandLogs( + sessionId, + commandId, + (chunk: string) => options.onStdout?.(chunk), + (chunk: string) => options.onStderr?.(chunk) + ) + const finished = await this.sandbox.process.getSessionCommand(sessionId, commandId) + return { stdout: '', stderr: '', exitCode: finished.exitCode ?? 0 } + } catch (error) { + return { stdout: '', stderr: getErrorMessage(error), exitCode: 1 } + } finally { + try { + await this.sandbox.process.deleteSession(sessionId) + } catch {} + } + } + + async readFile(path: string): Promise { + const buffer = await this.sandbox.fs.downloadFile(path) + return buffer.toString('utf-8') + } + + async writeFile(path: string, content: string | ArrayBuffer): Promise { + const buffer = + typeof content === 'string' ? Buffer.from(content, 'utf-8') : Buffer.from(content) + await this.sandbox.fs.uploadFile(buffer, path) + } + + async kill(): Promise { + await this.sandbox.delete() + } +} + +/** Quotes a value for a POSIX `KEY=value` env file. */ +function shellQuote(value: string): string { + return `'${value.replace(/'/g, `'\\''`)}'` +} + +function lastNonEmptyLine(output: string): string { + const lines = output.split('\n').filter((line) => line.trim().length > 0) + return lines.length > 0 ? lines[lines.length - 1] : 'Execution failed' +} + +export const daytonaProvider: SandboxProvider = { + id: 'daytona', + async create(kind: SandboxKind, options?: CreateSandboxOptions): Promise { + const apiKey = env.DAYTONA_API_KEY + if (!apiKey) { + throw new Error('DAYTONA_API_KEY is required when the Daytona sandbox provider is selected') + } + const snapshot = snapshotFor(kind) + const language = options?.language ?? CodeLanguage.Python + logger.info('Creating Daytona sandbox', { kind, snapshot }) + + const { Daytona } = await import('@daytonaio/sdk') + const daytona = new Daytona({ apiKey }) + const sandbox = await daytona.create({ snapshot, language: toDaytonaLanguage(language) } as any) + + return new DaytonaSandboxHandle(sandbox, language) + }, +} diff --git a/apps/sim/lib/execution/remote-sandbox/e2b.ts b/apps/sim/lib/execution/remote-sandbox/e2b.ts new file mode 100644 index 00000000000..f21a06e019d --- /dev/null +++ b/apps/sim/lib/execution/remote-sandbox/e2b.ts @@ -0,0 +1,131 @@ +import type { Sandbox as E2BSandbox } from '@e2b/code-interpreter' +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { env } from '@/lib/core/config/env' +import { CodeLanguage } from '@/lib/execution/languages' +import type { + CreateSandboxOptions, + RunCommandOptions, + SandboxCodeResult, + SandboxCommandResult, + SandboxHandle, + SandboxKind, + SandboxProvider, +} from '@/lib/execution/remote-sandbox/types' + +const logger = createLogger('E2BSandboxProvider') + +function templateFor(kind: SandboxKind): string | undefined { + // Document generation uses a dedicated template (python-pptx/docx/openpyxl/ + // reportlab + fonts); shell/code execution use the general shell template. + // Doc fails closed: never run LLM-authored Python in E2B's default template + // (which is not vetted for this) just because the doc template id is unset. + if (kind === 'doc') { + if (!env.MOTHERSHIP_E2B_DOC_TEMPLATE_ID) { + throw new Error('Document compiler not configured (MOTHERSHIP_E2B_DOC_TEMPLATE_ID is unset)') + } + return env.MOTHERSHIP_E2B_DOC_TEMPLATE_ID + } + // Pi fails closed for the same reason: the coding agent needs the Pi CLI + git + // baked into a vetted template, never E2B's default image. + if (kind === 'pi') { + if (!env.E2B_PI_TEMPLATE_ID) { + throw new Error('Pi cloud agent not configured (E2B_PI_TEMPLATE_ID is unset)') + } + return env.E2B_PI_TEMPLATE_ID + } + return env.MOTHERSHIP_E2B_TEMPLATE_ID +} + +class E2BSandboxHandle implements SandboxHandle { + constructor( + private readonly sandbox: E2BSandbox, + private readonly language: CodeLanguage + ) {} + + get sandboxId(): string { + return this.sandbox.sandboxId + } + + async runCode(code: string, options: { timeoutMs: number }): Promise { + const execution = await this.sandbox.runCode(code, { + language: this.language === CodeLanguage.Python ? 'python' : 'javascript', + timeoutMs: options.timeoutMs, + }) + + // Kernel stream entries are chunks, not lines — each already carries its own + // newlines, and one long line can arrive split across several entries. + // Concatenate each stream verbatim: joining chunks with '\n' injected a + // newline at every chunk boundary, which corrupted large single-line + // __SIM_RESULT__ payloads and silently truncated the persisted result. + return { + text: execution.text ?? '', + stdout: (execution.logs?.stdout ?? []).join(''), + stderr: (execution.logs?.stderr ?? []).join(''), + error: execution.error + ? { + name: execution.error.name, + value: execution.error.value, + traceback: execution.error.traceback, + } + : undefined, + } + } + + async runCommand(command: string, options: RunCommandOptions): Promise { + try { + const result = await this.sandbox.commands.run(command, { + ...(options.envs ? { envs: options.envs } : {}), + timeoutMs: options.timeoutMs, + ...(options.rootUser ? { user: 'root' as const } : {}), + ...(options.onStdout ? { onStdout: options.onStdout } : {}), + ...(options.onStderr ? { onStderr: options.onStderr } : {}), + }) + return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode } + } catch (error) { + // The SDK throws on non-zero exit; callers want the streams, not a throw. + const failure = error as { + stdout?: string + stderr?: string + message?: string + exitCode?: number + } + return { + stdout: failure.stdout ?? '', + stderr: failure.stderr ?? failure.message ?? getErrorMessage(error), + exitCode: failure.exitCode ?? 1, + } + } + } + + readFile(path: string): Promise { + return this.sandbox.files.read(path) + } + + async writeFile(path: string, content: string | ArrayBuffer): Promise { + await this.sandbox.files.write(path, content as string) + } + + async kill(): Promise { + await this.sandbox.kill() + } +} + +export const e2bProvider: SandboxProvider = { + id: 'e2b', + async create(kind: SandboxKind, options?: CreateSandboxOptions): Promise { + const apiKey = env.E2B_API_KEY + if (!apiKey) { + throw new Error('E2B_API_KEY is required when E2B is enabled') + } + const templateName = templateFor(kind) + logger.info('Creating E2B sandbox', { kind, template: templateName || '(default)' }) + + const { Sandbox } = await import('@e2b/code-interpreter') + const sandbox = templateName + ? await Sandbox.create(templateName, { apiKey }) + : await Sandbox.create({ apiKey }) + + return new E2BSandboxHandle(sandbox, options?.language ?? CodeLanguage.Python) + }, +} diff --git a/apps/sim/lib/execution/remote-sandbox/index.ts b/apps/sim/lib/execution/remote-sandbox/index.ts new file mode 100644 index 00000000000..921a6586e3e --- /dev/null +++ b/apps/sim/lib/execution/remote-sandbox/index.ts @@ -0,0 +1,423 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +import { isFeatureEnabled } from '@/lib/core/config/feature-flags' +import type { CodeLanguage } from '@/lib/execution/languages' +import { daytonaProvider } from '@/lib/execution/remote-sandbox/daytona' +import { e2bProvider } from '@/lib/execution/remote-sandbox/e2b' +import type { + SandboxCommandResult, + SandboxExecutionRequest, + SandboxExecutionResult, + SandboxFile, + SandboxHandle, + SandboxKind, + SandboxProvider, + SandboxShellExecutionRequest, +} from '@/lib/execution/remote-sandbox/types' + +export type { + SandboxExecutionRequest, + SandboxExecutionResult, + SandboxFile, + SandboxShellExecutionRequest, +} from '@/lib/execution/remote-sandbox/types' + +const logger = createLogger('RemoteSandbox') + +/** + * Resolves which provider serves this execution. + * + * Selection is deliberately resolved ONCE, before the sandbox is created, and is + * never revisited mid-execution: user code has side effects (HTTP calls, S3 + * writes, DB mutations), so retrying a partially-executed run on the other + * provider could duplicate them. This is a manual failover — flip the + * `sandbox-provider-daytona` flag and new executions move over. + */ +async function resolveProvider(): Promise { + const useDaytona = await isFeatureEnabled('sandbox-provider-daytona') + return useDaytona ? daytonaProvider : e2bProvider +} + +async function createSandbox( + kind: SandboxKind, + options?: { language?: CodeLanguage } +): Promise { + const provider = await resolveProvider() + const sandbox = await provider.create(kind, options) + logger.info('Created sandbox', { provider: provider.id, kind, sandboxId: sandbox.sandboxId }) + return sandbox +} + +/** + * Materializes sandbox input files before user code runs. `content` entries are written inline; + * `url` entries are fetched from inside the sandbox via `curl` — their bytes never pass through the + * web process, so the mount size is bounded by sandbox disk, not web heap. The URL and paths are + * passed as env vars (never interpolated into the shell) so a presigned query string can't break or + * inject. A failed fetch throws so user code never runs against a missing mount. + */ +async function writeSandboxInputs( + sandbox: SandboxHandle, + files: SandboxFile[] | undefined, + opts: { rootUser?: boolean } +): Promise { + if (!files?.length) return + const fetchedByUrl: string[] = [] + const writtenInline: string[] = [] + for (const file of files) { + if (file.type === 'url') { + const dir = file.path.slice(0, file.path.lastIndexOf('/')) + let result: SandboxCommandResult + try { + result = await sandbox.runCommand( + 'set -e; [ -n "$DIR" ] && mkdir -p "$DIR"; curl -fsS --retry 3 --retry-connrefused --max-time 300 "$URL" -o "$DST"', + { + envs: { URL: file.url, DST: file.path, DIR: dir }, + timeoutMs: 300_000, + rootUser: opts.rootUser, + } + ) + } catch (error) { + throw new Error( + `Failed to fetch mounted file into sandbox at ${file.path}: ${getErrorMessage(error)}` + ) + } + // Providers differ on whether a non-zero exit throws, so the exit code is + // checked explicitly — a silently-missing mount is exactly what this guard + // exists to prevent. + if (result.exitCode !== 0) { + throw new Error( + `Failed to fetch mounted file into sandbox at ${file.path}: ${result.stderr || `curl exited ${result.exitCode}`}` + ) + } + fetchedByUrl.push(file.path) + } else if (file.encoding === 'base64') { + const buf = Buffer.from(file.content, 'base64') + await sandbox.writeFile( + file.path, + buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) + ) + writtenInline.push(file.path) + } else { + await sandbox.writeFile(file.path, file.content) + writtenInline.push(file.path) + } + } + // Split counts so it's visible whether a mount was fetched in-sandbox (by presigned URL, no bytes + // through the web process) or written inline. + logger.info('Materialized sandbox inputs', { + sandboxId: sandbox.sandboxId, + fetchedByUrlCount: fetchedByUrl.length, + writtenInlineCount: writtenInline.length, + fetchedByUrl, + writtenInline, + }) +} + +/** + * Marker prefix for the serialized code result printed to stdout. Emitters + * (the wrapper builders in the function-execute route) interpolate this + * constant so producer and parser cannot drift. + */ +export const SIM_RESULT_PREFIX = '__SIM_RESULT__=' + +/** + * Extracts the `__SIM_RESULT__=` marker line from stdout and parses its JSON + * payload. Takes the LAST marker line: the wrapper prints its marker after all + * user output, so an earlier user-printed line with the same prefix (debug + * output, a grepped log) never shadows the real result. `parseFailed` means + * the last marker's payload was not valid JSON — `rawPayload` carries it so + * callers whose markers are user-authored (shell) can fall back to the plain + * string, while wrapper-backed callers treat it as transport corruption. + */ +function extractSimResult(stdout: string): { + result: unknown + cleanedStdout: string + parseFailed: boolean + rawPayload?: string +} { + const lines = stdout.split('\n') + let markerIndex = -1 + for (let i = lines.length - 1; i >= 0; i--) { + if (lines[i].startsWith(SIM_RESULT_PREFIX)) { + markerIndex = i + break + } + } + if (markerIndex === -1) { + return { result: null, cleanedStdout: stdout, parseFailed: false } + } + const rawPayload = lines[markerIndex].slice(SIM_RESULT_PREFIX.length) + let result: unknown = null + let parseFailed = false + try { + result = JSON.parse(rawPayload) + } catch { + parseFailed = true + } + const filteredLines = lines.filter((l) => !l.startsWith(SIM_RESULT_PREFIX)) + if (filteredLines.length > 0 && filteredLines[filteredLines.length - 1] === '') { + filteredLines.pop() + } + return { result, cleanedStdout: filteredLines.join('\n'), parseFailed, rawPayload } +} + +const SIM_RESULT_CORRUPTED_ERROR = + 'Sandbox result was corrupted in transport (the __SIM_RESULT__ line failed to parse). ' + + "Do not trust or persist this call's output. For large results, write the content to a " + + 'file inside the sandbox and export it via outputs.files[].sandboxPath instead of returning it.' + +function shouldReadSandboxPathAsBase64(outputSandboxPath: string): boolean { + const ext = outputSandboxPath.slice(outputSandboxPath.lastIndexOf('.')).toLowerCase() + const binaryExts = new Set([ + '.png', + '.jpg', + '.jpeg', + '.gif', + '.webp', + '.pdf', + '.zip', + '.mp3', + '.mp4', + '.docx', + '.pptx', + '.xlsx', + ]) + return binaryExts.has(ext) +} + +async function readSandboxOutputFile( + sandbox: SandboxHandle, + outputSandboxPath: string, + options?: { rootUser?: boolean } +): Promise { + try { + if (shouldReadSandboxPathAsBase64(outputSandboxPath)) { + const b64Result = await sandbox.runCommand(`base64 -w0 "${outputSandboxPath}"`, { + timeoutMs: 120_000, + rootUser: options?.rootUser, + }) + if (b64Result.exitCode !== 0) throw new Error(b64Result.stderr || 'base64 failed') + return b64Result.stdout + } + return await sandbox.readFile(outputSandboxPath) + } catch (error) { + logger.warn('Failed to read requested sandbox output file', { + outputSandboxPath, + error: getErrorMessage(error), + }) + return undefined + } +} + +function requestedOutputSandboxPaths(req: { + outputSandboxPath?: string + outputSandboxPaths?: string[] +}): string[] { + const paths = [...(req.outputSandboxPaths ?? [])] + if (req.outputSandboxPath && !paths.includes(req.outputSandboxPath)) { + paths.push(req.outputSandboxPath) + } + return paths +} + +async function collectExportedFiles( + sandbox: SandboxHandle, + req: { outputSandboxPath?: string; outputSandboxPaths?: string[] }, + options?: { rootUser?: boolean } +): Promise<{ exportedFiles?: Record; exportedFileContent?: string }> { + const exportedFiles: Record = {} + for (const outputSandboxPath of requestedOutputSandboxPaths(req)) { + const content = await readSandboxOutputFile(sandbox, outputSandboxPath, options) + if (content !== undefined) { + exportedFiles[outputSandboxPath] = content + } + } + return { + exportedFileContent: req.outputSandboxPath ? exportedFiles[req.outputSandboxPath] : undefined, + exportedFiles: Object.keys(exportedFiles).length ? exportedFiles : undefined, + } +} + +export async function executeInSandbox( + req: SandboxExecutionRequest +): Promise { + const { code, language, timeoutMs } = req + + const sandbox = await createSandbox(req.sandboxKind ?? 'code', { language }) + const sandboxId = sandbox.sandboxId + + try { + // Inside the try so a failed mount still kills the sandbox via the finally below. + await writeSandboxInputs(sandbox, req.sandboxFiles, {}) + + const execution = await sandbox.runCode(code, { timeoutMs }) + + if (execution.error) { + const errorMessage = `${execution.error.name}: ${execution.error.value}` + logger.error('Sandbox execution error', { sandboxId, error: execution.error, errorMessage }) + return { + result: null, + stdout: execution.error.traceback || errorMessage, + error: errorMessage, + sandboxId, + } + } + + // Distinct sources (final-expression text, stdout, stderr) join with '\n' so + // the marker is found no matter which stream carried it. Each individual + // stream is already concatenated verbatim by the provider, because injecting + // a newline at chunk boundaries corrupted large single-line payloads. + const combinedOutput = [execution.text, execution.stdout, execution.stderr] + .filter(Boolean) + .join('\n') + + const extraction = extractSimResult(combinedOutput) + const cleanedStdout = extraction.cleanedStdout + + // The wrapper always emits valid single-line JSON, so a marker that fails + // to parse means the payload was mangled in transport — never persist it. + if (extraction.parseFailed) { + logger.error('Sandbox result marker failed to parse', { + sandboxId, + stdoutLength: execution.stdout.length, + }) + return { + result: null, + stdout: cleanedStdout, + error: SIM_RESULT_CORRUPTED_ERROR, + sandboxId, + } + } + + const { exportedFiles, exportedFileContent } = await collectExportedFiles(sandbox, req) + + return { + result: extraction.result, + stdout: cleanedStdout, + sandboxId, + exportedFileContent, + exportedFiles, + } + } finally { + try { + await sandbox.kill() + } catch {} + } +} + +export async function executeShellInSandbox( + req: SandboxShellExecutionRequest +): Promise { + const { code, envs, timeoutMs } = req + + const sandbox = await createSandbox(req.sandboxKind ?? 'shell') + const sandboxId = sandbox.sandboxId + + try { + // Inside the try so a failed mount still kills the sandbox via the finally below. + await writeSandboxInputs(sandbox, req.sandboxFiles, { rootUser: true }) + + const result = await sandbox.runCommand(code, { + envs: { + ...envs, + PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.local/bin', + }, + timeoutMs, + rootUser: true, + }) + + const stdout = [result.stdout, result.stderr].filter(Boolean).join('\n') + + if (result.exitCode !== 0) { + const errorMessage = result.stderr || `Process exited with code ${result.exitCode}` + logger.error('Sandbox shell execution error', { + sandboxId, + exitCode: result.exitCode, + stderr: result.stderr?.slice(0, 500), + }) + return { result: null, stdout, error: errorMessage, sandboxId } + } + + // Shell scripts have no wrapper: any __SIM_RESULT__ line is user-authored + // (e.g. `echo "__SIM_RESULT__=$STATUS"`), so a non-JSON payload is a plain + // string result, not transport corruption. + const extraction = extractSimResult(stdout) + const parsed = extraction.parseFailed ? extraction.rawPayload : extraction.result + + const { exportedFiles, exportedFileContent } = await collectExportedFiles(sandbox, req, { + rootUser: true, + }) + + return { + result: parsed, + stdout: extraction.cleanedStdout, + sandboxId, + exportedFileContent, + exportedFiles, + } + } finally { + try { + await sandbox.kill() + } catch {} + } +} + +/** Result of one command run inside a Pi sandbox. */ +export interface PiSandboxCommandResult { + stdout: string + stderr: string + exitCode: number +} + +/** Runs commands and moves files inside a live Pi sandbox. */ +export interface PiSandboxRunner { + run( + command: string, + options: { + envs?: Record + timeoutMs: number + onStdout?: (chunk: string) => void + onStderr?: (chunk: string) => void + } + ): Promise + readFile(path: string): Promise + /** + * Writes a file via the sandbox filesystem API. Bytes go through the provider + * SDK, never a shell, so untrusted content (the assembled prompt, a commit + * message) is delivered without any shell parsing — callers reference it by a + * fixed path. + */ + writeFile(path: string, content: string): Promise +} + +/** + * Creates a Pi sandbox, keeps it alive for the duration of `fn` (so the cloned + * repo persists across the clone -> agent -> push commands), streams command + * output, and always kills the sandbox afterward. Per-command envs are isolated, + * so secrets handed to one command never leak into the next. + */ +export async function withPiSandbox(fn: (runner: PiSandboxRunner) => Promise): Promise { + const sandbox = await createSandbox('pi') + logger.info('Started Pi sandbox', { sandboxId: sandbox.sandboxId }) + + const runner: PiSandboxRunner = { + run: (command, options) => + sandbox.runCommand(command, { + envs: options.envs, + timeoutMs: options.timeoutMs, + rootUser: true, + onStdout: options.onStdout, + onStderr: options.onStderr, + }), + readFile: (path) => sandbox.readFile(path), + writeFile: (path, content) => sandbox.writeFile(path, content), + } + + try { + return await fn(runner) + } finally { + try { + await sandbox.kill() + } catch {} + } +} diff --git a/apps/sim/lib/execution/remote-sandbox/types.ts b/apps/sim/lib/execution/remote-sandbox/types.ts new file mode 100644 index 00000000000..5141f49b27d --- /dev/null +++ b/apps/sim/lib/execution/remote-sandbox/types.ts @@ -0,0 +1,129 @@ +import type { CodeLanguage } from '@/lib/execution/languages' + +/** + * Which vetted image a sandbox runs in. Every kind fails closed when its + * template/snapshot id is unset, so LLM-authored code can never land in a + * provider's unvetted default image. + */ +export type SandboxKind = 'code' | 'shell' | 'doc' | 'pi' + +export type SandboxProviderId = 'e2b' | 'daytona' + +/** + * A sandbox input file. `content` entries are written inline; `url` entries are fetched from inside + * the sandbox (so large mounts never pass their bytes through the web process). + */ +export type SandboxFile = + | { type?: 'content'; path: string; content: string; encoding?: 'base64' } + | { type: 'url'; path: string; url: string } + +export interface SandboxExecutionRequest { + code: string + language: CodeLanguage + timeoutMs: number + sandboxFiles?: SandboxFile[] + outputSandboxPath?: string + outputSandboxPaths?: string[] + /** + * Which sandbox image to run in. Defaults to 'code' (mothership-shell). + * Document generation passes 'doc' so it runs in the doc image + * (mothership-docs) that has python-pptx/docx/openpyxl/reportlab installed. + */ + sandboxKind?: 'code' | 'doc' +} + +export interface SandboxShellExecutionRequest { + code: string + envs: Record + timeoutMs: number + sandboxFiles?: SandboxFile[] + outputSandboxPath?: string + outputSandboxPaths?: string[] + /** + * Which sandbox image to run in. Defaults to 'shell' (mothership-shell). + * The Node document engines (pptxgenjs/docx + react-icons/sharp) pass 'doc' so + * they run in the doc image (mothership-docs). + */ + sandboxKind?: 'shell' | 'doc' +} + +export interface SandboxExecutionResult { + result: unknown + stdout: string + sandboxId?: string + error?: string + exportedFileContent?: string + exportedFiles?: Record +} + +/** Result of one command run inside a sandbox. */ +export interface SandboxCommandResult { + stdout: string + stderr: string + exitCode: number +} + +/** + * Normalized error from a code execution. Both providers report this shape: + * E2B's `Execution.error` and Daytona's `ExecutionResult.error` agree on + * `{ name, value, traceback }`, so `formatSandboxError`'s line-offset handling + * works unchanged across providers. + */ +export interface SandboxCodeError { + name: string + value: string + traceback?: string +} + +/** Result of a code (non-shell) execution. */ +export interface SandboxCodeResult { + /** The final-expression value, when the provider surfaces one separately from stdout. */ + text: string + stdout: string + stderr: string + error?: SandboxCodeError +} + +export interface RunCommandOptions { + envs?: Record + timeoutMs: number + /** Run as root. The shell and Pi paths depend on this; the code path does not. */ + rootUser?: boolean + onStdout?: (chunk: string) => void + onStderr?: (chunk: string) => void +} + +/** + * A live sandbox. Deliberately the smallest surface that satisfies every caller, + * so adding a third provider stays cheap. + */ +export interface SandboxHandle { + readonly sandboxId: string + /** + * Runs code in the language fixed at {@link SandboxProvider.create} time. + * Language is bound at creation rather than per call because Daytona applies it + * as a sandbox label (`code-toolbox-language`) and silently ignores a per-call + * override — passing `javascript` to its `codeRun` executes the source through + * Python instead. We create one sandbox per execution, so binding costs nothing. + */ + runCode(code: string, options: { timeoutMs: number }): Promise + runCommand(command: string, options: RunCommandOptions): Promise + readFile(path: string): Promise + /** + * Writes a file via the sandbox filesystem API. Bytes never pass through a + * shell, so untrusted content (an assembled prompt, a commit message) is + * delivered without any shell parsing. + */ + writeFile(path: string, content: string | ArrayBuffer): Promise + kill(): Promise +} + +export interface CreateSandboxOptions { + /** Bound at creation — see {@link SandboxHandle.runCode}. */ + language?: CodeLanguage +} + +export interface SandboxProvider { + readonly id: SandboxProviderId + create(kind: SandboxKind, options?: CreateSandboxOptions): Promise +} diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index ebcb52775ea..3c458571a62 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -130,6 +130,7 @@ const nextConfig: NextConfig = { 'isolated-vm', '@e2b/code-interpreter', 'e2b', + '@daytonaio/sdk', '@earendil-works/pi-ai', '@earendil-works/pi-coding-agent', ], diff --git a/apps/sim/package.json b/apps/sim/package.json index 1b9c251ff56..056cd32c491 100644 --- a/apps/sim/package.json +++ b/apps/sim/package.json @@ -65,6 +65,7 @@ "@browserbasehq/stagehand": "^3.2.1", "@calcom/embed-react": "1.5.3", "@cerebras/cerebras_cloud_sdk": "^1.23.0", + "@daytonaio/sdk": "0.197.0", "@e2b/code-interpreter": "^2.0.0", "@earendil-works/pi-ai": "0.80.10", "@earendil-works/pi-coding-agent": "0.80.10", diff --git a/apps/sim/scripts/build-pi-daytona-snapshot.ts b/apps/sim/scripts/build-pi-daytona-snapshot.ts new file mode 100644 index 00000000000..3c2c3d68c2e --- /dev/null +++ b/apps/sim/scripts/build-pi-daytona-snapshot.ts @@ -0,0 +1,101 @@ +#!/usr/bin/env bun + +/** + * Builds the Daytona snapshot used by Create PR and Review Code — the failover + * counterpart of `build-pi-e2b-template.ts`. + * + * Both renderers consume `pi-sandbox-packages.ts`, so the two providers cannot + * drift apart. + * + * Unlike the E2B template, which layers onto `code-interpreter-v1` (Debian + * trixie + Python 3.13 + Node 20), Daytona has no equivalent base, so this image + * reconstructs that foundation: Python for the review tools' helper script and + * Node 22 for the Pi CLI. + * + * Usage: + * DAYTONA_API_KEY=... bun run apps/sim/scripts/build-pi-daytona-snapshot.ts --name sim-pi: + * bun run apps/sim/scripts/build-pi-daytona-snapshot.ts --print + * + * Daytona rejects the latest/lts/stable tags, so the name MUST carry an explicit + * tag — CI passes the same `-` it uses for ECR images. + * + * After it builds, set the printed value in the Sim app's .env: + * DAYTONA_PI_SNAPSHOT_ID= + */ + +import { Daytona, Image } from '@daytonaio/sdk' +import { + PI_APT, + PI_NODE_MAJOR, + PI_NODE_VERSION_ASSERT, + PI_NPM, +} from '@/scripts/pi-sandbox-packages' + +/** Matches E2B's base: Debian 13 (trixie) with Python 3.13 installed to /usr/local. */ +const BASE_IMAGE = 'python:3.13-slim-trixie' + +/** + * `daytona-large` sizing. 10 GB is a HARD per-sandbox disk cap — the API rejects + * anything larger ("Disk request 20GB exceeds maximum allowed per sandbox + * (10GB)"), regardless of plan tier, and raising it requires contacting Daytona. + * That is the binding constraint on how large a repo Pi can clone here. + */ +const RESOURCES = { cpu: 4, memory: 8, disk: 10 } as const + +const APT_PREFIX = 'DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends' + +export const piImage = Image.base(BASE_IMAGE).runCommands( + `apt-get update && ${APT_PREFIX} curl ca-certificates gnupg && rm -rf /var/lib/apt/lists/*`, + // Node 22 from NodeSource, asserted at build time so an upstream change that + // ships an older Node fails here rather than at the first agent run. + `apt-get update && curl -fsSL https://deb.nodesource.com/setup_${PI_NODE_MAJOR}.x | bash - && ${APT_PREFIX} nodejs && rm -rf /var/lib/apt/lists/* && ${PI_NODE_VERSION_ASSERT}`, + `apt-get update && ${APT_PREFIX} ${PI_APT.join(' ')} && rm -rf /var/lib/apt/lists/*`, + `npm install -g ${PI_NPM.join(' ')}`, + // The clone target. E2B's base ships a world-writable /code; Pi writes to + // /workspace (cloud-review-tools.ts:14), so create it explicitly. + 'mkdir -p /workspace' +) + +async function main() { + const args = process.argv.slice(2) + + if (args.includes('--print')) { + console.log(piImage.dockerfile) + return + } + + const nameIdx = args.indexOf('--name') + const snapshotName = nameIdx !== -1 ? args[nameIdx + 1] : process.env.DAYTONA_SNAPSHOT_NAME + if (!snapshotName) { + console.error('A snapshot name is required (--name or DAYTONA_SNAPSHOT_NAME)') + process.exit(1) + } + // Daytona resolves snapshots by exact tag; `latest` is rejected outright, and an + // untagged name would silently pin to whatever was built last. + if (!snapshotName.includes(':')) { + console.error( + `Snapshot name must include an explicit tag (got "${snapshotName}"). ` + + 'Daytona does not support latest/lts/stable.' + ) + process.exit(1) + } + if (!process.env.DAYTONA_API_KEY) { + console.error('DAYTONA_API_KEY is required') + process.exit(1) + } + + console.log(`Building Pi Daytona snapshot: ${snapshotName}\n`) + + const daytona = new Daytona({ apiKey: process.env.DAYTONA_API_KEY }) + await daytona.snapshot.create( + { name: snapshotName, image: piImage, resources: RESOURCES }, + { onLogs: (log: string) => process.stdout.write(` ${log}`) } + ) + + console.log(`\nDone. Set in .env: DAYTONA_PI_SNAPSHOT_ID=${snapshotName}`) +} + +main().catch((error: unknown) => { + console.error('Build failed:', error instanceof Error ? error.message : error) + process.exit(1) +}) diff --git a/apps/sim/scripts/build-pi-e2b-template.ts b/apps/sim/scripts/build-pi-e2b-template.ts index f4641a2b7b3..147f933dae9 100644 --- a/apps/sim/scripts/build-pi-e2b-template.ts +++ b/apps/sim/scripts/build-pi-e2b-template.ts @@ -16,29 +16,26 @@ */ import { defaultBuildLogger, Template, waitForTimeout } from '@e2b/code-interpreter' +import { + PI_APT, + PI_NODE_MAJOR, + PI_NODE_VERSION_ASSERT, + PI_NPM, +} from '@/scripts/pi-sandbox-packages' const DEFAULT_TEMPLATE_NAME = 'sim-pi' -/** Exact first-party Pi versions mirrored from bun.lock because E2B builds run npm independently. */ -const PI_PACKAGES = [ - '@earendil-works/pi-coding-agent@0.80.10', - '@earendil-works/pi-agent-core@0.80.10', - '@earendil-works/pi-ai@0.80.10', - '@earendil-works/pi-tui@0.80.10', -] as const - /** Pi 0.80 requires Node >=22.19; E2B's code-interpreter base currently ships Node 20. */ -const INSTALL_NODE_COMMAND = - 'curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs && node -e "const [major, minor] = process.versions.node.split(\'.\').map(Number); if (major < 22 || (major === 22 && minor < 19)) process.exit(1)"' +const INSTALL_NODE_COMMAND = `curl -fsSL https://deb.nodesource.com/setup_${PI_NODE_MAJOR}.x | bash - && apt-get install -y nodejs && ${PI_NODE_VERSION_ASSERT}` -/** Pi uses E2B's command and filesystem APIs, so the inherited Jupyter service is unnecessary. */ +/** Pi uses the command and filesystem APIs, so the inherited Jupyter service is unnecessary. */ const START_COMMAND = 'sleep infinity' const piTemplate = Template() .fromTemplate('code-interpreter-v1') .runCmd(INSTALL_NODE_COMMAND, { user: 'root' }) - .aptInstall(['git', 'gh', 'openssh-client', 'ca-certificates', 'ripgrep', 'fd-find']) - .npmInstall([...PI_PACKAGES], { g: true }) + .aptInstall([...PI_APT]) + .npmInstall([...PI_NPM], { g: true }) .setStartCmd(START_COMMAND, waitForTimeout(1_000)) async function main() { diff --git a/apps/sim/scripts/pi-sandbox-packages.ts b/apps/sim/scripts/pi-sandbox-packages.ts new file mode 100644 index 00000000000..4a7b29c2fe9 --- /dev/null +++ b/apps/sim/scripts/pi-sandbox-packages.ts @@ -0,0 +1,54 @@ +/** + * The single source of truth for what goes into the Pi sandbox image. + * + * Two renderers consume these lists: + * - `build-pi-e2b-template.ts` — E2B template, via the `Template()` builder DSL + * - `build-pi-daytona-snapshot.ts` — Daytona snapshot, via the `Image` builder + * + * A package added here reaches both providers. Adding one to only a single + * renderer is the drift that makes a failover fail at the worst moment. + * + * The copilot repo holds the equivalent lists for the shell and doc sandboxes + * (`copilot/scripts/sandbox/packages.ts`); the Pi image lives here because Sim + * owns the Pi block. + */ + +/** Exact first-party Pi versions mirrored from bun.lock — image builds run npm independently. */ +export const PI_NPM = [ + '@earendil-works/pi-coding-agent@0.80.10', + '@earendil-works/pi-agent-core@0.80.10', + '@earendil-works/pi-ai@0.80.10', + '@earendil-works/pi-tui@0.80.10', +] as const + +/** + * `git`/`gh`/`openssh-client` back the clone → commit → push flow. `ripgrep` is + * required, not optional: the review tools shell out to the `rg` binary by name + * (`cloud-review-tools-script.ts:146`), so a missing package breaks code search + * at runtime rather than at build time. + */ +export const PI_APT = [ + 'git', + 'gh', + 'openssh-client', + 'ca-certificates', + 'ripgrep', + 'fd-find', +] as const + +/** + * Pi 0.80 requires Node >= 22.19 — higher than the Node 20 both the E2B base and + * the other two sandbox images carry, so this image installs its own. + */ +export const PI_NODE_MAJOR = 22 + +/** Fails the build loudly if the installed Node is older than Pi supports. */ +export const PI_NODE_VERSION_ASSERT = + 'node -e "const [major, minor] = process.versions.node.split(\'.\').map(Number); if (major < 22 || (major === 22 && minor < 19)) process.exit(1)"' + +/** + * The review tools run `python3 /workspace/sim-review-tools.py` + * (`cloud-review-tools.ts:15`). E2B's `code-interpreter-v1` base ships Python, so + * only the Daytona image has to provide it explicitly. + */ +export const PI_REQUIRES_PYTHON3 = true diff --git a/apps/sim/scripts/verify-sandbox-parity.ts b/apps/sim/scripts/verify-sandbox-parity.ts new file mode 100644 index 00000000000..4cb379883f2 --- /dev/null +++ b/apps/sim/scripts/verify-sandbox-parity.ts @@ -0,0 +1,198 @@ +#!/usr/bin/env bun + +/** + * Runs the real sandbox execution paths against whichever provider the + * `sandbox-provider-daytona` flag currently selects, and prints a pass/fail + * matrix. + * + * This is the pre-flip confidence check: run it against E2B, run it against + * Daytona, and compare. Every case exercises the shared layer end-to-end + * (`executeInSandbox` / `executeShellInSandbox`) against a live sandbox — not + * mocks — so it catches the failures unit tests cannot: a missing package, an + * expired snapshot, an image that vanished during an org move, blocked egress. + * + * Usage: + * # E2B (flag off) + * bun run apps/sim/scripts/verify-sandbox-parity.ts + * + * # Daytona (flag on via its env fallback) + * SANDBOX_PROVIDER_DAYTONA=true \ + * DAYTONA_SHELL_SNAPSHOT_ID=mothership-shell: \ + * DAYTONA_DOC_SNAPSHOT_ID=mothership-docs: \ + * bun run apps/sim/scripts/verify-sandbox-parity.ts + * + * Exits non-zero if any case fails, so it can be wired to a schedule later. + */ + +import { CodeLanguage } from '@/lib/execution/languages' +import { executeInSandbox, executeShellInSandbox } from '@/lib/execution/remote-sandbox' + +const SIM_RESULT_PREFIX = '__SIM_RESULT__=' + +interface Case { + name: string + /** Skipped unless the doc image is configured for the active provider. */ + needsDoc?: boolean + run: () => Promise<{ ok: boolean; detail: string }> +} + +const CASES: Case[] = [ + { + name: 'python: result marker round-trip', + run: async () => { + const res = await executeInSandbox({ + code: `import json\nprint("stdout-line")\nprint("${SIM_RESULT_PREFIX}" + json.dumps({"n": 42}))`, + language: CodeLanguage.Python, + timeoutMs: 120_000, + }) + const value = (res.result as { n?: number } | null)?.n + return { + ok: value === 42 && res.stdout.includes('stdout-line') && !res.error, + detail: `result=${JSON.stringify(res.result)} stdout=${JSON.stringify(res.stdout)}`, + } + }, + }, + { + name: 'python: data-science imports (base parity)', + run: async () => { + const res = await executeInSandbox({ + code: `import numpy, pandas, requests, bs4, openpyxl\nimport json\nprint("${SIM_RESULT_PREFIX}" + json.dumps({"numpy": numpy.__version__}))`, + language: CodeLanguage.Python, + timeoutMs: 180_000, + }) + return { + ok: !res.error && Boolean((res.result as { numpy?: string } | null)?.numpy), + detail: res.error ?? `numpy=${(res.result as { numpy?: string } | null)?.numpy}`, + } + }, + }, + { + name: 'python: structured error (name/value/traceback)', + run: async () => { + const res = await executeInSandbox({ + code: 'raise ValueError("boom")', + language: CodeLanguage.Python, + timeoutMs: 120_000, + }) + return { + ok: res.error === 'ValueError: boom' && res.result === null, + detail: `error=${JSON.stringify(res.error)}`, + } + }, + }, + { + name: 'python: outbound network (egress parity)', + run: async () => { + const res = await executeInSandbox({ + code: `import json, urllib.request\ncode = urllib.request.urlopen("https://example.com", timeout=20).status\nprint("${SIM_RESULT_PREFIX}" + json.dumps({"status": code}))`, + language: CodeLanguage.Python, + timeoutMs: 120_000, + }) + const status = (res.result as { status?: number } | null)?.status + return { ok: status === 200, detail: res.error ?? `status=${status}` } + }, + }, + { + name: 'javascript: imports run under node', + run: async () => { + const res = await executeInSandbox({ + code: `const os = require('os')\nconsole.log('${SIM_RESULT_PREFIX}' + JSON.stringify({ platform: os.platform() }))`, + language: CodeLanguage.JavaScript, + timeoutMs: 120_000, + }) + const platform = (res.result as { platform?: string } | null)?.platform + return { ok: platform === 'linux', detail: res.error ?? `platform=${platform}` } + }, + }, + { + name: 'shell: env vars + user-authored marker', + run: async () => { + const res = await executeShellInSandbox({ + code: `echo "${SIM_RESULT_PREFIX}$MY_VAR"`, + envs: { MY_VAR: 'from-env' }, + timeoutMs: 120_000, + }) + return { ok: res.result === 'from-env', detail: `result=${JSON.stringify(res.result)}` } + }, + }, + { + name: 'mount: inline file in, text file out', + run: async () => { + const res = await executeInSandbox({ + code: `data = open("/tmp/in.txt").read().strip()\nopen("/tmp/out.txt", "w").write(data.upper())\nprint("${SIM_RESULT_PREFIX}null")`, + language: CodeLanguage.Python, + timeoutMs: 120_000, + sandboxFiles: [{ path: '/tmp/in.txt', content: 'mounted' }], + outputSandboxPath: '/tmp/out.txt', + }) + return { + ok: res.exportedFileContent?.trim() === 'MOUNTED', + detail: res.error ?? `exported=${JSON.stringify(res.exportedFileContent)}`, + } + }, + }, + { + name: 'doc: xlsx compile + base64 binary export', + needsDoc: true, + run: async () => { + const res = await executeInSandbox({ + code: `import openpyxl\nwb = openpyxl.Workbook(); ws = wb.active\nws["A1"] = 5; ws["A2"] = 7; ws["A3"] = "=A1+A2"\nwb.save("/tmp/out.xlsx")\nprint("${SIM_RESULT_PREFIX}null")`, + language: CodeLanguage.Python, + timeoutMs: 180_000, + sandboxKind: 'doc', + outputSandboxPath: '/tmp/out.xlsx', + }) + const b64 = res.exportedFileContent ?? '' + // xlsx is a ZIP: base64 of a PK.. header always starts UEsD. + return { + ok: b64.startsWith('UEsD') && b64.length > 1000, + detail: res.error ?? `base64Len=${b64.length} head=${b64.slice(0, 8)}`, + } + }, + }, +] + +async function main() { + const provider = process.env.SANDBOX_PROVIDER_DAYTONA ? 'daytona' : 'e2b' + const docConfigured = + provider === 'daytona' + ? Boolean(process.env.DAYTONA_DOC_SNAPSHOT_ID) + : Boolean(process.env.MOTHERSHIP_E2B_DOC_TEMPLATE_ID) + + console.log(`\nsandbox parity — provider: ${provider}\n${'='.repeat(50)}`) + + let failed = 0 + let skipped = 0 + for (const testCase of CASES) { + if (testCase.needsDoc && !docConfigured) { + console.log(`SKIP ${testCase.name} (doc image not configured)`) + skipped++ + continue + } + const started = Date.now() + try { + const { ok, detail } = await testCase.run() + const seconds = ((Date.now() - started) / 1000).toFixed(1) + console.log(`${ok ? 'PASS' : 'FAIL'} ${testCase.name} (${seconds}s)`) + if (!ok) { + console.log(` ${detail}`) + failed++ + } + } catch (error) { + console.log(`FAIL ${testCase.name}`) + console.log(` threw: ${error instanceof Error ? error.message : String(error)}`) + failed++ + } + } + + const passed = CASES.length - failed - skipped + console.log( + `${'='.repeat(50)}\n${provider}: ${passed} passed, ${failed} failed, ${skipped} skipped\n` + ) + if (failed > 0) process.exit(1) +} + +main().catch((error: unknown) => { + console.error('harness error:', error instanceof Error ? error.message : error) + process.exit(1) +}) diff --git a/bun.lock b/bun.lock index dcce09861c5..1a44dd65c3b 100644 --- a/bun.lock +++ b/bun.lock @@ -133,6 +133,7 @@ "@browserbasehq/stagehand": "^3.2.1", "@calcom/embed-react": "1.5.3", "@cerebras/cerebras_cloud_sdk": "^1.23.0", + "@daytonaio/sdk": "0.197.0", "@e2b/code-interpreter": "^2.0.0", "@earendil-works/pi-ai": "0.80.10", "@earendil-works/pi-coding-agent": "0.80.10", @@ -745,6 +746,8 @@ "@aws-sdk/lib-dynamodb": ["@aws-sdk/lib-dynamodb@3.1032.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.1", "@aws-sdk/util-dynamodb": "^3.996.2", "@smithy/core": "^3.23.15", "@smithy/smithy-client": "^4.12.11", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-dynamodb": "^3.1032.0" } }, "sha512-rYGhqP1H0Fy4r1yvWTmEAx0qqy1Zd9OzI8pPkXo6KSEDjZ4EwU+6QN1V+KLX3XTU6FQouF5LTvqLtl/CW4gxyQ=="], + "@aws-sdk/lib-storage": ["@aws-sdk/lib-storage@3.1088.0", "", { "dependencies": { "@smithy/core": "^3.29.4", "@smithy/types": "^4.16.1", "buffer": "5.6.0", "events": "3.3.0", "stream-browserify": "3.0.0", "tslib": "^2.6.2" }, "peerDependencies": { "@aws-sdk/client-s3": "^3.1088.0" } }, "sha512-OElyotfOuTLAkWeYfWvgFQ/ABVs5xi9+UzlnKveFnbyfzV9um0guDWGanG/xGlw9ofAiplscHlNauChtJUBNWA=="], + "@aws-sdk/middleware-bucket-endpoint": ["@aws-sdk/middleware-bucket-endpoint@3.972.25", "", { "dependencies": { "@aws-sdk/middleware-sdk-s3": "^3.972.52", "tslib": "^2.6.2" } }, "sha512-zcRjdhS46gQ+omEKod2Q83A+42dQlFgQP9GfsK2XcDCli8kzA3q1QH+hDpIZUDbKaXmkTSn0JG3WP5yds5j38g=="], "@aws-sdk/middleware-endpoint-discovery": ["@aws-sdk/middleware-endpoint-discovery@3.972.19", "", { "dependencies": { "@aws-sdk/endpoint-cache": "^3.972.8", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-FMgyzUq3Jh+ONRYxryBRNdBd+FUX8PwRl07ccQknNdoms6KCeAEusCkl6whqpDrPQ6OH0ddeSifKyqYSs2DLIw=="], @@ -947,6 +950,14 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + "@daytona/analytics-api-client": ["@daytona/analytics-api-client@0.197.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-8S8JBVwIhErhDv22kCtifonfMnpQXtoAqz3migT53u7LCnjnVkqOeUFB/xN4SD7LN+Bhbh6AMVkvNwZA2BkIfw=="], + + "@daytona/api-client": ["@daytona/api-client@0.197.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-O7BF07FOmmbNrp/An3EIx/Vq99wSHvxxWl4nUttw0eHpe7oKdYaUVlM2MmdJ/AbrFnYDLHfrQZGZfeUOEeRjpw=="], + + "@daytona/toolbox-api-client": ["@daytona/toolbox-api-client@0.197.0", "", { "dependencies": { "axios": "^1.6.1" } }, "sha512-uBcbIAPcqeJUOpessqf2Za6Jid/Negn0x3wJlTcby391gsPUYDgsGzOmDZMs/rFKQ+/xtz/DAd7VThJZC5fnIQ=="], + + "@daytonaio/sdk": ["@daytonaio/sdk@0.197.0", "", { "dependencies": { "@aws-sdk/client-s3": "^3.787.0", "@aws-sdk/lib-storage": "^3.798.0", "@daytona/analytics-api-client": "0.197.0", "@daytona/api-client": "0.197.0", "@daytona/toolbox-api-client": "0.197.0", "@iarna/toml": "^2.2.5", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.219.0", "@opentelemetry/instrumentation-http": "^0.219.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-node": "^0.219.0", "@opentelemetry/sdk-trace-base": "^2.8.0", "@opentelemetry/semantic-conventions": "^1.40.0", "axios": "^1.15.2", "busboy": "^1.0.0", "dotenv": "^17.0.1", "expand-tilde": "^2.0.2", "fast-glob": "^3.3.0", "form-data": "^4.0.4", "isomorphic-ws": "^5.0.0", "pathe": "^2.0.3", "shell-quote": "^1.8.2", "tar": "^7.5.11" } }, "sha512-RFrK/TLZy8S0VyQcXOzOv70VKNUoUyJ45u/HlCJjmXu5vyK3TH0pQJv9yJn8uT49W+4ypMSODcz0YJIRjH16VA=="], + "@derhuerst/http-basic": ["@derhuerst/http-basic@8.2.4", "", { "dependencies": { "caseless": "^0.12.0", "concat-stream": "^2.0.0", "http-response-object": "^3.0.1", "parse-cache-control": "^1.0.1" } }, "sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw=="], "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], @@ -1063,6 +1074,8 @@ "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="], + "@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="], + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], "@iconify/utils": ["@iconify/utils@3.1.3", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw=="], @@ -1289,9 +1302,11 @@ "@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.217.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.217.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-24ucQMjz7Y34Kw3trbxL2ZrssbtgWnR+Clpaa+YdeWuuyH3Cvk23Q03PcQvqiZrDvt8AmQmjgg9v6Y9PHoxG7w=="], - "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + "@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/instrumentation": "0.219.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-nNt1fqpyah/OKjNHdEOu8xLwISppRU2qJuF8aR+fCcftVwdFkPgtworBLA+TI1HU2iF508jcQBF2gerWczJAXg=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-transformer": "0.219.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA=="], - "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.217.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-exporter-base": "0.217.0", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag=="], + "@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.219.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-iIk/s8QQu39zpTrRRmsW/Eg3SE2+Hg8tLWepr2FLRgmwUpNd0IpCTLJEHJ77hpt4hgIS8MAh44UYI4xQPZwWlw=="], "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.217.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.217.0", "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/sdk-logs": "0.217.0", "@opentelemetry/sdk-metrics": "2.7.1", "@opentelemetry/sdk-trace-base": "2.7.1", "protobufjs": "8.0.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-MKK8UHKFUOGAvbZRWh90MhwHG+Fxm6OROBdjKPCF+HQobjuJ/Kuf8Chs8CR45X1aqotxrMj7OxTdsXe8sXuGVA=="], @@ -2197,7 +2212,7 @@ "bson": ["bson@6.10.4", "", {}, "sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng=="], - "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "buffer": ["buffer@5.6.0", "", { "dependencies": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" } }, "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw=="], "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], @@ -2619,6 +2634,8 @@ "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expand-tilde": ["expand-tilde@2.0.2", "", { "dependencies": { "homedir-polyfill": "^1.0.1" } }, "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], @@ -2689,6 +2706,8 @@ "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + "forwarded-parse": ["forwarded-parse@2.1.2", "", {}, "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw=="], + "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], "framer-motion": ["framer-motion@12.42.2", "", { "dependencies": { "motion-dom": "^12.42.2", "motion-utils": "^12.39.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw=="], @@ -2805,6 +2824,8 @@ "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + "homedir-polyfill": ["homedir-polyfill@1.0.3", "", { "dependencies": { "parse-passwd": "^1.0.0" } }, "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA=="], + "hono": ["hono@4.12.25", "", {}, "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ=="], "hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="], @@ -2929,6 +2950,8 @@ "isomorphic-unfetch": ["isomorphic-unfetch@3.1.0", "", { "dependencies": { "node-fetch": "^2.6.1", "unfetch": "^4.2.0" } }, "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q=="], + "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], @@ -3391,6 +3414,8 @@ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], + "parse-passwd": ["parse-passwd@1.0.0", "", {}, "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q=="], + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], @@ -3747,6 +3772,8 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "shell-quote": ["shell-quote@1.10.0", "", {}, "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA=="], + "shiki": ["shiki@4.3.1", "", { "dependencies": { "@shikijs/core": "4.3.1", "@shikijs/engine-javascript": "4.3.1", "@shikijs/engine-oniguruma": "4.3.1", "@shikijs/langs": "4.3.1", "@shikijs/themes": "4.3.1", "@shikijs/types": "4.3.1", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw=="], "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], @@ -3823,6 +3850,8 @@ "stdin-discarder": ["stdin-discarder@0.2.2", "", {}, "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ=="], + "stream-browserify": ["stream-browserify@3.0.0", "", { "dependencies": { "inherits": "~2.0.4", "readable-stream": "^3.5.0" } }, "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA=="], + "stream-events": ["stream-events@1.0.5", "", { "dependencies": { "stubs": "^3.0.0" } }, "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg=="], "stream-shift": ["stream-shift@1.0.3", "", {}, "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ=="], @@ -4163,6 +4192,10 @@ "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1069.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.21", "@aws-sdk/nested-clients": "^3.997.21", "@aws-sdk/types": "^3.973.13", "@smithy/core": "^3.24.6", "@smithy/types": "^4.14.3", "tslib": "^2.6.2" } }, "sha512-ks4X+kngC3PA5howV7Qu1TgG4bfC4jPykKdvw3nmBSXR9yZxRJouBholFSNQ5kY3L+Fgwyw+LCjzQmNi+KR91g=="], + "@aws-sdk/lib-storage/@smithy/core": ["@smithy/core@3.29.4", "", { "dependencies": { "@smithy/types": "^4.16.1", "tslib": "^2.6.2" } }, "sha512-G1GRglAabzEhqghJMBAd54FkRS7SAFGHEwbhcI9r+O+LIMuFsLyXkLZkCoFSgAglRu8s/URVXJB0hglq3ZipIg=="], + + "@aws-sdk/lib-storage/@smithy/types": ["@smithy/types@4.16.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg=="], + "@aws-sdk/xml-builder/fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], "@azure/communication-email/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], @@ -4199,6 +4232,12 @@ "@cerebras/cerebras_cloud_sdk/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/configuration": "0.219.0", "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/exporter-logs-otlp-grpc": "0.219.0", "@opentelemetry/exporter-logs-otlp-http": "0.219.0", "@opentelemetry/exporter-logs-otlp-proto": "0.219.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.219.0", "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.219.0", "@opentelemetry/exporter-prometheus": "0.219.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.219.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/exporter-trace-otlp-proto": "0.219.0", "@opentelemetry/exporter-zipkin": "2.8.0", "@opentelemetry/instrumentation": "0.219.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", "@opentelemetry/propagator-b3": "2.8.0", "@opentelemetry/propagator-jaeger": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NWLpWLEb8gV3+JBHYoIrktbM385wyHpRJoh3J/4Q52d4PR+AlPMNGJT3DzBUrDSUEVbKAXoHR+EDAPxtiNcj8g=="], + + "@daytonaio/sdk/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + "@earendil-works/pi-ai/@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.91.1", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw=="], "@earendil-works/pi-ai/@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1048.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.11", "@aws-sdk/credential-provider-node": "^3.972.42", "@aws-sdk/eventstream-handler-node": "^3.972.16", "@aws-sdk/middleware-eventstream": "^3.972.12", "@aws-sdk/middleware-websocket": "^3.972.19", "@aws-sdk/token-providers": "3.1048.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/fetch-http-handler": "^5.4.2", "@smithy/node-http-handler": "^4.7.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ=="], @@ -4235,18 +4274,34 @@ "@octokit/plugin-rest-endpoint-methods/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="], + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + + "@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.217.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-exporter-base": "0.217.0", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag=="], + + "@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], "@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.217.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-exporter-base": "0.217.0", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag=="], + "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], @@ -4255,14 +4310,22 @@ "@opentelemetry/exporter-prometheus/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-grpc-exporter-base": ["@opentelemetry/otlp-grpc-exporter-base@0.217.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-exporter-base": "0.217.0", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7RTAdZuOsCDnsyqTCG4+bDzrfnsWdzkRs7z0AVi/V3tEQx0oKeyc+OuRWYxnRsmaJXgxcmB8vb/lfxn58Dj6Ag=="], + "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], "@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], @@ -4271,6 +4334,18 @@ "@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw=="], + "@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + + "@opentelemetry/instrumentation-http/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ=="], + + "@opentelemetry/otlp-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], "@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], @@ -4285,6 +4360,8 @@ "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + "@opentelemetry/sdk-node/@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.217.0", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/otlp-transformer": "0.217.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-eYfqnB3UhKu/5frhd1R6+FprKygbhkomuaceMXDyzxbfXB9tKgZOVmjaJ02CkLA6Tdzumxl+e2H+vo2a8jiMPQ=="], + "@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ=="], "@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.7.1", "", { "dependencies": { "@opentelemetry/core": "2.7.1", "@opentelemetry/resources": "2.7.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-MpDJdkiFDs3Pm1RHO3KByuZbuBdJEXEAkiC0+yJdsZGVCdf1RpHR6n+LHDcS7ffmfrt5kVCzJSCfm4z2C7v0uQ=="], @@ -4749,6 +4826,8 @@ "mysql2/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "neo4j-driver-bolt-connection/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "neo4j-driver-bolt-connection/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], @@ -4845,6 +4924,8 @@ "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "stream-browserify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + "streamdown/tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -4911,6 +4992,46 @@ "@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + "@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + + "@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/configuration": ["@opentelemetry/configuration@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "yaml": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-wXZUYv4ngu43nA4WEhuXNacm46LW+17LRM8nKyIhBzroRA24PBYjMnakwzR/w777nFUB5xlgsYTTeuXxumZM1Q=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.8.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc": ["@opentelemetry/exporter-logs-otlp-grpc@0.219.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/sdk-logs": "0.219.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-7SvzDCIclHWAcCwZ1MTOLcwn4BVNPGI3QxS/DJraPNe1TTL+4TvUBq5zeQV8tsnYvtDN7wKW2qocVmaCP2l7sQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/sdk-logs": "0.219.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-mhl2HL6GmZI8b8PwPfqMws/5ovJfbRTxwc9Y5agVVHiQ+e5SL1btsFr/kJDgt7YCexDtsUn5HAreHQO9szFS0A=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto": ["@opentelemetry/exporter-logs-otlp-proto@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Ayw4Gf71PS9jhBVaYywa4WsajnqfDehMkTdVH3TSAVHqPcsAv/AhH/wTNRYNt99szeYr6Gbd/D6RjZD77wAxHg=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-grpc": ["@opentelemetry/exporter-metrics-otlp-grpc@0.219.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.8.0", "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-metrics": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6LaaSrPxK5L55bXevWajvOMxGOpNm0n12tG53TeZaUeNzXwLPg6d2KCC1zAlGsojan+xRG71mA4Qqs9K2VVrKQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-metrics": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-6CaDRbMVHZSDWzNXwrR8y/H4B/Z1eMNnkHiPQlTx3Ojz2OHY4X/aff/UC4P/3pHUQSuTfi3oh2UsPPZppw+Vrg=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-proto": ["@opentelemetry/exporter-metrics-otlp-proto@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/exporter-metrics-otlp-http": "0.219.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-metrics": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DUS7XyIiEnoeccQUvuKy0G2/YqeKhpN8FVIrGbrLNIVMj10yeIFLRzRv0tibCI2kXXvlTTABVexGAk78wHk2ug=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-prometheus": ["@opentelemetry/exporter-prometheus@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TxOnJ85eWJY5JyOJsNMXiRTYlkDcOv0u3KbXEzWCc+tUS9sjL/BC6BcdxZ0B9r2OFVqsrZFXUzSD2sZUy42Ucw=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc": ["@opentelemetry/exporter-trace-otlp-grpc@0.219.0", "", { "dependencies": { "@grpc/grpc-js": "^1.14.3", "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-grpc-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BkDNv1UD6BscW19MxbAxVmSYSSFuyeqR6buV2/HTYqA7GrR0EbTFzqG6h86T3PtXmpdbsWjMGLDdjG2rikG27Q=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto": ["@opentelemetry/exporter-trace-otlp-proto@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-lF/LUBfhOFmxJa+SQsLN7ziV4MHa2pyKgOM6JNehSOfU+npjM4gwm9oIKEJrzrWcexMcqydiyoFy0XCb1Ql3wQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-zipkin": ["@opentelemetry/exporter-zipkin@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0" } }, "sha512-Mj84UkEa17BK2o903VTXW3wM8CrSZexGs4tRGVZVIMM9ni1T6TuGx5IrRfoWKAbshx42D5/kc7YV+axypLPYyA=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-X5t7I8GyIO9rmGHwoedZLREpQqrF1WW2nxzNNym6HOKpFiE+rvqV3ngC0xcZVO2YwIGf3KKmRdWrYwdwz3H9RQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/propagator-b3": ["@opentelemetry/propagator-b3@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SazlvuSKi5533rPHTW2TwBwdMakhjZST4SYs0YauuvfGDkT13KbG1gJS75hV0uWVeevhtVP9sAIlaZLTHdSbMg=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Xnz9zZvvQzUw+9DrOn0MomR7BxFCkA2pcfXBQuHC28ndJpSbjLs7knzYb05kw5SyCjSsEWombkZMgGcJSk8JVg=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="], + "@earendil-works/pi-ai/@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1048.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.11", "@aws-sdk/nested-clients": "^3.997.9", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.2", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA=="], "@earendil-works/pi-ai/@aws-sdk/client-bedrock-runtime/@smithy/node-http-handler": ["@smithy/node-http-handler@4.8.0", "", { "dependencies": { "@smithy/core": "^3.25.0", "@smithy/types": "^4.15.0", "tslib": "^2.6.2" } }, "sha512-Mq7TNt/VhlEWiYRLQGpzUWeUxh899UGpjKh7Ru0WVIDIjnE+cTRAn0NYlFQ6bWfsQnKnpCbWJj86HzmcG0qEdg=="], @@ -4967,6 +5088,16 @@ "@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="], + "@opentelemetry/instrumentation-http/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="], + + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="], + + "@opentelemetry/otlp-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="], + + "@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="], + "@opentelemetry/otlp-transformer/protobufjs/@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="], "@radix-ui/react-accordion/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.3.0", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA=="], @@ -5265,6 +5396,8 @@ "react-email/chokidar/readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "readable-web-to-node-stream/readable-stream/buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "readable-web-to-node-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "restore-cursor/onetime/mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], @@ -5283,6 +5416,8 @@ "simstudio/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "stream-browserify/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "teeny-request/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], @@ -5359,6 +5494,26 @@ "@cerebras/cerebras_cloud_sdk/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="], + + "@daytonaio/sdk/@opentelemetry/exporter-trace-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-logs-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + + "@daytonaio/sdk/@opentelemetry/sdk-node/@opentelemetry/exporter-trace-otlp-proto/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="], + "@google-cloud/storage/google-auth-library/gcp-metadata/google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="], "@opentelemetry/otlp-transformer/protobufjs/@types/node/undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], From d4327d3d8a28dc80b03f13c5317106e715b3317e Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 23 Jul 2026 12:01:21 -0700 Subject: [PATCH 2/8] improvement(sandbox): select the provider by SANDBOX_PROVIDER env var MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the boolean sandbox-provider-daytona feature flag with a SANDBOX_PROVIDER env var naming the provider ('e2b' default, or 'daytona'). A boolean doesn't scale to a third adapter; a keyed registry does. - PROVIDERS is a Record, so adding an adapter is one entry plus one id-union member — an unhandled provider is a compile error, not a runtime surprise - resolveProvider() reads env synchronously and throws on an unknown value (fail fast) instead of an async feature-flag lookup - drops the sandbox-provider-daytona flag and SANDBOX_PROVIDER_DAYTONA fallback Verified end-to-end: a Python function block through the running app routes to Daytona (Creating Daytona sandbox, kind: code) with SANDBOX_PROVIDER=daytona. --- apps/sim/lib/core/config/env.ts | 6 ++- apps/sim/lib/core/config/feature-flags.ts | 10 ---- .../remote-sandbox/conformance.test.ts | 50 ++++++++++--------- .../sim/lib/execution/remote-sandbox/index.ts | 39 +++++++++++---- apps/sim/scripts/verify-sandbox-parity.ts | 13 +++-- 5 files changed, 67 insertions(+), 51 deletions(-) diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 7fca56733a5..80c9081dcc2 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -454,12 +454,14 @@ 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) + // Remote Code Execution provider selection + SANDBOX_PROVIDER: z.string().optional(), // Which sandbox provider serves remote executions: 'e2b' (default) or 'daytona' + + // Daytona Remote Code Execution (used when SANDBOX_PROVIDER=daytona) 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) diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index 7981cea2eed..558707fa587 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -105,16 +105,6 @@ 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/' + diff --git a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts index 87d3c45d19a..9f249ce756d 100644 --- a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts +++ b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts @@ -10,7 +10,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { CodeLanguage } from '@/lib/execution/languages' const { - mockIsFeatureEnabled, + mockEnv, mockE2BCreate, mockE2BRunCode, mockE2BCommandsRun, @@ -25,7 +25,17 @@ const { mockDownloadFile, mockDelete, } = vi.hoisted(() => ({ - mockIsFeatureEnabled: vi.fn(), + mockEnv: { + SANDBOX_PROVIDER: 'e2b' as string | undefined, + E2B_API_KEY: 'test-key', + MOTHERSHIP_E2B_TEMPLATE_ID: 'mothership-shell', + MOTHERSHIP_E2B_DOC_TEMPLATE_ID: 'mothership-docs', + E2B_PI_TEMPLATE_ID: 'sim-pi', + DAYTONA_API_KEY: 'test-key', + DAYTONA_SHELL_SNAPSHOT_ID: 'mothership-shell:v1' as string | undefined, + DAYTONA_DOC_SNAPSHOT_ID: 'mothership-docs:v1' as string | undefined, + DAYTONA_PI_SNAPSHOT_ID: 'sim-pi:v1' as string | undefined, + }, mockE2BCreate: vi.fn(), mockE2BRunCode: vi.fn(), mockE2BCommandsRun: vi.fn(), @@ -47,19 +57,7 @@ vi.mock('@daytonaio/sdk', () => ({ create = mockDaytonaCreate }, })) -vi.mock('@/lib/core/config/env', () => ({ - env: { - E2B_API_KEY: 'test-key', - MOTHERSHIP_E2B_TEMPLATE_ID: 'mothership-shell', - MOTHERSHIP_E2B_DOC_TEMPLATE_ID: 'mothership-docs', - E2B_PI_TEMPLATE_ID: 'sim-pi', - DAYTONA_API_KEY: 'test-key', - DAYTONA_SHELL_SNAPSHOT_ID: 'mothership-shell:v1', - DAYTONA_DOC_SNAPSHOT_ID: 'mothership-docs:v1', - DAYTONA_PI_SNAPSHOT_ID: 'sim-pi:v1', - }, -})) -vi.mock('@/lib/core/config/feature-flags', () => ({ isFeatureEnabled: mockIsFeatureEnabled })) +vi.mock('@/lib/core/config/env', () => ({ env: mockEnv })) import { executeInSandbox, @@ -70,9 +68,9 @@ import { type Provider = 'e2b' | 'daytona' const PROVIDERS: Provider[] = ['e2b', 'daytona'] -/** Points the shared layer at one provider and stubs that provider's SDK. */ +/** Points the shared layer at one provider via the SANDBOX_PROVIDER env var. */ function useProvider(provider: Provider) { - mockIsFeatureEnabled.mockResolvedValue(provider === 'daytona') + mockEnv.SANDBOX_PROVIDER = provider } /** Stubs a code execution that prints `stdout` and emits `result` via the marker. */ @@ -295,11 +293,11 @@ describe.each(PROVIDERS)('sandbox conformance [%s]', (provider) => { }) describe('provider selection', () => { - it('routes to E2B when the flag is off and Daytona when it is on', async () => { + it('routes by SANDBOX_PROVIDER, defaulting to E2B when unset', async () => { stubCodeRun('e2b', `${SIM_RESULT_PREFIX}null`) stubCodeRun('daytona', `${SIM_RESULT_PREFIX}null`) - useProvider('e2b') + mockEnv.SANDBOX_PROVIDER = undefined await executeInSandbox({ code: 'x', language: CodeLanguage.Python, timeoutMs: 1000 }) expect(mockE2BCreate).toHaveBeenCalledTimes(1) expect(mockDaytonaCreate).not.toHaveBeenCalled() @@ -309,6 +307,13 @@ describe('provider selection', () => { expect(mockDaytonaCreate).toHaveBeenCalledTimes(1) }) + it('throws on an unknown SANDBOX_PROVIDER', async () => { + mockEnv.SANDBOX_PROVIDER = 'modal' + await expect( + executeInSandbox({ code: 'x', language: CodeLanguage.Python, timeoutMs: 1000 }) + ).rejects.toThrow(/Unknown SANDBOX_PROVIDER "modal"/) + }) + it('binds language at create time so JS never runs through the Python toolbox', async () => { useProvider('daytona') mockProcessCodeRun.mockResolvedValue({ result: `${SIM_RESULT_PREFIX}null`, exitCode: 0 }) @@ -325,9 +330,8 @@ describe('provider selection', () => { it('fails closed when a Daytona snapshot id is unset', async () => { useProvider('daytona') - const { env } = await import('@/lib/core/config/env') - const original = env.DAYTONA_DOC_SNAPSHOT_ID - ;(env as { DAYTONA_DOC_SNAPSHOT_ID?: string }).DAYTONA_DOC_SNAPSHOT_ID = undefined + const original = mockEnv.DAYTONA_DOC_SNAPSHOT_ID + mockEnv.DAYTONA_DOC_SNAPSHOT_ID = undefined await expect( executeInSandbox({ @@ -337,6 +341,6 @@ describe('provider selection', () => { sandboxKind: 'doc', }) ).rejects.toThrow(/DAYTONA_DOC_SNAPSHOT_ID is unset/) - ;(env as { DAYTONA_DOC_SNAPSHOT_ID?: string }).DAYTONA_DOC_SNAPSHOT_ID = original + mockEnv.DAYTONA_DOC_SNAPSHOT_ID = original }) }) diff --git a/apps/sim/lib/execution/remote-sandbox/index.ts b/apps/sim/lib/execution/remote-sandbox/index.ts index 921a6586e3e..c076180260b 100644 --- a/apps/sim/lib/execution/remote-sandbox/index.ts +++ b/apps/sim/lib/execution/remote-sandbox/index.ts @@ -1,6 +1,6 @@ import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' -import { isFeatureEnabled } from '@/lib/core/config/feature-flags' +import { env } from '@/lib/core/config/env' import type { CodeLanguage } from '@/lib/execution/languages' import { daytonaProvider } from '@/lib/execution/remote-sandbox/daytona' import { e2bProvider } from '@/lib/execution/remote-sandbox/e2b' @@ -12,6 +12,7 @@ import type { SandboxHandle, SandboxKind, SandboxProvider, + SandboxProviderId, SandboxShellExecutionRequest, } from '@/lib/execution/remote-sandbox/types' @@ -25,24 +26,44 @@ export type { const logger = createLogger('RemoteSandbox') /** - * Resolves which provider serves this execution. + * The known sandbox providers. Keyed by {@link SandboxProviderId}, so adding an + * adapter is one entry here plus one member on the id union — the type makes an + * unhandled provider a compile error, not a runtime surprise. + */ +const PROVIDERS: Record = { + e2b: e2bProvider, + daytona: daytonaProvider, +} + +const DEFAULT_PROVIDER: SandboxProviderId = 'e2b' + +/** + * Resolves which provider serves this execution from the `SANDBOX_PROVIDER` env + * var (defaulting to {@link DEFAULT_PROVIDER}). * * Selection is deliberately resolved ONCE, before the sandbox is created, and is * never revisited mid-execution: user code has side effects (HTTP calls, S3 - * writes, DB mutations), so retrying a partially-executed run on the other - * provider could duplicate them. This is a manual failover — flip the - * `sandbox-provider-daytona` flag and new executions move over. + * writes, DB mutations), so retrying a partially-executed run on another provider + * could duplicate them. Changing providers is a config change — set + * `SANDBOX_PROVIDER` and redeploy; in-flight executions are unaffected. */ -async function resolveProvider(): Promise { - const useDaytona = await isFeatureEnabled('sandbox-provider-daytona') - return useDaytona ? daytonaProvider : e2bProvider +function resolveProvider(): SandboxProvider { + const configured = env.SANDBOX_PROVIDER + if (!configured) return PROVIDERS[DEFAULT_PROVIDER] + const provider = PROVIDERS[configured as SandboxProviderId] + if (!provider) { + throw new Error( + `Unknown SANDBOX_PROVIDER "${configured}" (expected one of: ${Object.keys(PROVIDERS).join(', ')})` + ) + } + return provider } async function createSandbox( kind: SandboxKind, options?: { language?: CodeLanguage } ): Promise { - const provider = await resolveProvider() + const provider = resolveProvider() const sandbox = await provider.create(kind, options) logger.info('Created sandbox', { provider: provider.id, kind, sandboxId: sandbox.sandboxId }) return sandbox diff --git a/apps/sim/scripts/verify-sandbox-parity.ts b/apps/sim/scripts/verify-sandbox-parity.ts index 4cb379883f2..74e02f4fbe6 100644 --- a/apps/sim/scripts/verify-sandbox-parity.ts +++ b/apps/sim/scripts/verify-sandbox-parity.ts @@ -2,21 +2,20 @@ /** * Runs the real sandbox execution paths against whichever provider the - * `sandbox-provider-daytona` flag currently selects, and prints a pass/fail - * matrix. + * `SANDBOX_PROVIDER` env var selects, and prints a pass/fail matrix. * - * This is the pre-flip confidence check: run it against E2B, run it against + * This is the pre-switch confidence check: run it against E2B, run it against * Daytona, and compare. Every case exercises the shared layer end-to-end * (`executeInSandbox` / `executeShellInSandbox`) against a live sandbox — not * mocks — so it catches the failures unit tests cannot: a missing package, an * expired snapshot, an image that vanished during an org move, blocked egress. * * Usage: - * # E2B (flag off) + * # E2B (default) * bun run apps/sim/scripts/verify-sandbox-parity.ts * - * # Daytona (flag on via its env fallback) - * SANDBOX_PROVIDER_DAYTONA=true \ + * # Daytona + * SANDBOX_PROVIDER=daytona \ * DAYTONA_SHELL_SNAPSHOT_ID=mothership-shell: \ * DAYTONA_DOC_SNAPSHOT_ID=mothership-docs: \ * bun run apps/sim/scripts/verify-sandbox-parity.ts @@ -153,7 +152,7 @@ const CASES: Case[] = [ ] async function main() { - const provider = process.env.SANDBOX_PROVIDER_DAYTONA ? 'daytona' : 'e2b' + const provider = process.env.SANDBOX_PROVIDER || 'e2b' const docConfigured = provider === 'daytona' ? Boolean(process.env.DAYTONA_DOC_SNAPSHOT_ID) From 008729152c2fc5305ff68d7f4b5d2ee935a2bd9f Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 23 Jul 2026 13:04:51 -0700 Subject: [PATCH 3/8] fix(sandbox): gate remote execution by provider availability, not E2B MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the review round on #5860. - Availability was gated on isE2bEnabled / isE2BDocEnabled, so a Daytona-only deployment (E2B_ENABLED unset) had its Python/shell/JS-with-imports and doc paths rejected before the provider-neutral sandbox call could run. Replace both with provider-aware flags (isRemoteSandboxEnabled / isDocSandboxEnabled) derived from the selected SANDBOX_PROVIDER's own credentials + image. E2B behavior is unchanged (the E2B branch mirrors the old definitions exactly). - Make the function-block gate error messages provider-neutral. - Daytona's streaming runCommand (Pi) returned empty stdout/stderr and delivered output only via callbacks, so the Pi cloud flow — which parses markers from stdout and formats errors from stderr — saw nothing. Accumulate the streamed chunks and return them while still forwarding to the callbacks. Renames the env-flag exports (and the @sim/testing mock) to match. Adds a conformance test that the streamed Pi output lands in stdout/stderr. --- .../app/api/function/execute/route.test.ts | 24 ++++---- apps/sim/app/api/function/execute/route.ts | 37 ++++++------ apps/sim/app/api/mothership/execute/route.ts | 4 +- .../files/[fileId]/compiled-check/route.ts | 4 +- apps/sim/lib/copilot/chat/payload.ts | 4 +- .../copilot/tools/server/files/doc-compile.ts | 6 +- .../tools/server/files/doc-servable.test.ts | 8 +-- .../tools/server/files/edit-content.ts | 4 +- .../tools/server/files/workspace-file.ts | 6 +- apps/sim/lib/copilot/vfs/workspace-vfs.ts | 10 ++-- apps/sim/lib/core/config/env-flags.ts | 32 ++++++++--- .../remote-sandbox/conformance.test.ts | 56 ++++++++++++++++++- .../lib/execution/remote-sandbox/daytona.ts | 19 ++++++- apps/sim/lib/mothership/inbox/executor.ts | 4 +- packages/testing/src/mocks/env-flags.mock.ts | 8 +-- 15 files changed, 157 insertions(+), 69 deletions(-) diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index 0292896f702..b240a68a099 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -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, @@ -351,7 +351,7 @@ describe('Function Execute API Route', () => { }) it('exports multiple declared sandbox output files', async () => { - envFlagsMock.isE2bEnabled = true + envFlagsMock.isRemoteSandboxEnabled = true mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', stdout: 'ok', @@ -419,7 +419,7 @@ describe('Function Execute API Route', () => { }) it('prevalidates all sandbox output destinations before writing any files', async () => { - envFlagsMock.isE2bEnabled = true + envFlagsMock.isRemoteSandboxEnabled = true mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', stdout: 'ok', @@ -463,7 +463,7 @@ describe('Function Execute API Route', () => { }) it('rejects duplicate sandbox output destinations before writing files', async () => { - envFlagsMock.isE2bEnabled = true + envFlagsMock.isRemoteSandboxEnabled = true mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', stdout: 'ok', @@ -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 + envFlagsMock.isRemoteSandboxEnabled = true mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', stdout: 'ok', @@ -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"', @@ -582,14 +582,14 @@ 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' mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', @@ -636,7 +636,7 @@ 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' mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', @@ -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', diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index f032abfa943..79ceee93129 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -16,7 +16,7 @@ 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 { executeInIsolatedVM, type IsolatedVMBrokerHandler } from '@/lib/execution/isolated-vm' @@ -1513,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.' ) } @@ -1528,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, }) @@ -1593,26 +1593,26 @@ 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.' ) } @@ -1622,9 +1622,12 @@ export const POST = withRouteHandler(async (req: NextRequest) => { // 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)." + 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)." : 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.' @@ -1639,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, }) diff --git a/apps/sim/app/api/mothership/execute/route.ts b/apps/sim/app/api/mothership/execute/route.ts index 62eb7bc22ed..9904802b644 100644 --- a/apps/sim/app/api/mothership/execute/route.ts +++ b/apps/sim/app/api/mothership/execute/route.ts @@ -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, @@ -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 diff --git a/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts b/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts index eef2c1b9af0..da54edf8957 100644 --- a/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts +++ b/apps/sim/app/api/workspaces/[id]/files/[fileId]/compiled-check/route.ts @@ -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' @@ -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) { diff --git a/apps/sim/lib/copilot/chat/payload.ts b/apps/sim/lib/copilot/chat/payload.ts index 1ca42814edf..f02b655fdb5 100644 --- a/apps/sim/lib/copilot/chat/payload.ts +++ b/apps/sim/lib/copilot/chat/payload.ts @@ -14,7 +14,7 @@ import { getToolEntry } from '@/lib/copilot/tool-executor/router' import { getCopilotToolDescription } from '@/lib/copilot/tools/descriptions' import { encodeVfsSegment } from '@/lib/copilot/vfs/path-utils' import type { BlockVisibilityState } from '@/lib/core/config/block-visibility' -import { isE2BDocEnabled, isHosted } from '@/lib/core/config/env-flags' +import { isDocSandboxEnabled, isHosted } from '@/lib/core/config/env-flags' import { trackChatUpload } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { buildArchiveExtractGuidance, isArchiveFileName } from '@/lib/uploads/utils/file-utils' import { stripVersionSuffix } from '@/tools/utils' @@ -431,7 +431,7 @@ export async function buildCopilotRequestPayload( : {}), // Tell the copilot file subagent which document toolchain to write. Emitted // only in Python mode so the JS path sends no new field (Go defaults to js). - ...(isE2BDocEnabled ? { docCompiler: 'python' } : {}), + ...(isDocSandboxEnabled ? { docCompiler: 'python' } : {}), isHosted, } } diff --git a/apps/sim/lib/copilot/tools/server/files/doc-compile.ts b/apps/sim/lib/copilot/tools/server/files/doc-compile.ts index c136450970c..5f2bf0b7e22 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-compile.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-compile.ts @@ -1,7 +1,7 @@ import { createLogger } from '@sim/logger' import { sha256Hex } from '@sim/security/hash' import { getErrorMessage } from '@sim/utils/errors' -import { isE2BDocEnabled } from '@/lib/core/config/env-flags' +import { isDocSandboxEnabled } from '@/lib/core/config/env-flags' import { isFeatureEnabled } from '@/lib/core/config/feature-flags' import { CodeLanguage } from '@/lib/execution/languages' import { @@ -61,7 +61,7 @@ export interface E2BDocFormat { /** * Resolves the E2B doc format + engine for a filename, or null for non-docs. * pptx/docx → node, pdf/xlsx → python. Only meaningful when the E2B doc sandbox - * is enabled; callers gate on isE2BDocEnabled before using this. + * is enabled; callers gate on isDocSandboxEnabled before using this. */ export async function getE2BDocFormat(fileName: string): Promise { const l = fileName.toLowerCase() @@ -536,7 +536,7 @@ export async function resolveServableDocBytes(args: { if (stored) { return { buffer: stored.buffer, contentType: stored.contentType } } - if (isE2BDocEnabled && (await getE2BDocFormat(fileName))) { + if (isDocSandboxEnabled && (await getE2BDocFormat(fileName))) { throw new DocCompileUserError('Document is still being generated') } } diff --git a/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts b/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts index bdac8e0e129..14542d6f584 100644 --- a/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts +++ b/apps/sim/lib/copilot/tools/server/files/doc-servable.test.ts @@ -53,7 +53,7 @@ afterAll(resetEnvFlagsMock) describe('resolveServableDocBytes', () => { beforeEach(() => { vi.clearAllMocks() - setEnvFlags({ isE2BDocEnabled: true }) + setEnvFlags({ isDocSandboxEnabled: true }) betaFlag.value = false }) @@ -90,7 +90,7 @@ describe('resolveServableDocBytes', () => { it('throws DocCompileUserError when a generated doc artifact is not ready (E2B regime)', async () => { mockLoadCompiledDoc.mockResolvedValue(null) - setEnvFlags({ isE2BDocEnabled: true }) + setEnvFlags({ isDocSandboxEnabled: true }) await expect( resolveServableDocBytes({ @@ -105,7 +105,7 @@ describe('resolveServableDocBytes', () => { it('compiles via the sandbox when E2B is disabled and no artifact is stored', async () => { mockLoadCompiledDoc.mockResolvedValue(null) - setEnvFlags({ isE2BDocEnabled: false }) + setEnvFlags({ isDocSandboxEnabled: false }) const compiled = Buffer.from('%PDF-isolated-vm-binary') mockRunSandboxTask.mockResolvedValue(compiled) @@ -150,7 +150,7 @@ describe('resolveServableDocBytes', () => { it('throws when a generated XLSX artifact is not ready (E2B + mothership-beta enabled)', async () => { mockLoadCompiledDoc.mockResolvedValue(null) - setEnvFlags({ isE2BDocEnabled: true }) + setEnvFlags({ isDocSandboxEnabled: true }) betaFlag.value = true await expect( diff --git a/apps/sim/lib/copilot/tools/server/files/edit-content.ts b/apps/sim/lib/copilot/tools/server/files/edit-content.ts index ffc12f6721e..f854f670853 100644 --- a/apps/sim/lib/copilot/tools/server/files/edit-content.ts +++ b/apps/sim/lib/copilot/tools/server/files/edit-content.ts @@ -5,7 +5,7 @@ import { type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' -import { isE2BDocEnabled } from '@/lib/core/config/env-flags' +import { isDocSandboxEnabled } from '@/lib/core/config/env-flags' import { updateWorkspaceFileContent } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { getE2BDocFormat } from './doc-compile' import { buildEmbeddedImageRefWarning } from './embedded-image-refs' @@ -71,7 +71,7 @@ export const editContentServerTool: BaseServerTool { const { source, fileName, workspaceId, ownerKey, signal, fallbackMime } = args const docInfo = getDocumentFormatInfo(fileName) - const e2bFmt = isE2BDocEnabled ? await getE2BDocFormat(fileName) : null + const e2bFmt = isDocSandboxEnabled ? await getE2BDocFormat(fileName) : null if (!e2bFmt && fileName.toLowerCase().endsWith('.xlsx')) { return { ok: false, - message: isE2BDocEnabled + message: isDocSandboxEnabled ? 'Excel (.xlsx) generation is currently behind the mothership-beta feature flag and is not available.' : 'Excel (.xlsx) generation requires the E2B document sandbox, which is not enabled in this environment.', } diff --git a/apps/sim/lib/copilot/vfs/workspace-vfs.ts b/apps/sim/lib/copilot/vfs/workspace-vfs.ts index 2e07211fc51..cf3d48816e8 100644 --- a/apps/sim/lib/copilot/vfs/workspace-vfs.ts +++ b/apps/sim/lib/copilot/vfs/workspace-vfs.ts @@ -98,7 +98,7 @@ import { workspacePlansBackingFolderPath, } from '@/lib/copilot/vfs/workflow-aliases' import type { BlockVisibilityState } from '@/lib/core/config/block-visibility' -import { isE2BDocEnabled, isHosted } from '@/lib/core/config/env-flags' +import { isDocSandboxEnabled, isHosted } from '@/lib/core/config/env-flags' import { isFeatureEnabled } from '@/lib/core/config/feature-flags' import { getAccessibleEnvCredentials, @@ -935,7 +935,7 @@ export class WorkspaceVFS { totalLines: 1, } } - if (isE2BDocEnabled && (await getE2BDocFormat(record.name))) { + if (isDocSandboxEnabled && (await getE2BDocFormat(record.name))) { bin = ( await compileDoc({ source: code, fileName: record.name, workspaceId: this._workspaceId }) ).buffer @@ -988,7 +988,7 @@ export class WorkspaceVFS { record = await this.resolveWorkspaceFileForDynamicRead(path, 'compiled') if (!record) return null const ext = record.name.split('.').pop()?.toLowerCase() ?? '' - const e2bFmt = isE2BDocEnabled ? await getE2BDocFormat(record.name) : null + const e2bFmt = isDocSandboxEnabled ? await getE2BDocFormat(record.name) : null const taskId = BINARY_DOC_TASKS[ext] if (!e2bFmt && !taskId) return null @@ -1114,7 +1114,7 @@ export class WorkspaceVFS { } const extractMatch = /^files\/.+\/extract$/.test(path) - if (extractMatch && isE2BDocEnabled) { + if (extractMatch && isDocSandboxEnabled) { let record: WorkspaceFileRecord | null = null try { record = await this.resolveWorkspaceFileForDynamicRead(path, 'extract') @@ -1183,7 +1183,7 @@ export class WorkspaceVFS { record = await this.resolveWorkspaceFileForDynamicRead(path, 'compiled-check') if (!record) return null const ext = record.name.split('.').pop()?.toLowerCase() ?? '' - const e2bFmt = isE2BDocEnabled ? await getE2BDocFormat(record.name) : null + const e2bFmt = isDocSandboxEnabled ? await getE2BDocFormat(record.name) : null const taskId = BINARY_DOC_TASKS[ext] const isMermaidFile = ext === 'mmd' || ext === 'mermaid' if (!e2bFmt && !taskId && !isMermaidFile) return null diff --git a/apps/sim/lib/core/config/env-flags.ts b/apps/sim/lib/core/config/env-flags.ts index eaeeaabcac8..4df5b046c36 100644 --- a/apps/sim/lib/core/config/env-flags.ts +++ b/apps/sim/lib/core/config/env-flags.ts @@ -219,23 +219,41 @@ export const isDataDrainsEnabled = isTruthy(env.DATA_DRAINS_ENABLED) export const isForkingEnabled = isTruthy(env.FORKING_ENABLED) /** - * Is E2B enabled for remote code execution + * The selected remote sandbox provider (`SANDBOX_PROVIDER`), defaulting to E2B. + * Availability below is derived from THIS provider's credentials, so a + * Daytona-only deployment (E2B unset) still enables remote execution. */ -export const isE2bEnabled = isTruthy(env.E2B_ENABLED) +const sandboxProvider = (env.SANDBOX_PROVIDER || 'e2b').toLowerCase() /** - * Whether the E2B document-generation sandbox is enabled. + * Whether remote code/shell execution is available with the selected provider. * - * Requires E2B (with an API key) AND a dedicated doc-generation template id. - * When true, ALL four formats compile in the E2B doc sandbox: pptx/docx via Node + * E2B keeps its explicit `E2B_ENABLED` switch; Daytona is available once its API + * key is set (the shell snapshot is verified at create time, failing closed). + * Mirrors the E2B gate exactly when the provider is E2B, so existing behavior is + * unchanged. + */ +export const isRemoteSandboxEnabled = + sandboxProvider === 'daytona' ? Boolean(env.DAYTONA_API_KEY) : isTruthy(env.E2B_ENABLED) + +/** + * Whether the document-generation sandbox is available with the selected + * provider — its credential AND its dedicated doc image (E2B doc template, or + * Daytona doc snapshot). + * + * When true, ALL four formats compile in the doc sandbox: pptx/docx via Node * (pptxgenjs/docx + react-icons/sharp icons), pdf/xlsx via Python * (reportlab/openpyxl). When false, compilation stays on the JavaScript * (isolated-vm) path, byte-identical to its prior behavior (and xlsx is * unavailable). Drives both the Sim compile backend and the `docCompiler` flag * sent to the copilot file subagent so the agent's output and compiler agree. */ -export const isE2BDocEnabled = - isE2bEnabled && Boolean(env.E2B_API_KEY) && Boolean(env.MOTHERSHIP_E2B_DOC_TEMPLATE_ID) +export const isDocSandboxEnabled = + sandboxProvider === 'daytona' + ? Boolean(env.DAYTONA_API_KEY) && Boolean(env.DAYTONA_DOC_SNAPSHOT_ID) + : isTruthy(env.E2B_ENABLED) && + Boolean(env.E2B_API_KEY) && + Boolean(env.MOTHERSHIP_E2B_DOC_TEMPLATE_ID) /** * Whether Ollama is configured (OLLAMA_URL is set). diff --git a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts index 9f249ce756d..1a5a9816328 100644 --- a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts +++ b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts @@ -24,6 +24,11 @@ const { mockUploadFile, mockDownloadFile, mockDelete, + mockCreateSession, + mockExecuteSessionCommand, + mockGetSessionCommandLogs, + mockGetSessionCommand, + mockDeleteSession, } = vi.hoisted(() => ({ mockEnv: { SANDBOX_PROVIDER: 'e2b' as string | undefined, @@ -49,6 +54,11 @@ const { mockUploadFile: vi.fn(), mockDownloadFile: vi.fn(), mockDelete: vi.fn(), + mockCreateSession: vi.fn(), + mockExecuteSessionCommand: vi.fn(), + mockGetSessionCommandLogs: vi.fn(), + mockGetSessionCommand: vi.fn(), + mockDeleteSession: vi.fn(), })) vi.mock('@e2b/code-interpreter', () => ({ Sandbox: { create: mockE2BCreate } })) @@ -63,6 +73,7 @@ import { executeInSandbox, executeShellInSandbox, SIM_RESULT_PREFIX, + withPiSandbox, } from '@/lib/execution/remote-sandbox' type Provider = 'e2b' | 'daytona' @@ -102,11 +113,21 @@ beforeEach(() => { mockDaytonaCreate.mockResolvedValue({ id: 'sb_1', codeInterpreter: { runCode: mockInterpreterRunCode }, - process: { codeRun: mockProcessCodeRun, executeCommand: mockExecuteCommand }, + process: { + codeRun: mockProcessCodeRun, + executeCommand: mockExecuteCommand, + createSession: mockCreateSession, + executeSessionCommand: mockExecuteSessionCommand, + getSessionCommandLogs: mockGetSessionCommandLogs, + getSessionCommand: mockGetSessionCommand, + deleteSession: mockDeleteSession, + }, fs: { uploadFile: mockUploadFile, downloadFile: mockDownloadFile }, delete: mockDelete, }) mockExecuteCommand.mockResolvedValue({ result: '', exitCode: 0 }) + mockExecuteSessionCommand.mockResolvedValue({ cmdId: 'cmd_1' }) + mockGetSessionCommand.mockResolvedValue({ exitCode: 0 }) }) describe.each(PROVIDERS)('sandbox conformance [%s]', (provider) => { @@ -343,4 +364,37 @@ describe('provider selection', () => { ).rejects.toThrow(/DAYTONA_DOC_SNAPSHOT_ID is unset/) mockEnv.DAYTONA_DOC_SNAPSHOT_ID = original }) + + it('accumulates streamed Pi output into stdout/stderr, not just callbacks', async () => { + useProvider('daytona') + // Daytona streams via getSessionCommandLogs callbacks; the runner must also + // return the joined output so the Pi cloud flow can parse markers from stdout + // and format errors from stderr. + mockGetSessionCommandLogs.mockImplementation( + async ( + _sid: string, + _cid: string, + onStdout: (c: string) => void, + onStderr: (c: string) => void + ) => { + onStdout('__BASE_SHA__=abc123\n') + onStderr('warning: detached HEAD\n') + } + ) + mockGetSessionCommand.mockResolvedValue({ exitCode: 2 }) + + const streamedOut: string[] = [] + const result = await withPiSandbox((runner) => + runner.run('git clone ...', { + timeoutMs: 1000, + onStdout: (c) => streamedOut.push(c), + }) + ) + + expect(result.stdout).toContain('__BASE_SHA__=abc123') + expect(result.stderr).toContain('detached HEAD') + expect(result.exitCode).toBe(2) + // Callbacks still fire for live streaming. + expect(streamedOut.join('')).toContain('__BASE_SHA__=abc123') + }) }) diff --git a/apps/sim/lib/execution/remote-sandbox/daytona.ts b/apps/sim/lib/execution/remote-sandbox/daytona.ts index ae2e6b87f87..4202af7b1ee 100644 --- a/apps/sim/lib/execution/remote-sandbox/daytona.ts +++ b/apps/sim/lib/execution/remote-sandbox/daytona.ts @@ -147,14 +147,27 @@ class DaytonaSandboxHandle implements SandboxHandle { toSeconds(options.timeoutMs) ) const commandId: string = started.cmdId ?? started.commandId + // Accumulate the streamed chunks as well as forwarding them: callers read + // markers out of stdout (the Pi cloud flow parses __BASE_SHA__/__CHANGED__) + // and format failures from stderr, so returning empty strings here would + // both break marker extraction and blank out error messages even though + // the callbacks fired correctly. + let stdout = '' + let stderr = '' await this.sandbox.process.getSessionCommandLogs( sessionId, commandId, - (chunk: string) => options.onStdout?.(chunk), - (chunk: string) => options.onStderr?.(chunk) + (chunk: string) => { + stdout += chunk + options.onStdout?.(chunk) + }, + (chunk: string) => { + stderr += chunk + options.onStderr?.(chunk) + } ) const finished = await this.sandbox.process.getSessionCommand(sessionId, commandId) - return { stdout: '', stderr: '', exitCode: finished.exitCode ?? 0 } + return { stdout, stderr, exitCode: finished.exitCode ?? 0 } } catch (error) { return { stdout: '', stderr: getErrorMessage(error), exitCode: 1 } } finally { diff --git a/apps/sim/lib/mothership/inbox/executor.ts b/apps/sim/lib/mothership/inbox/executor.ts index 6886590acbb..b04b5655fe4 100644 --- a/apps/sim/lib/mothership/inbox/executor.ts +++ b/apps/sim/lib/mothership/inbox/executor.ts @@ -18,7 +18,7 @@ import { computeWorkspaceEntitlements } from '@/lib/copilot/entitlements' import { runHeadlessCopilotLifecycle } from '@/lib/copilot/request/lifecycle/headless' import { requestChatTitle } from '@/lib/copilot/request/lifecycle/start' import type { OrchestratorResult } from '@/lib/copilot/request/types' -import { isE2BDocEnabled, isHosted } from '@/lib/core/config/env-flags' +import { isDocSandboxEnabled, isHosted } from '@/lib/core/config/env-flags' import * as agentmail from '@/lib/mothership/inbox/agentmail-client' import { formatEmailAsMessage } from '@/lib/mothership/inbox/format' import { sendInboxResponse } from '@/lib/mothership/inbox/response' @@ -242,7 +242,7 @@ export async function executeInboxTask(taskId: string): Promise { messageId: userMessageId, isHosted, workspaceContext, - ...(isE2BDocEnabled ? { docCompiler: 'python' } : {}), + ...(isDocSandboxEnabled ? { docCompiler: 'python' } : {}), ...(integrationTools.length > 0 ? { integrationTools } : {}), ...(userPermission ? { userPermission } : {}), ...(entitlements.length > 0 ? { entitlements } : {}), diff --git a/packages/testing/src/mocks/env-flags.mock.ts b/packages/testing/src/mocks/env-flags.mock.ts index ee6557a4245..5800e2860ac 100644 --- a/packages/testing/src/mocks/env-flags.mock.ts +++ b/packages/testing/src/mocks/env-flags.mock.ts @@ -31,8 +31,8 @@ export interface EnvFlagsMockState { isDataRetentionEnabled: boolean isDataDrainsEnabled: boolean isForkingEnabled: boolean - isE2bEnabled: boolean - isE2BDocEnabled: boolean + isRemoteSandboxEnabled: boolean + isDocSandboxEnabled: boolean isOllamaConfigured: boolean isAzureConfigured: boolean isCohereConfigured: boolean @@ -71,8 +71,8 @@ const defaultEnvFlagsState: EnvFlagsMockState = { isDataRetentionEnabled: false, isDataDrainsEnabled: false, isForkingEnabled: false, - isE2bEnabled: false, - isE2BDocEnabled: false, + isRemoteSandboxEnabled: false, + isDocSandboxEnabled: false, isOllamaConfigured: false, isAzureConfigured: false, isCohereConfigured: false, From bf6991c34f55b31a3cb76b39a1df4422900d9e4b Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 23 Jul 2026 13:11:37 -0700 Subject: [PATCH 4/8] fix(sandbox): resolve SANDBOX_PROVIDER case-insensitively MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the round-2 review on #5860. env-flags lowercased SANDBOX_PROVIDER for the availability gate, but resolveProvider looked up the raw value in a lowercase-keyed map — so 'Daytona' passed the gate then threw Unknown SANDBOX_PROVIDER at create. resolveProvider now normalizes casing identically. --- .../lib/execution/remote-sandbox/conformance.test.ts | 10 ++++++++++ apps/sim/lib/execution/remote-sandbox/index.ts | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts index 1a5a9816328..0f88344e947 100644 --- a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts +++ b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts @@ -335,6 +335,16 @@ describe('provider selection', () => { ).rejects.toThrow(/Unknown SANDBOX_PROVIDER "modal"/) }) + it('resolves SANDBOX_PROVIDER case-insensitively', async () => { + mockEnv.SANDBOX_PROVIDER = 'Daytona' + stubCodeRun('daytona', `${SIM_RESULT_PREFIX}null`) + + await executeInSandbox({ code: 'x', language: CodeLanguage.Python, timeoutMs: 1000 }) + + expect(mockDaytonaCreate).toHaveBeenCalledTimes(1) + expect(mockE2BCreate).not.toHaveBeenCalled() + }) + it('binds language at create time so JS never runs through the Python toolbox', async () => { useProvider('daytona') mockProcessCodeRun.mockResolvedValue({ result: `${SIM_RESULT_PREFIX}null`, exitCode: 0 }) diff --git a/apps/sim/lib/execution/remote-sandbox/index.ts b/apps/sim/lib/execution/remote-sandbox/index.ts index c076180260b..68f8ae065a8 100644 --- a/apps/sim/lib/execution/remote-sandbox/index.ts +++ b/apps/sim/lib/execution/remote-sandbox/index.ts @@ -48,12 +48,15 @@ const DEFAULT_PROVIDER: SandboxProviderId = 'e2b' * `SANDBOX_PROVIDER` and redeploy; in-flight executions are unaffected. */ function resolveProvider(): SandboxProvider { - const configured = env.SANDBOX_PROVIDER + // Normalize casing identically to env-flags' availability gate — otherwise a + // value like `Daytona` would pass the gate (which lowercases) but miss this + // lowercase-keyed map and throw at create time. + const configured = env.SANDBOX_PROVIDER?.toLowerCase() if (!configured) return PROVIDERS[DEFAULT_PROVIDER] const provider = PROVIDERS[configured as SandboxProviderId] if (!provider) { throw new Error( - `Unknown SANDBOX_PROVIDER "${configured}" (expected one of: ${Object.keys(PROVIDERS).join(', ')})` + `Unknown SANDBOX_PROVIDER "${env.SANDBOX_PROVIDER}" (expected one of: ${Object.keys(PROVIDERS).join(', ')})` ) } return provider From 60002aed345bf18e938c03867307400d967a3995 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 23 Jul 2026 13:39:09 -0700 Subject: [PATCH 5/8] fix(sandbox): use getErrorMessage in build/verify scripts check:utils flagged the inline `error instanceof Error ? error.message : ...` pattern in the two new scripts. Use getErrorMessage from @sim/utils/errors, matching the repo convention the check enforces. --- apps/sim/scripts/build-pi-daytona-snapshot.ts | 3 ++- apps/sim/scripts/verify-sandbox-parity.ts | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/sim/scripts/build-pi-daytona-snapshot.ts b/apps/sim/scripts/build-pi-daytona-snapshot.ts index 3c2c3d68c2e..57846b155ea 100644 --- a/apps/sim/scripts/build-pi-daytona-snapshot.ts +++ b/apps/sim/scripts/build-pi-daytona-snapshot.ts @@ -24,6 +24,7 @@ */ import { Daytona, Image } from '@daytonaio/sdk' +import { getErrorMessage } from '@sim/utils/errors' import { PI_APT, PI_NODE_MAJOR, @@ -96,6 +97,6 @@ async function main() { } main().catch((error: unknown) => { - console.error('Build failed:', error instanceof Error ? error.message : error) + console.error('Build failed:', getErrorMessage(error)) process.exit(1) }) diff --git a/apps/sim/scripts/verify-sandbox-parity.ts b/apps/sim/scripts/verify-sandbox-parity.ts index 74e02f4fbe6..1820a291b8f 100644 --- a/apps/sim/scripts/verify-sandbox-parity.ts +++ b/apps/sim/scripts/verify-sandbox-parity.ts @@ -23,6 +23,7 @@ * Exits non-zero if any case fails, so it can be wired to a schedule later. */ +import { getErrorMessage } from '@sim/utils/errors' import { CodeLanguage } from '@/lib/execution/languages' import { executeInSandbox, executeShellInSandbox } from '@/lib/execution/remote-sandbox' @@ -179,7 +180,7 @@ async function main() { } } catch (error) { console.log(`FAIL ${testCase.name}`) - console.log(` threw: ${error instanceof Error ? error.message : String(error)}`) + console.log(` threw: ${getErrorMessage(error)}`) failed++ } } @@ -192,6 +193,6 @@ async function main() { } main().catch((error: unknown) => { - console.error('harness error:', error instanceof Error ? error.message : error) + console.error('harness error:', getErrorMessage(error)) process.exit(1) }) From 27e91a4d5df41541fbd12912fbec56ed7dfd63e0 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 23 Jul 2026 13:50:58 -0700 Subject: [PATCH 6/8] fix(sandbox): fall back to stdout for Daytona failure text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Daytona merges both streams into stdout and returns an empty stderr, but the shell-error, base64-export, and URL-mount error builders read only result.stderr — so Daytona failures surfaced a generic 'Process exited with code N' / 'base64 failed' / 'curl exited N' instead of the real command output that the API and agents rely on. Fall back to stdout before the generic message (provider-agnostic: E2B still populates stderr). Strengthens the shell-error conformance test to assert the real output surfaces. --- .../execution/remote-sandbox/conformance.test.ts | 11 +++++++---- apps/sim/lib/execution/remote-sandbox/index.ts | 13 ++++++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts index 0f88344e947..8ce5336c23e 100644 --- a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts +++ b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts @@ -266,17 +266,20 @@ describe.each(PROVIDERS)('sandbox conformance [%s]', (provider) => { expect(res.error).toBeUndefined() }) - it('surfaces a non-zero shell exit as an error', async () => { + it('surfaces the real command output as the error, not a generic message', async () => { + // E2B populates stderr; Daytona merges both streams into stdout with an empty + // stderr, so the error must fall back to stdout or Daytona failures would read + // as a generic "Process exited with code N". if (provider === 'e2b') { - mockE2BCommandsRun.mockResolvedValue({ stdout: '', stderr: 'bad', exitCode: 3 }) + mockE2BCommandsRun.mockResolvedValue({ stdout: '', stderr: 'boom detail', exitCode: 3 }) } else { - mockExecuteCommand.mockResolvedValue({ result: 'bad', exitCode: 3 }) + mockExecuteCommand.mockResolvedValue({ result: 'boom detail', exitCode: 3 }) } const res = await executeShellInSandbox({ code: 'false', envs: {}, timeoutMs: 1000 }) expect(res.result).toBeNull() - expect(res.error).toBeTruthy() + expect(res.error).toContain('boom detail') }) it('exports binary output files as base64', async () => { diff --git a/apps/sim/lib/execution/remote-sandbox/index.ts b/apps/sim/lib/execution/remote-sandbox/index.ts index 68f8ae065a8..c9e3977eb03 100644 --- a/apps/sim/lib/execution/remote-sandbox/index.ts +++ b/apps/sim/lib/execution/remote-sandbox/index.ts @@ -109,8 +109,9 @@ async function writeSandboxInputs( // checked explicitly — a silently-missing mount is exactly what this guard // exists to prevent. if (result.exitCode !== 0) { + // Daytona merges streams into stdout, so fall back to it for the real error. throw new Error( - `Failed to fetch mounted file into sandbox at ${file.path}: ${result.stderr || `curl exited ${result.exitCode}`}` + `Failed to fetch mounted file into sandbox at ${file.path}: ${result.stderr || result.stdout || `curl exited ${result.exitCode}`}` ) } fetchedByUrl.push(file.path) @@ -220,7 +221,10 @@ async function readSandboxOutputFile( timeoutMs: 120_000, rootUser: options?.rootUser, }) - if (b64Result.exitCode !== 0) throw new Error(b64Result.stderr || 'base64 failed') + // Daytona merges streams into stdout, so fall back to it for the real error. + if (b64Result.exitCode !== 0) { + throw new Error(b64Result.stderr || b64Result.stdout || 'base64 failed') + } return b64Result.stdout } return await sandbox.readFile(outputSandboxPath) @@ -353,7 +357,10 @@ export async function executeShellInSandbox( const stdout = [result.stdout, result.stderr].filter(Boolean).join('\n') if (result.exitCode !== 0) { - const errorMessage = result.stderr || `Process exited with code ${result.exitCode}` + // Daytona merges both streams into stdout (stderr is always empty), so fall + // back to stdout for the real command output before the generic message. + const errorMessage = + result.stderr || result.stdout || `Process exited with code ${result.exitCode}` logger.error('Sandbox shell execution error', { sandboxId, exitCode: result.exitCode, From 7b693c14db4a4fd126b1d5265aa3369429847d7c Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 23 Jul 2026 14:01:15 -0700 Subject: [PATCH 7/8] fix(sandbox): enforce timeout on Daytona streaming; drop leftover E2B copy - Daytona's streaming path (Pi) started the command with runAsync:true and then awaited getSessionCommandLogs with no bound, so a hung command never timed out the way E2B's commands.run({ timeoutMs }) does. Race the log stream against the timeout; on expiry return exit 124 with the accumulated output, and the finally's deleteSession terminates the still-running command. - Two user-facing strings still named E2B after the provider-neutral rename (the isolated-vm sandboxPath remediation and the disabled-xlsx message). Made both provider-neutral. Adds a streaming-timeout conformance test. --- apps/sim/app/api/function/execute/route.ts | 8 +-- .../tools/server/files/workspace-file.ts | 2 +- .../remote-sandbox/conformance.test.ts | 16 ++++++ .../lib/execution/remote-sandbox/daytona.ts | 54 ++++++++++++++----- 4 files changed, 61 insertions(+), 19 deletions(-) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 79ceee93129..85a0ea43bde 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -1616,12 +1616,12 @@ export const POST = withRouteHandler(async (req: NextRequest) => { ) } - // 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. + // when no remote sandbox is enabled or the call is a custom tool. if ( !useRemoteSandbox && (outputSandboxPaths.length > 0 || outputSandboxPath || _sandboxFiles?.length) @@ -1630,7 +1630,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { ? "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 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, diff --git a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts index e2ad23ae4f0..7ddf618b382 100644 --- a/apps/sim/lib/copilot/tools/server/files/workspace-file.ts +++ b/apps/sim/lib/copilot/tools/server/files/workspace-file.ts @@ -211,7 +211,7 @@ export async function compileDocForWrite(args: { ok: false, message: isDocSandboxEnabled ? 'Excel (.xlsx) generation is currently behind the mothership-beta feature flag and is not available.' - : 'Excel (.xlsx) generation requires the E2B document sandbox, which is not enabled in this environment.', + : 'Excel (.xlsx) generation requires the document sandbox, which is not enabled in this environment.', } } diff --git a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts index 8ce5336c23e..615b7733451 100644 --- a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts +++ b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts @@ -410,4 +410,20 @@ describe('provider selection', () => { // Callbacks still fire for live streaming. expect(streamedOut.join('')).toContain('__BASE_SHA__=abc123') }) + + it('enforces the timeout on a hung streaming command', async () => { + useProvider('daytona') + // runAsync returns immediately, so a never-resolving log stream must be + // bounded by the timeout rather than awaited forever. + mockGetSessionCommandLogs.mockReturnValue(new Promise(() => {})) + + const result = await withPiSandbox((runner) => + runner.run('hang forever', { timeoutMs: 20, onStdout: () => {} }) + ) + + expect(result.exitCode).toBe(124) + expect(result.stderr).toContain('timed out') + // The session is deleted to terminate the still-running command. + expect(mockDeleteSession).toHaveBeenCalled() + }) }) diff --git a/apps/sim/lib/execution/remote-sandbox/daytona.ts b/apps/sim/lib/execution/remote-sandbox/daytona.ts index 4202af7b1ee..f7f93f170ff 100644 --- a/apps/sim/lib/execution/remote-sandbox/daytona.ts +++ b/apps/sim/lib/execution/remote-sandbox/daytona.ts @@ -130,6 +130,10 @@ class DaytonaSandboxHandle implements SandboxHandle { ): Promise { const sessionId = `sim-${generateShortId(12)}` await this.sandbox.process.createSession(sessionId) + // Declared outside the try so the catch can return whatever streamed before a + // failure, rather than blanking the output. + let stdout = '' + let stderr = '' try { let script = command if (options.envs && Object.keys(options.envs).length > 0) { @@ -152,24 +156,46 @@ class DaytonaSandboxHandle implements SandboxHandle { // and format failures from stderr, so returning empty strings here would // both break marker extraction and blank out error messages even though // the callbacks fired correctly. - let stdout = '' - let stderr = '' - await this.sandbox.process.getSessionCommandLogs( - sessionId, - commandId, - (chunk: string) => { - stdout += chunk - options.onStdout?.(chunk) - }, - (chunk: string) => { - stderr += chunk - options.onStderr?.(chunk) + const streamed = this.sandbox.process + .getSessionCommandLogs( + sessionId, + commandId, + (chunk: string) => { + stdout += chunk + options.onStdout?.(chunk) + }, + (chunk: string) => { + stderr += chunk + options.onStderr?.(chunk) + } + ) + .then(() => 'done' as const) + + // `runAsync: true` returns immediately, so the timeout must be enforced here + // — otherwise a hung command streams forever, unlike E2B's commands.run + // which honors timeoutMs for the whole run. On timeout, the finally's + // deleteSession terminates the still-running command. + let timer: ReturnType | undefined + const timedOut = new Promise<'timeout'>((resolve) => { + timer = setTimeout(() => resolve('timeout'), options.timeoutMs) + }) + try { + const outcome = await Promise.race([streamed, timedOut]) + if (outcome === 'timeout') { + return { + stdout, + stderr: stderr || `Command timed out after ${options.timeoutMs}ms`, + exitCode: 124, + } } - ) + } finally { + if (timer) clearTimeout(timer) + } + const finished = await this.sandbox.process.getSessionCommand(sessionId, commandId) return { stdout, stderr, exitCode: finished.exitCode ?? 0 } } catch (error) { - return { stdout: '', stderr: getErrorMessage(error), exitCode: 1 } + return { stdout, stderr, exitCode: 1 } } finally { try { await this.sandbox.process.deleteSession(sessionId) From d09765b75d444053199b40889505a1be374763fd Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Thu, 23 Jul 2026 14:08:33 -0700 Subject: [PATCH 8/8] fix(sandbox): handle orphaned stream promise + preserve error detail Two regressions from the previous timeout fix: - When the timeout won the race, the abandoned getSessionCommandLogs promise would reject on deleteSession with no handler (unhandledRejection). Attach a .catch that records the error and yields an 'error' outcome, so a late rejection is always handled. - The streaming catch dropped the thrown error, so failures before any chunks (env write, executeSessionCommand, missing cmdId) surfaced as empty output. Fall back to getErrorMessage(error) when nothing streamed. Adds conformance tests for the stream-reject and start-throw paths. --- .../remote-sandbox/conformance.test.ts | 26 ++++++++++++++++ .../lib/execution/remote-sandbox/daytona.ts | 31 +++++++++++++------ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts index 615b7733451..1e8edb89192 100644 --- a/apps/sim/lib/execution/remote-sandbox/conformance.test.ts +++ b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts @@ -426,4 +426,30 @@ describe('provider selection', () => { // The session is deleted to terminate the still-running command. expect(mockDeleteSession).toHaveBeenCalled() }) + + it('surfaces the thrown error when the stream rejects', async () => { + useProvider('daytona') + // A failure with no chunks streamed must still carry the error detail, not an + // empty/opaque message. + mockGetSessionCommandLogs.mockRejectedValue(new Error('session died')) + + const result = await withPiSandbox((runner) => + runner.run('git clone ...', { timeoutMs: 1000, onStdout: () => {} }) + ) + + expect(result.exitCode).toBe(1) + expect(result.stderr).toContain('session died') + }) + + it('surfaces the error when starting the streamed command throws', async () => { + useProvider('daytona') + mockExecuteSessionCommand.mockRejectedValue(new Error('start failed')) + + const result = await withPiSandbox((runner) => + runner.run('git clone ...', { timeoutMs: 1000, onStdout: () => {} }) + ) + + expect(result.exitCode).toBe(1) + expect(result.stderr).toContain('start failed') + }) }) diff --git a/apps/sim/lib/execution/remote-sandbox/daytona.ts b/apps/sim/lib/execution/remote-sandbox/daytona.ts index f7f93f170ff..97295b1b90e 100644 --- a/apps/sim/lib/execution/remote-sandbox/daytona.ts +++ b/apps/sim/lib/execution/remote-sandbox/daytona.ts @@ -156,6 +156,11 @@ class DaytonaSandboxHandle implements SandboxHandle { // and format failures from stderr, so returning empty strings here would // both break marker extraction and blank out error messages even though // the callbacks fired correctly. + // The `.catch` keeps its own reference to the stream's outcome AND ensures a + // late rejection is always handled — when the timeout wins the race, this + // promise is abandoned and deleteSession (finally) tears the session down, + // which rejects it; without the handler that would be an unhandledRejection. + let streamError: unknown const streamed = this.sandbox.process .getSessionCommandLogs( sessionId, @@ -170,6 +175,10 @@ class DaytonaSandboxHandle implements SandboxHandle { } ) .then(() => 'done' as const) + .catch((error: unknown) => { + streamError = error + return 'error' as const + }) // `runAsync: true` returns immediately, so the timeout must be enforced here // — otherwise a hung command streams forever, unlike E2B's commands.run @@ -179,23 +188,27 @@ class DaytonaSandboxHandle implements SandboxHandle { const timedOut = new Promise<'timeout'>((resolve) => { timer = setTimeout(() => resolve('timeout'), options.timeoutMs) }) + let outcome: 'done' | 'error' | 'timeout' try { - const outcome = await Promise.race([streamed, timedOut]) - if (outcome === 'timeout') { - return { - stdout, - stderr: stderr || `Command timed out after ${options.timeoutMs}ms`, - exitCode: 124, - } - } + outcome = await Promise.race([streamed, timedOut]) } finally { if (timer) clearTimeout(timer) } + if (outcome === 'timeout') { + return { + stdout, + stderr: stderr || `Command timed out after ${options.timeoutMs}ms`, + exitCode: 124, + } + } + if (outcome === 'error') { + return { stdout, stderr: stderr || getErrorMessage(streamError), exitCode: 1 } + } const finished = await this.sandbox.process.getSessionCommand(sessionId, commandId) return { stdout, stderr, exitCode: finished.exitCode ?? 0 } } catch (error) { - return { stdout, stderr, exitCode: 1 } + return { stdout, stderr: stderr || getErrorMessage(error), exitCode: 1 } } finally { try { await this.sandbox.process.deleteSession(sessionId)