mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
@@ -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 }
|
||||
|
||||
@@ -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
@@ -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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Generated
+54
-40
@@ -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
@@ -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",
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
Reference in New Issue
Block a user