ausername-12345
Initial commit - Forge: self-improving AI assistant with filesystem access for self-editing
4381a41 | 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 }; | |