Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ When running with Docker, use `host.docker.internal` if vLLM is on your host mac

**Requirements:**
- [Bun](https://bun.sh/) runtime
- [Node.js](https://nodejs.org/) v20+ (required for sandboxed code execution)
- PostgreSQL 12+ with [pgvector extension](https://github.com/pgvector/pgvector) (required for AI embeddings)

**Note:** Sim uses vector embeddings for AI features like knowledge bases and semantic search, which requires the `pgvector` PostgreSQL extension.
Expand Down
114 changes: 69 additions & 45 deletions apps/sim/lib/execution/isolated-vm.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type ChildProcess, spawn } from 'node:child_process'
import { type ChildProcess, execSync } from 'node:child_process'
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
Expand All @@ -7,6 +7,19 @@ import { createLogger } from '@/lib/logs/console/logger'

const logger = createLogger('IsolatedVMExecution')

let nodeAvailable: boolean | null = null

function checkNodeAvailable(): boolean {
if (nodeAvailable !== null) return nodeAvailable
try {
execSync('node --version', { stdio: 'ignore' })
nodeAvailable = true
} catch {
nodeAvailable = false
}
return nodeAvailable
}

export interface IsolatedVMExecutionRequest {
code: string
params: Record<string, unknown>
Expand Down Expand Up @@ -171,6 +184,16 @@ async function ensureWorker(): Promise<void> {
if (workerReadyPromise) return workerReadyPromise

workerReadyPromise = new Promise<void>((resolve, reject) => {
if (!checkNodeAvailable()) {
reject(
new Error(
'Node.js is required for code execution but was not found. ' +
'Please install Node.js (v20+) from https://nodejs.org'
)
)
return
}

const currentDir = path.dirname(fileURLToPath(import.meta.url))
const workerPath = path.join(currentDir, 'isolated-vm-worker.cjs')

Expand All @@ -179,53 +202,54 @@ async function ensureWorker(): Promise<void> {
return
}

worker = spawn(process.execPath, [workerPath], {
stdio: ['ignore', 'pipe', 'inherit', 'ipc'],
serialization: 'json',
})
import('node:child_process').then(({ spawn }) => {
worker = spawn('node', [workerPath], {
stdio: ['ignore', 'pipe', 'inherit', 'ipc'],
serialization: 'json',
})

worker.on('message', handleWorkerMessage)

const startTimeout = setTimeout(() => {
worker?.kill()
worker = null
workerReady = false
workerReadyPromise = null
reject(new Error('Worker failed to start within timeout'))
}, 10000)

const readyHandler = (message: unknown) => {
if (
typeof message === 'object' &&
message !== null &&
(message as { type?: string }).type === 'ready'
) {
workerReady = true
clearTimeout(startTimeout)
worker?.off('message', readyHandler)
resolve()
}
}
worker.on('message', readyHandler)
worker.on('message', handleWorkerMessage)

worker.on('exit', (code) => {
logger.warn('Isolated-vm worker exited', { code })
if (workerIdleTimeout) {
clearTimeout(workerIdleTimeout)
workerIdleTimeout = null
}
worker = null
workerReady = false
workerReadyPromise = null
for (const [id, pending] of pendingExecutions) {
clearTimeout(pending.timeout)
pending.resolve({
result: null,
stdout: '',
error: { message: 'Worker process exited unexpectedly', name: 'WorkerError' },
})
pendingExecutions.delete(id)
const startTimeout = setTimeout(() => {
worker?.kill()
worker = null
workerReady = false
workerReadyPromise = null
reject(new Error('Worker failed to start within timeout'))
}, 10000)

const readyHandler = (message: unknown) => {
if (
typeof message === 'object' &&
message !== null &&
(message as { type?: string }).type === 'ready'
) {
workerReady = true
clearTimeout(startTimeout)
worker?.off('message', readyHandler)
resolve()
}
}
worker.on('message', readyHandler)

worker.on('exit', () => {
if (workerIdleTimeout) {
clearTimeout(workerIdleTimeout)
workerIdleTimeout = null
}
worker = null
workerReady = false
workerReadyPromise = null
for (const [id, pending] of pendingExecutions) {
clearTimeout(pending.timeout)
pending.resolve({
result: null,
stdout: '',
error: { message: 'Worker process exited unexpectedly', name: 'WorkerError' },
})
pendingExecutions.delete(id)
}
})
})
})

Expand Down
Loading