const vm = require('vm'); const fs = require('fs'); const pathModule = require('path'); const PROJECT_ROOT = pathModule.resolve(__dirname, '..'); function makeSafeFs() { return { readFileSync: (p, enc) => fs.readFileSync(p, enc), writeFileSync: (p, data) => fs.writeFileSync(p, data), readdirSync: (p) => fs.readdirSync(p), mkdirSync: (p, opts) => fs.mkdirSync(p, opts), existsSync: (p) => fs.existsSync(p), unlinkSync: (p) => fs.unlinkSync(p), statSync: (p) => fs.statSync(p), }; } function makeSafePath() { return { join: (...parts) => pathModule.join(...parts), resolve: (...parts) => pathModule.resolve(...parts), dirname: (p) => pathModule.dirname(p), basename: (p, ext) => pathModule.basename(p, ext), extname: (p) => pathModule.extname(p), relative: (from, to) => pathModule.relative(from, to), }; } // Runs tool code of the form: // async function run(args) { ...; return something; } // in a restricted context. This isolates accidental bugs (typos, bad logic, // infinite loops via timeout) but it is NOT a hardened security boundary - // tool code still runs inside the same Node process. The approval step is // the real safety gate: nothing runs until you've read and approved it. async function runTool(code, args, timeoutMs = 10000) { const logs = []; const sandbox = { args, fetch: (...a) => fetch(...a), console: { log: (...a) => logs.push(a.map((x) => (typeof x === 'string' ? x : JSON.stringify(x))).join(' ')), }, fs: makeSafeFs(), path: makeSafePath(), __projectRoot: PROJECT_ROOT, __dirname: __dirname, JSON, Math, Date, Promise, URL, URLSearchParams, TextEncoder, TextDecoder, setTimeout, clearTimeout, __result: undefined, __error: undefined, }; const context = vm.createContext(sandbox); const wrapped = ` (function() { "use strict"; ${code} if (typeof run !== "function") { throw new Error('Tool code must define: async function run(args) { ... }'); } return run(args); })() `; let script; try { script = new vm.Script(wrapped, { filename: 'tool.js' }); } catch (e) { throw Object.assign(new Error(`Syntax error in tool code: ${e.message}`), { logs }); } let runPromise; try { // The `timeout` guards the synchronous portion (e.g. an infinite while loop // before the first await). Async continuations are guarded below. runPromise = script.runInContext(context, { timeout: Math.min(timeoutMs, 5000) }); } catch (e) { throw Object.assign(new Error(e.message), { stack: e.stack, logs }); } const timeoutGuard = new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool timed out after ${timeoutMs}ms`)), timeoutMs) ); try { const result = await Promise.race([Promise.resolve(runPromise), timeoutGuard]); return { result, logs }; } catch (e) { throw Object.assign(new Error(e.message), { stack: e.stack, logs }); } } module.exports = { runTool };