diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index 4b5c2194584..b240a68a099 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__=', })) @@ -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, @@ -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', @@ -351,8 +351,8 @@ describe('Function Execute API Route', () => { }) it('exports multiple declared sandbox output files', async () => { - envFlagsMock.isE2bEnabled = true - mockExecuteInE2B.mockResolvedValueOnce({ + envFlagsMock.isRemoteSandboxEnabled = true + mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', stdout: 'ok', sandboxId: 'sandbox-123', @@ -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'], }) @@ -419,8 +419,8 @@ describe('Function Execute API Route', () => { }) it('prevalidates all sandbox output destinations before writing any files', async () => { - envFlagsMock.isE2bEnabled = true - mockExecuteInE2B.mockResolvedValueOnce({ + envFlagsMock.isRemoteSandboxEnabled = true + mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', stdout: 'ok', sandboxId: 'sandbox-123', @@ -463,8 +463,8 @@ describe('Function Execute API Route', () => { }) it('rejects duplicate sandbox output destinations before writing files', async () => { - envFlagsMock.isE2bEnabled = true - mockExecuteInE2B.mockResolvedValueOnce({ + envFlagsMock.isRemoteSandboxEnabled = true + mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', stdout: 'ok', sandboxId: 'sandbox-123', @@ -508,8 +508,8 @@ describe('Function Execute API Route', () => { }) it('returns a targeted error when a declared sandbox output is missing', async () => { - envFlagsMock.isE2bEnabled = true - mockExecuteInE2B.mockResolvedValueOnce({ + envFlagsMock.isRemoteSandboxEnabled = true + mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', stdout: 'ok', sandboxId: 'sandbox-123', @@ -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"', @@ -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() }) @@ -582,16 +582,16 @@ describe('Function Execute API Route', () => { expect(response.status).toBe(422) expect(data.success).toBe(false) - // E2B is disabled in this test, so the remediation must name that cause - // instead of suggesting python (which would also fail without E2B). - expect(data.error).toContain('E2B is not enabled') + // No remote sandbox is enabled in this test, so the remediation must name + // that cause instead of suggesting python (which would also fail without one). + expect(data.error).toContain('No remote code sandbox is enabled') expect(mockExecuteInIsolatedVM).not.toHaveBeenCalled() }) it('flags an overwrite export whose bytes are identical to the current file content as unchanged', async () => { - envFlagsMock.isE2bEnabled = true + envFlagsMock.isRemoteSandboxEnabled = true const staleContent = '# doc\nunchanged mounted content\n' - mockExecuteInE2B.mockResolvedValueOnce({ + mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', stdout: 'ok', sandboxId: 'sandbox-123', @@ -636,9 +636,9 @@ describe('Function Execute API Route', () => { }) it('reports size, previousSize, and sha256 receipts on a successful overwrite export', async () => { - envFlagsMock.isE2bEnabled = true + envFlagsMock.isRemoteSandboxEnabled = true const newContent = '# doc\nnew content\n' - mockExecuteInE2B.mockResolvedValueOnce({ + mockExecuteInSandbox.mockResolvedValueOnce({ result: 'done', stdout: 'ok', sandboxId: 'sandbox-123', @@ -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__))") }) @@ -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 20568fd2010..85a0ea43bde 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -16,10 +16,9 @@ import { validateWorkspaceFileWriteTarget, writeWorkspaceFileByPath, } from '@/lib/copilot/vfs/resource-writer' -import { isE2bEnabled } from '@/lib/core/config/env-flags' +import { isRemoteSandboxEnabled } from '@/lib/core/config/env-flags' import { generateRequestId } from '@/lib/core/utils/request' import { withRouteHandler } from '@/lib/core/utils/with-route-handler' -import { executeInE2B, executeShellInE2B, SIM_RESULT_PREFIX } from '@/lib/execution/e2b' import { executeInIsolatedVM, type IsolatedVMBrokerHandler } from '@/lib/execution/isolated-vm' import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages' import { recordMaterializedAccessKeys } from '@/lib/execution/payloads/access-keys' @@ -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, @@ -1509,9 +1513,9 @@ export const POST = withRouteHandler(async (req: NextRequest) => { } if (lang === CodeLanguage.Shell) { - if (!isE2bEnabled) { + if (!isRemoteSandboxEnabled) { throw new Error( - 'Shell execution requires E2B to be enabled. Please contact your administrator to enable E2B.' + 'Shell execution requires a remote code sandbox to be enabled. Please contact your administrator to enable it.' ) } @@ -1524,7 +1528,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { } logger.info(`[${requestId}] E2B shell execution`, { - enabled: isE2bEnabled, + enabled: isRemoteSandboxEnabled, hasApiKey: Boolean(process.env.E2B_API_KEY), envVarCount: Object.keys(shellEnvs).length, }) @@ -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, @@ -1589,41 +1593,44 @@ export const POST = withRouteHandler(async (req: NextRequest) => { ) } - if (lang === CodeLanguage.Python && !isE2bEnabled) { + if (lang === CodeLanguage.Python && !isRemoteSandboxEnabled) { throw new Error( - 'Python execution requires E2B to be enabled. Please contact your administrator to enable E2B, or use JavaScript instead.' + 'Python execution requires a remote code sandbox to be enabled. Please contact your administrator to enable it, or use JavaScript instead.' ) } - if (lang === CodeLanguage.JavaScript && hasImports && !isE2bEnabled) { + if (lang === CodeLanguage.JavaScript && hasImports && !isRemoteSandboxEnabled) { throw new Error( - 'JavaScript code with import statements requires E2B to be enabled. Please remove the import statements, or contact your administrator to enable E2B.' + 'JavaScript code with import statements requires a remote code sandbox to be enabled. Please remove the import statements, or contact your administrator to enable it.' ) } - const useE2B = - isE2bEnabled && + const useRemoteSandbox = + isRemoteSandboxEnabled && !isCustomTool && (lang === CodeLanguage.Python || (lang === CodeLanguage.JavaScript && hasImports)) - if (useE2B && containsLargeValueRef(contextVariables)) { + if (useRemoteSandbox && containsLargeValueRef(contextVariables)) { throw new Error( - 'Large execution values require the JavaScript isolated-vm runtime. Remove imports, select a nested field, or read the value in a JavaScript function without E2B.' + 'Large execution values require the JavaScript isolated-vm runtime. Remove imports, select a nested field, or read the value in a JavaScript function without a remote sandbox.' ) } - // Sandbox file mounts and sandboxPath exports only exist in the E2B - // runtime; isolated-vm has no filesystem. Silently dropping a declared + // Sandbox file mounts and sandboxPath exports only exist in the remote + // sandbox runtime; isolated-vm has no filesystem. Silently dropping a declared // sandbox input/output here produced "export succeeded" responses with // zero bytes written, so refuse the call instead. The remediation depends // on WHY this call runs in isolated-vm — "switch to python" is a dead end - // when E2B is disabled or the call is a custom tool. - if (!useE2B && (outputSandboxPaths.length > 0 || outputSandboxPath || _sandboxFiles?.length)) { - const remediation = !isE2bEnabled - ? "E2B is not enabled on this deployment, so there is no sandbox filesystem for any language. Pass input data via params and return output as the code's return value with outputs.files[].path (no sandboxPath)." + // when no remote sandbox is enabled or the call is a custom tool. + if ( + !useRemoteSandbox && + (outputSandboxPaths.length > 0 || outputSandboxPath || _sandboxFiles?.length) + ) { + const remediation = !isRemoteSandboxEnabled + ? "No remote code sandbox is enabled on this deployment, so there is no sandbox filesystem for any language. Pass input data via params and return output as the code's return value with outputs.files[].path (no sandboxPath)." : 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, @@ -1635,9 +1642,9 @@ export const POST = withRouteHandler(async (req: NextRequest) => { ) } - if (useE2B) { + if (useRemoteSandbox) { logger.info(`[${requestId}] E2B status`, { - enabled: isE2bEnabled, + enabled: isRemoteSandboxEnabled, hasApiKey: Boolean(process.env.E2B_API_KEY), language: lang, }) @@ -1693,7 +1700,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { error: e2bError, exportedFileContent, exportedFiles, - } = await executeInE2B({ + } = await executeInSandbox({ code: codeForE2B, language: CodeLanguage.JavaScript, timeoutMs: timeout, @@ -1781,7 +1788,7 @@ export const POST = withRouteHandler(async (req: NextRequest) => { error: e2bError, exportedFileContent, exportedFiles, - } = await executeInE2B({ + } = await executeInSandbox({ code: codeForE2B, language: CodeLanguage.Python, timeoutMs: timeout, 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/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/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.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..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,10 +1,14 @@ 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 { 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, @@ -57,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() @@ -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, @@ -532,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-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..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 @@ -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' }, @@ -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.', + : 'Excel (.xlsx) generation requires the 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/core/config/env.ts b/apps/sim/lib/core/config/env.ts index 683f4356ac8..80c9081dcc2 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -454,6 +454,15 @@ 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) + // 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) + // 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/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..1e8edb89192 --- /dev/null +++ b/apps/sim/lib/execution/remote-sandbox/conformance.test.ts @@ -0,0 +1,455 @@ +/** + * @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 { + mockEnv, + mockE2BCreate, + mockE2BRunCode, + mockE2BCommandsRun, + mockE2BFilesRead, + mockE2BFilesWrite, + mockE2BKill, + mockDaytonaCreate, + mockInterpreterRunCode, + mockProcessCodeRun, + mockExecuteCommand, + mockUploadFile, + mockDownloadFile, + mockDelete, + mockCreateSession, + mockExecuteSessionCommand, + mockGetSessionCommandLogs, + mockGetSessionCommand, + mockDeleteSession, +} = vi.hoisted(() => ({ + 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(), + 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(), + mockCreateSession: vi.fn(), + mockExecuteSessionCommand: vi.fn(), + mockGetSessionCommandLogs: vi.fn(), + mockGetSessionCommand: vi.fn(), + mockDeleteSession: 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: mockEnv })) + +import { + executeInSandbox, + executeShellInSandbox, + SIM_RESULT_PREFIX, + withPiSandbox, +} from '@/lib/execution/remote-sandbox' + +type Provider = 'e2b' | 'daytona' +const PROVIDERS: Provider[] = ['e2b', 'daytona'] + +/** Points the shared layer at one provider via the SANDBOX_PROVIDER env var. */ +function useProvider(provider: Provider) { + mockEnv.SANDBOX_PROVIDER = provider +} + +/** 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, + 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) => { + 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 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: 'boom detail', exitCode: 3 }) + } else { + mockExecuteCommand.mockResolvedValue({ result: 'boom detail', exitCode: 3 }) + } + + const res = await executeShellInSandbox({ code: 'false', envs: {}, timeoutMs: 1000 }) + + expect(res.result).toBeNull() + expect(res.error).toContain('boom detail') + }) + + 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 by SANDBOX_PROVIDER, defaulting to E2B when unset', async () => { + stubCodeRun('e2b', `${SIM_RESULT_PREFIX}null`) + stubCodeRun('daytona', `${SIM_RESULT_PREFIX}null`) + + mockEnv.SANDBOX_PROVIDER = undefined + 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('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('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 }) + + 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 original = mockEnv.DAYTONA_DOC_SNAPSHOT_ID + mockEnv.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/) + 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') + }) + + 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() + }) + + 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 new file mode 100644 index 00000000000..97295b1b90e --- /dev/null +++ b/apps/sim/lib/execution/remote-sandbox/daytona.ts @@ -0,0 +1,262 @@ +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) + // 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) { + 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 + // 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. + // 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, + commandId, + (chunk: string) => { + stdout += chunk + options.onStdout?.(chunk) + }, + (chunk: string) => { + stderr += chunk + options.onStderr?.(chunk) + } + ) + .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 + // 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) + }) + let outcome: 'done' | 'error' | 'timeout' + try { + 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: 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..c9e3977eb03 --- /dev/null +++ b/apps/sim/lib/execution/remote-sandbox/index.ts @@ -0,0 +1,454 @@ +import { createLogger } from '@sim/logger' +import { getErrorMessage } from '@sim/utils/errors' +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' +import type { + SandboxCommandResult, + SandboxExecutionRequest, + SandboxExecutionResult, + SandboxFile, + SandboxHandle, + SandboxKind, + SandboxProvider, + SandboxProviderId, + SandboxShellExecutionRequest, +} from '@/lib/execution/remote-sandbox/types' + +export type { + SandboxExecutionRequest, + SandboxExecutionResult, + SandboxFile, + SandboxShellExecutionRequest, +} from '@/lib/execution/remote-sandbox/types' + +const logger = createLogger('RemoteSandbox') + +/** + * 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 another provider + * could duplicate them. Changing providers is a config change — set + * `SANDBOX_PROVIDER` and redeploy; in-flight executions are unaffected. + */ +function resolveProvider(): SandboxProvider { + // 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 "${env.SANDBOX_PROVIDER}" (expected one of: ${Object.keys(PROVIDERS).join(', ')})` + ) + } + return provider +} + +async function createSandbox( + kind: SandboxKind, + options?: { language?: CodeLanguage } +): Promise { + const provider = 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) { + // 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 || result.stdout || `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, + }) + // 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) + } 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) { + // 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, + 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/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/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..57846b155ea --- /dev/null +++ b/apps/sim/scripts/build-pi-daytona-snapshot.ts @@ -0,0 +1,102 @@ +#!/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 { getErrorMessage } from '@sim/utils/errors' +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:', getErrorMessage(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..1820a291b8f --- /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` env var selects, and prints a pass/fail matrix. + * + * 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 (default) + * bun run apps/sim/scripts/verify-sandbox-parity.ts + * + * # 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 + * + * 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' + +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 || '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: ${getErrorMessage(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:', getErrorMessage(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=="], 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,