tpmjs/packages/package-executor/src/executor.ts
Ajax Davis 30f581cbc3 feat: integrate Railway sandbox microservice for secure package execution
**Replace VM2 with Railway Microservice:**
- Remove VM2 dependency (incompatible with Next.js Turbopack bundling)
- Create Express sandbox service at `/services/sandbox-executor/`
- Uses isolated-vm for V8-level isolation with 128MB memory limit
- 10-second execution timeout with proper error handling

**Package Executor Client:**
- Rewrite `@tpmjs/package-executor` to call remote sandbox via HTTP
- Add `executePackage()`, `clearCache()`, `checkHealth()` functions
- Use AbortController for timeout handling
- Proper TypeScript type assertions for API responses
- Reads `SANDBOX_EXECUTOR_URL` from environment (defaults to localhost:3000)

**Sandbox Service Features:**
- `/execute` - Execute npm packages in isolated environment
- `/health` - Health check endpoint with service info
- `/cache/clear` - Clear npm package cache
- Package caching in `/tmp/.tpmjs-cache` for faster subsequent runs
- CORS support for web app integration
- Automatic ESM/CommonJS package detection

**Deployment Configuration:**
- Dockerfile with isolated-vm native dependencies (python3, make, g++)
- Railway.json with health checks and restart policies
- Environment variables: PORT, PACKAGE_CACHE_DIR, ALLOWED_ORIGINS
- Production URL: https://tpmjs-production.up.railway.app

**Integration:**
- Add SANDBOX_EXECUTOR_URL to .env.local
- Update Next.js config to mark package-executor as external
- Maintain existing API routes at `/api/tools/execute/[...slug]`

This architectural change enables secure package execution on Vercel
by moving sandboxing to a dedicated microservice on Railway.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-30 01:30:47 +10:00

119 lines
2.8 KiB
TypeScript

/**
* Package executor client
* Calls the remote sandbox service for secure package execution
*/
import type { ExecutionResult, ExecutorOptions } from './types.js';
const DEFAULT_TIMEOUT = 10000; // 10 seconds
const SANDBOX_URL = process.env.SANDBOX_EXECUTOR_URL || 'http://localhost:3000';
/**
* Execute a package function with parameters via remote sandbox
*/
export async function executePackage(
packageName: string,
functionName: string,
params: Record<string, unknown>,
options: ExecutorOptions = {}
): Promise<ExecutionResult> {
const startTime = Date.now();
const timeout = options.timeout || DEFAULT_TIMEOUT;
try {
// Call the remote sandbox service
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const response = await fetch(`${SANDBOX_URL}/execute`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
packageName,
functionName,
params,
}),
signal: controller.signal,
});
clearTimeout(timeoutId);
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: 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),
executionTimeMs,
};
}
}
/**
* Clear the package cache on the remote sandbox
*/
export async function clearCache(): Promise<void> {
try {
const response = await fetch(`${SANDBOX_URL}/cache/clear`, {
method: 'POST',
});
if (!response.ok) {
throw new Error(`Failed to clear cache: ${response.status}`);
}
} catch (error) {
console.error('Failed to clear sandbox cache:', error);
throw error;
}
}
/**
* Check if the sandbox service is healthy
*/
export async function checkHealth(): Promise<boolean> {
try {
const response = await fetch(`${SANDBOX_URL}/health`, {
method: 'GET',
});
return response.ok;
} catch {
return false;
}
}