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 };