diff --git a/packages/package-executor/src/executor.ts b/packages/package-executor/src/executor.ts index 1b33be6..16f6ceb 100644 --- a/packages/package-executor/src/executor.ts +++ b/packages/package-executor/src/executor.ts @@ -1,21 +1,15 @@ /** - * Package executor without sandboxing - * Executes npm packages directly - * - * TODO: Add proper sandboxing with isolated-vm or similar when Next.js compatible solution is found - * VM2 doesn't work with Next.js Turbopack due to runtime file access requirements + * Package executor client + * Calls the remote sandbox service for secure package execution */ -import { execSync } from 'child_process'; -import { existsSync, mkdirSync } from 'fs'; -import { join } from 'path'; import type { ExecutionResult, ExecutorOptions } from './types.js'; -const DEFAULT_TIMEOUT = 5000; // 5 seconds -const CACHE_DIR = process.env.PACKAGE_CACHE_DIR || '/tmp/.tpmjs-cache'; +const DEFAULT_TIMEOUT = 10000; // 10 seconds +const SANDBOX_URL = process.env.SANDBOX_EXECUTOR_URL || 'http://localhost:3000'; /** - * Execute a package function with parameters + * Execute a package function with parameters via remote sandbox */ export async function executePackage( packageName: string, @@ -25,49 +19,64 @@ export async function executePackage( ): Promise { const startTime = Date.now(); const timeout = options.timeout || DEFAULT_TIMEOUT; - const cacheDir = options.cacheDir || CACHE_DIR; try { - // Ensure package is installed - const packageDir = await ensurePackageInstalled(packageName, cacheDir); + // Call the remote sandbox service + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); - // Set up timeout - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => reject(new Error('Execution timeout')), timeout); + const response = await fetch(`${SANDBOX_URL}/execute`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + packageName, + functionName, + params, + }), + signal: controller.signal, }); - // Execute the package with dynamic import - const executionPromise = (async () => { - // Dynamic require from the package directory - const packagePath = join(packageDir, 'node_modules', packageName); + clearTimeout(timeoutId); - // Use require to load the package - // biome-ignore lint/security/noGlobalEval: Required for dynamic package execution - const pkg = require(packagePath); - - // Get the function to execute - const fn = typeof pkg === 'function' ? pkg : pkg[functionName || 'default']; - - if (typeof fn !== 'function') { - throw new Error(`Package ${packageName} does not export a function named ${functionName || 'default'}`); - } - - // Execute the function - const result = await Promise.resolve(fn(params)); - return result; - })(); - - // Race between execution and timeout - const result = await Promise.race([executionPromise, timeoutPromise]); const executionTimeMs = Date.now() - startTime; + if (!response.ok) { + const errorData = (await response.json().catch(() => ({ error: 'Unknown error' }))) as { + error?: string; + }; + return { + success: false, + error: errorData.error || `Sandbox service error: ${response.status}`, + executionTimeMs, + }; + } + + const result = (await response.json()) as { + success: boolean; + output?: unknown; + error?: string; + executionTimeMs?: number; + }; + return { - success: true, - output: result, - executionTimeMs, + success: result.success, + output: result.output, + error: result.error, + executionTimeMs: result.executionTimeMs || executionTimeMs, }; } catch (error) { const executionTimeMs = Date.now() - startTime; + + if (error instanceof Error && error.name === 'AbortError') { + return { + success: false, + error: 'Execution timeout', + executionTimeMs, + }; + } + return { success: false, error: error instanceof Error ? error.message : String(error), @@ -77,58 +86,34 @@ export async function executePackage( } /** - * Ensure a package is installed in the cache directory + * Clear the package cache on the remote sandbox */ -async function ensurePackageInstalled(packageName: string, cacheDir: string): Promise { - // Create cache directory if it doesn't exist - if (!existsSync(cacheDir)) { - mkdirSync(cacheDir, { recursive: true }); - } - - // Package-specific directory - const packageDir = join(cacheDir, packageName.replace(/[@/]/g, '_')); - - // Check if already installed - if (existsSync(join(packageDir, 'node_modules', packageName))) { - return packageDir; - } - - // Install the package +export async function clearCache(): Promise { try { - if (!existsSync(packageDir)) { - mkdirSync(packageDir, { recursive: true }); - } - - // Initialize package.json if not exists - const packageJsonPath = join(packageDir, 'package.json'); - if (!existsSync(packageJsonPath)) { - execSync('npm init -y', { - cwd: packageDir, - stdio: 'ignore', - }); - } - - // Install the package - execSync(`npm install ${packageName} --no-save`, { - cwd: packageDir, - stdio: 'ignore', - timeout: 30000, // 30 second timeout for installation + const response = await fetch(`${SANDBOX_URL}/cache/clear`, { + method: 'POST', }); - return packageDir; + if (!response.ok) { + throw new Error(`Failed to clear cache: ${response.status}`); + } } catch (error) { - throw new Error( - `Failed to install package ${packageName}: ${error instanceof Error ? error.message : String(error)}` - ); + console.error('Failed to clear sandbox cache:', error); + throw error; } } /** - * Clear the package cache + * Check if the sandbox service is healthy */ -export function clearCache(cacheDir?: string): void { - const dir = cacheDir || CACHE_DIR; - if (existsSync(dir)) { - execSync(`rm -rf ${dir}`, { stdio: 'ignore' }); +export async function checkHealth(): Promise { + try { + const response = await fetch(`${SANDBOX_URL}/health`, { + method: 'GET', + }); + + return response.ok; + } catch { + return false; } }