feat(code): updated nextjs, added webcontainer dependencies, added more complex code execution for the function block. use secure, sandboxed webcontainer and fallback to vm

This commit is contained in:
Waleed Latif
2025-03-01 14:13:21 -08:00
parent ae226e977c
commit 8171706f6c
9 changed files with 539 additions and 69 deletions
+21 -18
View File
@@ -1,6 +1,7 @@
import { useCallback, useState } from 'react'
import { v4 as uuidv4 } from 'uuid'
import { useConsoleStore } from '@/stores/console/store'
import { useExecutionStore } from '@/stores/execution/store'
import { useNotificationStore } from '@/stores/notifications/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { useWorkflowRegistry } from '@/stores/workflow/registry/store'
@@ -12,13 +13,13 @@ import { ExecutionResult } from '@/executor/types'
import { Serializer } from '@/serializer'
export function useWorkflowExecution() {
const [isExecuting, setIsExecuting] = useState(false)
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
const { blocks, edges, loops } = useWorkflowStore()
const { activeWorkflowId } = useWorkflowRegistry()
const { addNotification } = useNotificationStore()
const { toggleConsole, isOpen } = useConsoleStore()
const { getAllVariables } = useEnvironmentStore()
const { isExecuting, setIsExecuting } = useExecutionStore()
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
const persistLogs = async (logs: any[], executionId: string) => {
try {
@@ -91,7 +92,17 @@ export function useWorkflowExecution() {
const executor = new Executor(workflow, currentBlockStates, envVarValues)
const result = await executor.execute(activeWorkflowId)
// Prepare logs for persistence
// Set result and show notification immediately
setExecutionResult(result)
addNotification(
result.success ? 'console' : 'error',
result.success
? 'Workflow completed successfully'
: `Workflow execution failed: ${result.error}`,
activeWorkflowId
)
// Prepare logs for persistence (moved after notification)
const blockLogs = (result.logs || []).map((log) => ({
level: log.success ? 'info' : 'error',
message: `Block ${log.blockName || log.blockId} (${log.blockType}): ${
@@ -116,30 +127,22 @@ export function useWorkflowExecution() {
createdAt: new Date().toISOString(),
})
// Persist all logs
// Persist logs after notification
await persistLogs(blockLogs, executionId)
setExecutionResult(result)
// Show execution result notification
addNotification(
result.success ? 'console' : 'error',
result.success
? 'Workflow completed successfully'
: `Workflow execution failed: ${result.error}`,
activeWorkflowId
)
} catch (error: any) {
console.error('Workflow Execution Error:', error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
// Set error result and show notification immediately
setExecutionResult({
success: false,
output: { response: {} },
error: errorMessage,
logs: [],
})
addNotification('error', `Workflow execution failed: ${errorMessage}`, activeWorkflowId)
// Persist error log
// Persist error log after notification
await persistLogs(
[
{
@@ -151,8 +154,6 @@ export function useWorkflowExecution() {
],
executionId
)
addNotification('error', `Workflow execution failed: ${errorMessage}`, activeWorkflowId)
} finally {
setIsExecuting(false)
}
@@ -165,6 +166,8 @@ export function useWorkflowExecution() {
isOpen,
toggleConsole,
getAllVariables,
isExecuting,
setIsExecuting,
])
return { isExecuting, executionResult, handleRunWorkflow }
+1 -2
View File
@@ -17,12 +17,11 @@ export const FunctionBlock: BlockConfig<CodeExecutionOutput> = {
},
],
tools: {
access: ['function_execute'],
access: ['webcontainer_execute'],
},
inputs: {
code: { type: 'string', required: true },
timeout: { type: 'number', required: false },
memoryLimit: { type: 'number', required: false },
},
outputs: {
response: {
+35 -8
View File
@@ -460,17 +460,44 @@ export class FunctionBlockHandler implements BlockHandler {
inputs: Record<string, any>,
context: ExecutionContext
): Promise<BlockOutput> {
const tool = getTool(block.config.tool)
if (!tool) {
throw new Error(`Tool not found: ${block.config.tool}`)
// Only try WebContainer in browser environment with direct execution
const isBrowser = typeof window !== 'undefined'
if (isBrowser && window.crossOriginIsolated) {
try {
// Dynamically import WebContainer to prevent server-side import
const { executeCode } = await import('@/lib/webcontainer')
// Prepare code for execution
const codeContent = Array.isArray(inputs.code)
? inputs.code.map((c: { content: string }) => c.content).join('\n')
: inputs.code
// Execute directly in the browser
const result = await executeCode(codeContent, inputs, inputs.timeout || 5000)
if (!result.success) {
console.warn(`WebContainer API execution failed: ${result.error}`)
throw new Error(result.error || `WebContainer execution failed with no error message`)
}
return { response: result.output }
} catch (error: any) {
console.warn('WebContainer execution failed, falling back to VM:', error.message)
}
}
const result = await executeTool(block.config.tool, inputs, true)
if (!result.success) {
throw new Error(result.error || `Function execution failed with no error message`)
}
// Fall back to VM execution if WebContainer fails or not available
try {
const vmResult = await executeTool('function_execute', inputs, true)
return { response: result.output }
if (!vmResult.success) {
throw new Error(vmResult.error || `Function execution failed with no error message`)
}
return { response: vmResult.output }
} catch (vmError: any) {
throw new Error(`Function execution failed: ${vmError.message}`)
}
}
}
+323
View File
@@ -0,0 +1,323 @@
import { WebContainer } from '@webcontainer/api'
import { auth } from '@webcontainer/api'
// Singleton instance of WebContainer
let webcontainerInstance: WebContainer | null = null
let isBooting = false
let bootPromise: Promise<WebContainer> | null = null
/**
* Checks if the browser environment supports WebContainers
*/
function isCrossOriginIsolated(): boolean {
if (typeof window === 'undefined') return false
return !!window.crossOriginIsolated
}
/**
* Check if we're running in the browser
*/
function isBrowser(): boolean {
return typeof window !== 'undefined'
}
/**
* Initialize WebContainer authentication
*/
async function initializeAuth() {
if (!isBrowser()) return
try {
await auth.init({
clientId: process.env.WEBCONTAINER_CLIENT_ID || '',
scope: '',
})
} catch (error) {
console.error('Failed to initialize WebContainer auth:', error)
throw error
}
}
/**
* Initializes a WebContainer instance if one doesn't already exist
* @returns Promise<WebContainer>
*/
export async function getWebContainer(): Promise<WebContainer> {
// Only attempt WebContainer initialization in browser
if (!isBrowser()) {
throw new Error('WebContainer can only be initialized in browser environments')
}
// Return existing instance if available
if (webcontainerInstance) {
return webcontainerInstance
}
// Return existing boot promise if already booting
if (isBooting && bootPromise) {
return bootPromise
}
// Check if cross-origin isolation is enabled
if (!isCrossOriginIsolated()) {
console.warn('Cross-Origin Isolation is not enabled. WebContainers require COOP/COEP headers.')
throw new Error(
'WebContainers require cross-origin isolation. Please restart the server for changes to take effect.'
)
}
// Set flag and create boot promise
isBooting = true
bootPromise = (async () => {
try {
await initializeAuth()
// Boot a new WebContainer instance
webcontainerInstance = await WebContainer.boot({
// COEP configuration
coep: 'require-corp',
workdirName: 'sim-studio',
// Enable debugging
forwardPreviewErrors: true,
})
// Set up a basic project with a package.json
await setupBaseFileSystem(webcontainerInstance)
return webcontainerInstance
} catch (error) {
console.error('WebContainer boot error:', error)
isBooting = false
bootPromise = null
throw error
}
})()
return bootPromise
}
/**
* Sets up the base file system for the WebContainer
*/
async function setupBaseFileSystem(container: WebContainer): Promise<void> {
await container.mount({
'package.json': {
file: {
contents: JSON.stringify(
{
name: 'sim-studio-execution',
type: 'module',
version: '1.0.0',
description: 'Sim Studio Code Execution Environment',
dependencies: {},
},
null,
2
),
},
},
'README.md': {
file: {
contents:
'# Sim Studio Code Execution\n\nThis is a code execution environment for Sim Studio.',
},
},
})
}
/**
* Parse imports from code and install required packages
*/
async function installDependencies(container: WebContainer, code: string): Promise<void> {
// Simple regex to find import statements
const importRegex =
/import\s+?(?:(?:(?:[\w*\s{},]*)\s+from\s+?)|)(?:(?:"(?:[^"]*)")|(?:'([^']*)'))[^;]*/g
const matches = code.match(importRegex) || []
const packages = new Set<string>()
// Extract package names from import statements
for (const match of matches) {
const packageMatch = match.match(/from\s+['"]([^@\s'"]+)/)
if (packageMatch && packageMatch[1] && !packageMatch[1].startsWith('.')) {
packages.add(packageMatch[1])
}
}
if (packages.size > 0) {
// Install each package
const installProcess = await container.spawn('npm', ['install', ...Array.from(packages)])
const installExit = await installProcess.exit
if (installExit !== 0) {
throw new Error('Failed to install dependencies')
}
}
}
/**
* Execute code in a WebContainer
*/
export async function executeCode(
code: string,
params: Record<string, any> = {},
timeout: number = 5000
): Promise<{
success: boolean
output: {
result: any
stdout: string
executionTime: number
}
error?: string
}> {
const startTime = Date.now()
let process: any = null
try {
// Get or initialize WebContainer
const webcontainer = await getWebContainer()
// Install any required dependencies
await installDependencies(webcontainer, code)
// Split imports from the rest of the code
const importRegex =
/^import\s+?(?:(?:(?:[\w*\s{},]*)\s+from\s+?)|)(?:(?:"(?:[^"]*)")|(?:'([^']*)'))[^;]*;?\s*/gm
const imports = []
let remainingCode = code
let match
while ((match = importRegex.exec(code)) !== null) {
imports.push(match[0])
remainingCode = remainingCode.replace(match[0], '')
}
// Create the module file with proper structure
const moduleCode = `
${imports.join('\n')}
// Access to params
const params = ${JSON.stringify(params)};
// Capture return values
let __result;
// Execute code and capture result
__result = await (async () => {
${remainingCode}
})();
// Print the result to stdout for capture
if (__result !== undefined) {
console.log('__RESULT_START__');
console.log(JSON.stringify(__result));
console.log('__RESULT_END__');
}
// Ensure process exits after completion
process.exit(0);
`
// Mount the file as an ES module
await webcontainer.fs.writeFile('/code.mjs', moduleCode)
// Set up stdout capture
let stdout = ''
let result: any = null
let processCompleted = false
// Run the code with Node.js in ES module mode
process = await webcontainer.spawn('node', ['code.mjs'])
// Create a promise that resolves when we have our result
const resultPromise = new Promise<void>((resolve, reject) => {
// Collect output
process.output.pipeTo(
new WritableStream({
write(data) {
stdout += data
// Check for result markers
if (stdout.includes('__RESULT_START__') && stdout.includes('__RESULT_END__')) {
try {
const resultStart = stdout.indexOf('__RESULT_START__') + '__RESULT_START__'.length
const resultEnd = stdout.indexOf('__RESULT_END__')
const resultJson = stdout.substring(resultStart, resultEnd).trim()
if (resultJson) {
result = JSON.parse(resultJson)
}
// Don't resolve yet - wait for process to exit
} catch (error) {
console.error('Failed to parse result', error)
reject(error)
}
}
},
})
)
// Listen for process exit
process.exit.then((code: number) => {
processCompleted = true
if (code === 0) {
resolve()
} else {
reject(new Error(`Process exited with code ${code}`))
}
})
})
// Add a timeout
const timeoutPromise = new Promise<void>((_, reject) => {
setTimeout(() => {
if (!processCompleted && process) {
process.kill()
}
reject(new Error(`Execution timed out after ${timeout}ms`))
}, timeout)
})
// Wait for either completion or timeout
await Promise.race([resultPromise, timeoutPromise])
const executionTime = Date.now() - startTime
// Clean stdout of our result markers
const cleanedStdout = stdout
.replace(/(__RESULT_START__|__RESULT_END__)/g, '')
.replace(/\n{3,}/g, '\n\n') // Reduce multiple newlines
.trim()
return {
success: true,
output: {
result,
stdout: cleanedStdout,
executionTime,
},
}
} catch (error: any) {
// Ensure process is killed on error
if (process) {
try {
process.kill()
} catch (killError) {
console.error('Error killing process:', killError)
}
}
const executionTime = Date.now() - startTime
return {
success: false,
output: {
result: null,
stdout: '',
executionTime,
},
error: error.message || 'Code execution failed',
}
}
}
+35
View File
@@ -5,6 +5,41 @@ const nextConfig: NextConfig = {
buildActivity: false,
appIsrStatus: false,
},
images: {
domains: ['avatars.githubusercontent.com'],
},
async headers() {
return [
{
// API routes CORS headers
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
{ key: 'Access-Control-Allow-Origin', value: 'https://localhost:3001' },
{ key: 'Access-Control-Allow-Methods', value: 'GET,POST,OPTIONS,PUT,DELETE' },
{
key: 'Access-Control-Allow-Headers',
value:
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version',
},
],
},
{
// Cross-Origin Isolation headers (existing)
source: '/:path*',
headers: [
{
key: 'Cross-Origin-Embedder-Policy',
value: 'require-corp',
},
{
key: 'Cross-Origin-Opener-Policy',
value: 'same-origin',
},
],
},
]
},
}
export default nextConfig
+54 -40
View File
@@ -24,6 +24,7 @@
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.6",
"@types/lodash.debounce": "^4.0.9",
"@webcontainer/api": "^1.5.1-internal.9",
"better-auth": "^1.1.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -34,7 +35,7 @@
"drizzle-orm": "^0.39.3",
"lodash.debounce": "^4.0.8",
"lucide-react": "^0.469.0",
"next": "15.1.3",
"next": "^15.2.0",
"openai": "^4.85.4",
"postgres": "^3.4.5",
"prismjs": "^1.29.0",
@@ -1387,15 +1388,15 @@
"license": "MIT"
},
"node_modules/@next/env": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.3.tgz",
"integrity": "sha512-Q1tXwQCGWyA3ehMph3VO+E6xFPHDKdHFYosadt0F78EObYxPio0S09H9UGYznDe6Wc8eLKLG89GqcFJJDiK5xw==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.0.tgz",
"integrity": "sha512-eMgJu1RBXxxqqnuRJQh5RozhskoNUDHBFybvi+Z+yK9qzKeG7dadhv/Vp1YooSZmCnegf7JxWuapV77necLZNA==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.3.tgz",
"integrity": "sha512-aZtmIh8jU89DZahXQt1La0f2EMPt/i7W+rG1sLtYJERsP7GRnNFghsciFpQcKHcGh4dUiyTB5C1X3Dde/Gw8gg==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.0.tgz",
"integrity": "sha512-rlp22GZwNJjFCyL7h5wz9vtpBVuCt3ZYjFWpEPBGzG712/uL1bbSkS675rVAUCRZ4hjoTJ26Q7IKhr5DfJrHDA==",
"cpu": [
"arm64"
],
@@ -1409,12 +1410,13 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.3.tgz",
"integrity": "sha512-aw8901rjkVBK5mbq5oV32IqkJg+CQa6aULNlN8zyCWSsePzEG3kpDkAFkkTOh3eJ0p95KbkLyWBzslQKamXsLA==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.0.tgz",
"integrity": "sha512-DiU85EqSHogCz80+sgsx90/ecygfCSGl5P3b4XDRVZpgujBm5lp4ts7YaHru7eVTyZMjHInzKr+w0/7+qDrvMA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -1424,12 +1426,13 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.3.tgz",
"integrity": "sha512-YbdaYjyHa4fPK4GR4k2XgXV0p8vbU1SZh7vv6El4bl9N+ZSiMfbmqCuCuNU1Z4ebJMumafaz6UCC2zaJCsdzjw==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.0.tgz",
"integrity": "sha512-VnpoMaGukiNWVxeqKHwi8MN47yKGyki5q+7ql/7p/3ifuU2341i/gDwGK1rivk0pVYbdv5D8z63uu9yMw0QhpQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -1439,12 +1442,13 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.3.tgz",
"integrity": "sha512-qgH/aRj2xcr4BouwKG3XdqNu33SDadqbkqB6KaZZkozar857upxKakbRllpqZgWl/NDeSCBYPmUAZPBHZpbA0w==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.0.tgz",
"integrity": "sha512-ka97/ssYE5nPH4Qs+8bd8RlYeNeUVBhcnsNUmFM6VWEob4jfN9FTr0NBhXVi1XEJpj3cMfgSRW+LdE3SUZbPrw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -1454,12 +1458,13 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.3.tgz",
"integrity": "sha512-uzafnTFwZCPN499fNVnS2xFME8WLC9y7PLRs/yqz5lz1X/ySoxfaK2Hbz74zYUdEg+iDZPd8KlsWaw9HKkLEVw==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.0.tgz",
"integrity": "sha512-zY1JduE4B3q0k2ZCE+DAF/1efjTXUsKP+VXRtrt/rJCTgDlUyyryx7aOgYXNc1d8gobys/Lof9P9ze8IyRDn7Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -1469,12 +1474,13 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.3.tgz",
"integrity": "sha512-el6GUFi4SiDYnMTTlJJFMU+GHvw0UIFnffP1qhurrN1qJV3BqaSRUjkDUgVV44T6zpw1Lc6u+yn0puDKHs+Sbw==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.0.tgz",
"integrity": "sha512-QqvLZpurBD46RhaVaVBepkVQzh8xtlUN00RlG4Iq1sBheNugamUNPuZEH1r9X1YGQo1KqAe1iiShF0acva3jHQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
@@ -1484,12 +1490,13 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.3.tgz",
"integrity": "sha512-6RxKjvnvVMM89giYGI1qye9ODsBQpHSHVo8vqA8xGhmRPZHDQUE4jcDbhBwK0GnFMqBnu+XMg3nYukNkmLOLWw==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.0.tgz",
"integrity": "sha512-ODZ0r9WMyylTHAN6pLtvUtQlGXBL9voljv6ujSlcsjOxhtXPI1Ag6AhZK0SE8hEpR1374WZZ5w33ChpJd5fsjw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@@ -1499,12 +1506,13 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.3.tgz",
"integrity": "sha512-VId/f5blObG7IodwC5Grf+aYP0O8Saz1/aeU3YcWqNdIUAmFQY3VEPKPaIzfv32F/clvanOb2K2BR5DtDs6XyQ==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.0.tgz",
"integrity": "sha512-8+4Z3Z7xa13NdUuUAcpVNA6o76lNPniBd9Xbo02bwXQXnZgFvEopwY2at5+z7yHl47X9qbZpvwatZ2BRo3EdZw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
@@ -3711,6 +3719,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/@webcontainer/api": {
"version": "1.5.1-internal.9",
"resolved": "https://registry.npmjs.org/@webcontainer/api/-/api-1.5.1-internal.9.tgz",
"integrity": "sha512-l5cR4pzGwe+ZnR+HFKtz5Rq6JntSqay5ivX/LHoaf4vmTv8lYbNK0FQDPsXSd6Ual6F95eofCUJkyExDqK7Ozw==",
"license": "MIT"
},
"node_modules/abab": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz",
@@ -8453,12 +8467,12 @@
"license": "MIT"
},
"node_modules/next": {
"version": "15.1.3",
"resolved": "https://registry.npmjs.org/next/-/next-15.1.3.tgz",
"integrity": "sha512-5igmb8N8AEhWDYzogcJvtcRDU6n4cMGtBklxKD4biYv4LXN8+awc/bbQ2IM2NQHdVPgJ6XumYXfo3hBtErg1DA==",
"version": "15.2.0",
"resolved": "https://registry.npmjs.org/next/-/next-15.2.0.tgz",
"integrity": "sha512-VaiM7sZYX8KIAHBrRGSFytKknkrexNfGb8GlG6e93JqueCspuGte8i4ybn8z4ww1x3f2uzY4YpTaBEW4/hvsoQ==",
"license": "MIT",
"dependencies": {
"@next/env": "15.1.3",
"@next/env": "15.2.0",
"@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15",
"busboy": "1.6.0",
@@ -8473,14 +8487,14 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.1.3",
"@next/swc-darwin-x64": "15.1.3",
"@next/swc-linux-arm64-gnu": "15.1.3",
"@next/swc-linux-arm64-musl": "15.1.3",
"@next/swc-linux-x64-gnu": "15.1.3",
"@next/swc-linux-x64-musl": "15.1.3",
"@next/swc-win32-arm64-msvc": "15.1.3",
"@next/swc-win32-x64-msvc": "15.1.3",
"@next/swc-darwin-arm64": "15.2.0",
"@next/swc-darwin-x64": "15.2.0",
"@next/swc-linux-arm64-gnu": "15.2.0",
"@next/swc-linux-arm64-musl": "15.2.0",
"@next/swc-linux-x64-gnu": "15.2.0",
"@next/swc-linux-x64-musl": "15.2.0",
"@next/swc-win32-arm64-msvc": "15.2.0",
"@next/swc-win32-x64-msvc": "15.2.0",
"sharp": "^0.33.5"
},
"peerDependencies": {
+2 -1
View File
@@ -33,6 +33,7 @@
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.6",
"@types/lodash.debounce": "^4.0.9",
"@webcontainer/api": "^1.5.1-internal.9",
"better-auth": "^1.1.18",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -43,7 +44,7 @@
"drizzle-orm": "^0.39.3",
"lodash.debounce": "^4.0.8",
"lucide-react": "^0.469.0",
"next": "15.1.3",
"next": "^15.2.0",
"openai": "^4.85.4",
"postgres": "^3.4.5",
"prismjs": "^1.29.0",
+66
View File
@@ -0,0 +1,66 @@
import { ToolConfig } from '../types'
import { CodeExecutionInput, CodeExecutionOutput } from './execute'
const DEFAULT_TIMEOUT = 5000 // 5 seconds
export const webcontainerExecuteTool: ToolConfig<CodeExecutionInput, CodeExecutionOutput> = {
id: 'webcontainer_execute',
name: 'WebContainer Execute',
description:
'Execute JavaScript code in a secure WebContainer environment with Node.js runtime and full npm package support.',
version: '1.0.0',
params: {
code: {
type: 'string',
required: true,
description: 'The code to execute',
},
timeout: {
type: 'number',
required: false,
description: 'Execution timeout in milliseconds',
default: DEFAULT_TIMEOUT,
},
},
request: {
url: '/api/webcontainer',
method: 'POST',
headers: () => ({
'Content-Type': 'application/json',
}),
body: (params: CodeExecutionInput) => {
const codeContent = Array.isArray(params.code)
? params.code.map((c: { content: string }) => c.content).join('\n')
: params.code
return {
code: codeContent,
timeout: params.timeout || DEFAULT_TIMEOUT,
}
},
isInternalRoute: true,
},
transformResponse: async (response: Response): Promise<CodeExecutionOutput> => {
const result = await response.json()
if (!response.ok || !result.success) {
throw new Error(result.error || 'WebContainer code execution failed')
}
return {
success: true,
output: {
result: result.output.result,
stdout: result.output.stdout,
executionTime: result.output.executionTime,
},
}
},
transformError: (error: any) => {
return error.message || 'WebContainer code execution failed'
},
}
+2
View File
@@ -2,6 +2,7 @@ import { useCustomToolsStore } from '@/stores/custom-tools/store'
import { visionTool as crewAIVision } from './crewai/vision'
import { scrapeTool } from './firecrawl/scrape'
import { functionExecuteTool as functionExecute } from './function/execute'
import { webcontainerExecuteTool as webcontainerExecute } from './function/webcontainer'
import { commentTool } from './github/comment'
import { prTool } from './github/pr'
import { repoInfoTool } from './github/repo'
@@ -39,6 +40,7 @@ export const tools: Record<string, ToolConfig> = {
hubspot_contacts: hubspotContacts,
salesforce_opportunities: salesforceOpportunities,
function_execute: functionExecute,
webcontainer_execute: webcontainerExecute,
crewai_vision: crewAIVision,
firecrawl_scrape: scrapeTool,
jina_readurl: readUrlTool,