Skip to content

Commit 7049b5a

Browse files
authored
feat(freestyle): refactored handler in executor, migrated from webcontainer to freestyle for code execution, fixed condition block issues (#222)
* migrated from webcontainers to freestyle for sandboxed code execution, updated all references * fixed condition block edges disappearing * fix issue with condition block out-of-order resolving inputs * refactored handlers in executor to be separate files, added unit tests * remove old executor handlers file
1 parent 7811c6e commit 7049b5a

40 files changed

+3919
-1659
lines changed

docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,18 @@ services:
1313
- /app/.next
1414
environment:
1515
- NODE_ENV=development
16-
- DATABASE_URL=postgresql://postgres:postgres@db:5432/simstudio
16+
- DATABASE_URL=postgresql://postgres:password@postgres:5432/postgres
1717
- POSTGRES_URL=postgresql://postgres:postgres@db:5432/simstudio
1818
- BETTER_AUTH_URL=http://localhost:3000
1919
- NEXT_PUBLIC_APP_URL=http://localhost:3000
2020
- BETTER_AUTH_SECRET=your_auth_secret_here
2121
- ENCRYPTION_KEY=your_encryption_key_here
22+
- FREESTYLE_API_KEY=placeholder
2223
- GOOGLE_CLIENT_ID=placeholder
2324
- GOOGLE_CLIENT_SECRET=placeholder
2425
- GITHUB_CLIENT_ID=placeholder
2526
- GITHUB_CLIENT_SECRET=placeholder
2627
- RESEND_API_KEY=placeholder
27-
- WEBCONTAINER_CLIENT_ID=placeholder
2828
depends_on:
2929
db:
3030
condition: service_healthy

docs/content/docs/execution/advanced.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Comprehensive error information is captured in the execution logs:
5151

5252
For certain operations, the system provides automatic fallbacks:
5353

54-
- **Function Execution**: WebContainer execution first, then VM execution if needed
54+
- **Function Execution**: Freestyle execution first, then VM execution if needed
5555
- **API Requests**: Automatic retries for transient network errors
5656
- **Model Calls**: Fallback to alternative models if primary model is unavailable
5757

docs/content/docs/execution/index.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ The execution engine includes built-in error handling mechanisms:
194194

195195
- **Block-Level Errors** - Errors in one block don't necessarily stop the entire workflow execution.
196196
- **Detailed Error Logs** - Comprehensive error information is captured in the execution logs, including error messages, stack traces, and relevant context.
197-
- **Fallback Mechanisms** - For function execution, the system tries WebContainer execution first, then falls back to VM execution if needed.
197+
- **Fallback Mechanisms** - For function execution, the system tries Freestyle execution first, then falls back to VM execution if needed.
198198
- **Recovery Options** - Configure blocks to retry on failure or implement custom error handling logic.
199199

200200
### Environment Variables

packages/sdk/src/generated/registry.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ export const availableTools = [
1212
"hubspot_contacts",
1313
"salesforce_opportunities",
1414
"function_execute",
15-
"webcontainer_execute",
1615
"vision_tool",
1716
"firecrawl_scrape",
1817
"jina_readurl",
@@ -103,7 +102,6 @@ export const toolToBlockMap: Record<string, string> = {
103102
'hubspot_contacts': 'HubspotContactsBlock',
104103
'salesforce_opportunities': 'SalesforceOpportunitiesBlock',
105104
'function_execute': 'FunctionExecuteBlock',
106-
'webcontainer_execute': 'WebcontainerExecuteBlock',
107105
'vision_tool': 'VisionToolBlock',
108106
'firecrawl_scrape': 'FirecrawlScrapeBlock',
109107
'jina_readurl': 'JinaReadurlBlock',
@@ -160,7 +158,6 @@ export const toolRequiredParameters: Record<string, string[]> = {
160158
'hubspot_contacts': ["apiKey"],
161159
'salesforce_opportunities': ["apiKey"],
162160
'function_execute': [],
163-
'webcontainer_execute': [],
164161
'vision_tool': ["apiKey"],
165162
'firecrawl_scrape': ["apiKey"],
166163
'jina_readurl': ["apiKey"],

sim/.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 64` to generate
1212
# RESEND_API_KEY= # Uncomment and add your key from https://resend.com to send actual emails
1313
# If left commented out, emails will be logged to console instead
1414

15-
# StackBlitz (Webcontainer) API Key (Optional, for handling sandboxed code execution for functions/custom-tools)
16-
# WEBCONTAINER_CLIENT_ID= # Uncomment and add your key from https://stackblitz.com/docs/webcontainer-api#webcontainer-client-id
15+
# Freestyle API Key (Required for sandboxed code execution for functions/custom-tools)
16+
# FREESTYLE_API_KEY= # Uncomment and add your key from https://docs.freestyle.sh/Getting-Started/run
1717

1818
# S3 Storage Configuration (Optional)
1919
# Set USE_S3=true to enable S3 storage in development

sim/.prettierrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
"singleAttributePerLine": false,
1818
"plugins": ["prettier-plugin-tailwindcss", "@trivago/prettier-plugin-sort-imports"],
1919
"importOrder": [
20+
"^(.*)/__test-utils__/mock-dependencies$",
21+
"^(vitest|jest)$",
2022
"^(react/(.*)$)|^(react$)",
2123
"^(next/(.*)$)|^(next$)",
2224
"<THIRD_PARTY_MODULES>",

sim/app/api/function/execute/route.ts

Lines changed: 148 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { NextRequest, NextResponse } from 'next/server'
2+
import { FreestyleSandboxes } from 'freestyle-sandboxes'
23
import { createContext, Script } from 'vm'
34
import { createLogger } from '@/lib/logs/console-logger'
45

@@ -61,45 +62,156 @@ export async function POST(req: NextRequest) {
6162
// Resolve variables in the code with workflow environment variables
6263
const resolvedCode = resolveCodeVariables(code, params, envVars)
6364

64-
// Create a secure context with console logging
65-
const context = createContext({
66-
params,
67-
environmentVariables: envVars, // Make environment variables available in the context
68-
console: {
69-
log: (...args: any[]) => {
70-
const logMessage = args
71-
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
72-
.join(' ')
73-
stdout += logMessage
74-
},
75-
error: (...args: any[]) => {
76-
const errorMessage = args
77-
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
78-
.join(' ')
79-
logger.error(`[${requestId}] Code Console Error:`, errorMessage)
80-
stdout += 'ERROR: ' + errorMessage
81-
},
82-
},
83-
})
84-
85-
const script = new Script(`
86-
(async () => {
87-
try {
88-
${resolvedCode}
89-
} catch (error) {
90-
console.error(error);
91-
throw error;
65+
let result: any
66+
let executionMethod = 'vm' // Default execution method
67+
68+
// Try to use Freestyle if the API key is available
69+
if (process.env.FREESTYLE_API_KEY) {
70+
try {
71+
logger.info(`[${requestId}] Using Freestyle for code execution`)
72+
executionMethod = 'freestyle'
73+
74+
// Extract npm packages from code if needed
75+
const importRegex =
76+
/import\s+?(?:(?:(?:[\w*\s{},]*)\s+from\s+?)|)(?:(?:"([^"]*)")|(?:'([^']*)'))[^;]*/g
77+
const requireRegex = /const\s+[\w\s{}]*\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
78+
79+
const packages: Record<string, string> = {}
80+
const matches = [
81+
...resolvedCode.matchAll(importRegex),
82+
...resolvedCode.matchAll(requireRegex),
83+
]
84+
85+
// Extract package names from import statements
86+
for (const match of matches) {
87+
const packageName = match[1] || match[2]
88+
if (packageName && !packageName.startsWith('.') && !packageName.startsWith('/')) {
89+
// Extract just the package name without version or subpath
90+
const basePackageName = packageName.split('/')[0]
91+
packages[basePackageName] = 'latest' // Use latest version
92+
}
9293
}
93-
})()
94-
`)
9594

96-
const result = await script.runInContext(context, {
97-
timeout,
98-
displayErrors: true,
99-
})
95+
const freestyle = new FreestyleSandboxes({
96+
apiKey: process.env.FREESTYLE_API_KEY,
97+
})
98+
99+
// Wrap code in export default to match Freestyle's expectations
100+
const wrappedCode = `export default async () => {${resolvedCode}}`
101+
102+
// Execute the code with Freestyle
103+
const res = await freestyle.executeScript(wrappedCode, {
104+
nodeModules: packages,
105+
timeout: null,
106+
envVars: envVars,
107+
})
108+
109+
result = res.result
110+
logger.info(`[${requestId}] Freestyle execution result`, {
111+
result,
112+
stdout,
113+
})
114+
stdout =
115+
res.logs
116+
?.map((log) => (log.type === 'error' ? 'ERROR: ' : '') + log.message)
117+
.join('\n') || ''
118+
} catch (error: any) {
119+
// Log freestyle error and fall back to VM execution
120+
logger.error(`[${requestId}] Freestyle execution failed, falling back to VM:`, {
121+
error: error.message,
122+
stack: error.stack,
123+
})
124+
executionMethod = 'vm_fallback'
125+
126+
// Continue to VM execution
127+
const context = createContext({
128+
params,
129+
environmentVariables: envVars,
130+
console: {
131+
log: (...args: any[]) => {
132+
const logMessage =
133+
args
134+
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
135+
.join(' ') + '\n'
136+
stdout += logMessage
137+
},
138+
error: (...args: any[]) => {
139+
const errorMessage =
140+
args
141+
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
142+
.join(' ') + '\n'
143+
logger.error(`[${requestId}] Code Console Error:`, errorMessage)
144+
stdout += 'ERROR: ' + errorMessage
145+
},
146+
},
147+
})
148+
149+
const script = new Script(`
150+
(async () => {
151+
try {
152+
${resolvedCode}
153+
} catch (error) {
154+
console.error(error);
155+
throw error;
156+
}
157+
})()
158+
`)
159+
160+
result = await script.runInContext(context, {
161+
timeout,
162+
displayErrors: true,
163+
})
164+
logger.info(`[${requestId}] VM execution result`, {
165+
result,
166+
stdout,
167+
})
168+
}
169+
} else {
170+
// No Freestyle API key, use VM execution
171+
logger.info(`[${requestId}] Using VM for code execution (no Freestyle API key available)`)
172+
173+
// Create a secure context with console logging
174+
const context = createContext({
175+
params,
176+
environmentVariables: envVars,
177+
console: {
178+
log: (...args: any[]) => {
179+
const logMessage =
180+
args
181+
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
182+
.join(' ') + '\n'
183+
stdout += logMessage
184+
},
185+
error: (...args: any[]) => {
186+
const errorMessage =
187+
args
188+
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
189+
.join(' ') + '\n'
190+
logger.error(`[${requestId}] Code Console Error:`, errorMessage)
191+
stdout += 'ERROR: ' + errorMessage
192+
},
193+
},
194+
})
195+
196+
const script = new Script(`
197+
(async () => {
198+
try {
199+
${resolvedCode}
200+
} catch (error) {
201+
console.error(error);
202+
throw error;
203+
}
204+
})()
205+
`)
206+
207+
result = await script.runInContext(context, {
208+
timeout,
209+
displayErrors: true,
210+
})
211+
}
100212

101213
const executionTime = Date.now() - startTime
102-
logger.info(`[${requestId}] Function executed successfully`, {
214+
logger.info(`[${requestId}] Function executed successfully using ${executionMethod}`, {
103215
executionTime,
104216
})
105217

@@ -117,6 +229,7 @@ export async function POST(req: NextRequest) {
117229
const executionTime = Date.now() - startTime
118230
logger.error(`[${requestId}] Function execution failed`, {
119231
error: error.message || 'Unknown error',
232+
stack: error.stack,
120233
executionTime,
121234
})
122235

0 commit comments

Comments
 (0)