/** * Evaluate JavaScript code in engine262 and capture spec section marks. * Executes code in the engine262 JavaScript engine and returns the captured * ECMAScript spec section marks. * Uses child_process for true isolation and guaranteed termination via SIGKILL. */ import { spawn } from "node:child_process"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { z } from "zod"; // #region Zod schemas (not exported) const consoleEntrySchema = z.object({ method: z.string().describe("Console method name (log, warn, debug, error)"), values: z.array(z.unknown()).describe("Values logged to console"), }); const evaluateSuccessOutputSchema = z.object({ importantSections: z .array(z.string()) .describe("Important spec sections hit during execution"), otherSections: z .array(z.string()) .describe("Other spec sections hit during execution"), consoleOutput: z .array(consoleEntrySchema) .describe("Console output captured during execution"), error: z.undefined().optional(), }); const evaluateErrorOutputSchema = z.object({ importantSections: z.undefined().optional(), otherSections: z.undefined().optional(), consoleOutput: z.undefined().optional(), error: z.string().describe("Error message when execution fails"), }); // Combined output schema (single object with optional fields because MCP SDK doesn't accept union schemas) const evaluateOutputSchemaCombined = z.object({ importantSections: z .array(z.string()) .optional() .describe("Important spec sections hit during execution (absent on error)"), otherSections: z .array(z.string()) .optional() .describe("Other spec sections hit during execution (absent on error)"), consoleOutput: z .array(consoleEntrySchema) .optional() .describe("Console output captured during execution (absent on error)"), error: z.string().optional().describe("Error message when execution fails"), }); // #endregion // #region Exported Zod schemas /** * Tool metadata for reuse in OpenCode tools. */ export const toolMetadata = { description: "Executes pure JavaScript code in the engine262 JavaScript engine and captures which ECMAScript specification sections are hit during execution. " + "Returns an object with importantSections, otherSections, and consoleOutput arrays." + "Code must be pure ECMAScript with no DOM, browser, or Node.js APIs (no fs, document, window, etc.). " + "console object with log/warn/debug/error methods and ask262Debug are available globally (no import needed). " + "Use ask262Debug.startImportant() and ask262Debug.stopImportant() to mark important sections. " + "Example: console.log('test'); ask262Debug.startImportant(); let x = 1 + 2; ask262Debug.stopImportant();", args: { code: "JavaScript code to execute in engine262 (e.g., 'console.log([1,2,3].map(x => x * 2))')", }, }; /** * Input schema for the evaluate tool. */ export const inputSchema = z.object({ code: z.string().describe(toolMetadata.args.code), }); /** * Output schema for the evaluate tool. * Uses a single object schema with optional fields for MCP SDK compatibility * (MCP SDK doesn't support union schemas in outputSchema). */ export const outputSchema = evaluateOutputSchemaCombined; // #endregion // #region TypeScript types (inferred from Zod schemas) export type ConsoleEntry = z.infer; export type EvaluateSuccessOutput = z.infer; export type EvaluateErrorOutput = z.infer; /** Combined tool output type (success or error) */ export type EvaluateToolOutput = EvaluateSuccessOutput | EvaluateErrorOutput; export type EvaluateToolInput = z.infer; // #endregion /** * Tool name constant. */ export const toolName = "ask262_evaluate_in_engine262"; /** * Default timeout for code execution */ const DEFAULT_EXECUTION_TIMEOUT_MS = 1000; /** * Execute JavaScript code in engine262 using a child process. * @param code - JavaScript code to execute * @param timeoutMs - Maximum execution time in milliseconds * @returns Execution result as JSON string */ function executeInChildProcess( code: string, timeoutMs: number, ): Promise { return new Promise((resolve) => { // Get the runner script path const runnerPath = path.join( path.dirname(fileURLToPath(import.meta.url)), "evaluateInEngine262.runner.mjs", ); // Spawn child process using Bun const child = spawn("bun", [runnerPath], { stdio: ["pipe", "pipe", "pipe"], killSignal: "SIGKILL", // Force kill, can't be blocked }); let stdout = ""; let stderr = ""; // Collect stdout child.stdout?.on("data", (data: Buffer) => { stdout += data.toString(); }); // Collect stderr (for errors) child.stderr?.on("data", (data: Buffer) => { stderr += data.toString(); }); // Set up timeout let killedByTimeout = false; const timeoutId = setTimeout(() => { killedByTimeout = true; // SIGKILL can't be caught or blocked - guaranteed termination child.kill("SIGKILL"); }, timeoutMs); // Handle process exit child.on("close", (code, signal) => { // Clear timeout clearTimeout(timeoutId); if (signal === "SIGKILL" || signal === "SIGTERM") { // Process was killed (timeout or error) if (killedByTimeout) { resolve( JSON.stringify({ error: `Execution timeout after ${timeoutMs}ms`, }), ); } else { resolve( JSON.stringify({ error: stderr || "Process terminated", }), ); } return; } if (code !== 0) { resolve( JSON.stringify({ error: stderr || `Process exited with code ${code}`, }), ); return; } // Success - return stdout (should be JSON result) try { // Validate it's valid JSON JSON.parse(stdout); resolve(stdout); } catch { resolve( JSON.stringify({ error: `Invalid output: ${stdout.slice(0, 200)}`, }), ); } }); // Handle spawn errors child.on("error", (error) => { clearTimeout(timeoutId); resolve( JSON.stringify({ error: `Failed to spawn process: ${error.message}`, }), ); }); // Send code to child via stdin child.stdin?.write(code); child.stdin?.end(); }); } /** * Creates the evaluateInEngine262 tool function. * Executes JavaScript code in engine262 using a child process with timeout support. * Uses child_process for true isolation and guaranteed termination via SIGKILL. * @param timeoutMs - Maximum execution time in milliseconds * @returns Function that executes code and returns structured output */ export function createEvaluateInEngine262Tool( timeoutMs = DEFAULT_EXECUTION_TIMEOUT_MS, ) { return async ({ code }: EvaluateToolInput): Promise => { try { // Execute code in isolated child process const resultJson = await executeInChildProcess(code, timeoutMs); // Parse the result return JSON.parse(resultJson) as EvaluateToolOutput; } catch (error) { // Return error result return { error: error instanceof Error ? error.message : String(error), }; } }; }