File size: 3,062 Bytes
4381a41 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 | 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 };
|