autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
252 lines (251 loc) • 8.51 kB
JavaScript
/**
* SandboxRunner — 工具锻造沙箱验证
*
* 在受限环境中执行锻造的工具代码 + 测试用例,验证安全性和正确性。
* 使用 Node.js vm 模块提供隔离执行环境。
*
* 安全约束:
* - 5s 执行超时
* - 禁止 require/import (无文件系统 / 网络 / 子进程)
* - 内存限制 via vm 上下文隔离
* - 只暴露 console.log, JSON, Math, Date, Array, Object, String 等纯函数
*/
import { createContext, runInContext } from 'node:vm';
import Logger from '#infra/logging/Logger.js';
/* ────────────────────── Constants ────────────────────── */
const EXECUTION_TIMEOUT_MS = 5000;
/** 禁止使用的模式 */
const FORBIDDEN_PATTERNS = [
/\brequire\s*\(/,
/\bimport\s*\(/,
/\bprocess\b/,
/\b__dirname\b/,
/\b__filename\b/,
/\bglobal\b/,
/\bglobalThis\b/,
/\beval\s*\(/,
/\bFunction\s*\(/,
/\bfetch\s*\(/,
/\bXMLHttpRequest\b/,
/\bWebSocket\b/,
/\bchild_process\b/,
/\bfs\b\./,
/\bnet\b\./,
/\bhttp\b\./,
/\bhttps\b\./,
];
/* ────────────────────── Class ────────────────────── */
export class SandboxRunner {
#logger = Logger.getInstance();
/**
* 对工具代码进行安全检查
*/
checkSafety(code) {
const violations = [];
for (const pattern of FORBIDDEN_PATTERNS) {
if (pattern.test(code)) {
violations.push(`Forbidden pattern detected: ${pattern.source}`);
}
}
return { passed: violations.length === 0, violations };
}
/**
* 在沙箱中执行工具代码 + 测试用例
*
* @param code 工具函数代码(应 export default 或赋给 __toolHandler__)
* @param testCases 测试用例
*/
run(code, testCases) {
const start = Date.now();
// 1. 安全检查
const safetyCheck = this.checkSafety(code);
if (!safetyCheck.passed) {
return {
success: false,
testResults: [],
executionTime: Date.now() - start,
safetyCheck,
};
}
// 2. 构建沙箱上下文 — 只暴露安全的全局对象
const logs = [];
const sandbox = createContext({
console: {
log: (...args) => logs.push(args.map(String).join(' ')),
warn: (...args) => logs.push(`[warn] ${args.map(String).join(' ')}`),
error: (...args) => logs.push(`[error] ${args.map(String).join(' ')}`),
},
JSON,
Math,
Date,
Array,
Object,
String,
Number,
Boolean,
RegExp,
Map,
Set,
Error,
TypeError,
RangeError,
parseInt,
parseFloat,
isNaN,
isFinite,
encodeURIComponent,
decodeURIComponent,
// 工具函数占位,由代码赋值
__toolHandler__: null,
});
// 3. 加载工具代码
try {
const wrappedCode = `${code}\n;(typeof toolHandler !== 'undefined') && (__toolHandler__ = toolHandler);`;
runInContext(wrappedCode, sandbox, {
timeout: EXECUTION_TIMEOUT_MS,
filename: 'forged-tool.js',
});
}
catch (err) {
return {
success: false,
testResults: [
{
description: 'Code loading',
passed: false,
error: `Failed to load tool code: ${err.message}`,
},
],
executionTime: Date.now() - start,
safetyCheck,
};
}
const handler = sandbox.__toolHandler__;
if (typeof handler !== 'function') {
return {
success: false,
testResults: [
{
description: 'Handler check',
passed: false,
error: 'Tool code must define a `toolHandler` function',
},
],
executionTime: Date.now() - start,
safetyCheck,
};
}
// 4. 执行测试用例
const testResults = [];
let allPassed = true;
for (const tc of testCases) {
try {
const testCode = `__toolHandler__(${JSON.stringify(tc.input)})`;
const actual = runInContext(testCode, sandbox, {
timeout: EXECUTION_TIMEOUT_MS,
filename: 'forged-tool-test.js',
});
const passed = SandboxRunner.#deepEqual(actual, tc.expectedOutput);
if (!passed) {
allPassed = false;
}
testResults.push({
description: tc.description,
passed,
actualOutput: actual,
error: passed
? undefined
: `Expected ${JSON.stringify(tc.expectedOutput)}, got ${JSON.stringify(actual)}`,
});
}
catch (err) {
allPassed = false;
testResults.push({
description: tc.description,
passed: false,
error: err.message,
});
}
}
this.#logger.debug(`SandboxRunner: ${testResults.filter((t) => t.passed).length}/${testResults.length} tests passed`);
return {
success: allPassed,
testResults,
executionTime: Date.now() - start,
safetyCheck,
};
}
/**
* 从验证通过的代码创建可调用 handler
* 返回的 handler 每次调用都在新沙箱中执行(隔离)
*/
createHandler(code) {
return async (params) => {
const sandbox = createContext({
console: { log: () => { }, warn: () => { }, error: () => { } },
JSON,
Math,
Date,
Array,
Object,
String,
Number,
Boolean,
RegExp,
Map,
Set,
Error,
TypeError,
RangeError,
parseInt,
parseFloat,
isNaN,
isFinite,
encodeURIComponent,
decodeURIComponent,
__toolHandler__: null,
});
const wrappedCode = `${code}\n;(typeof toolHandler !== 'undefined') && (__toolHandler__ = toolHandler);`;
runInContext(wrappedCode, sandbox, {
timeout: EXECUTION_TIMEOUT_MS,
filename: 'forged-tool.js',
});
const handler = sandbox.__toolHandler__;
if (typeof handler !== 'function') {
throw new Error('Forged tool code does not define a toolHandler function');
}
const execCode = `__toolHandler__(${JSON.stringify(params)})`;
const result = runInContext(execCode, sandbox, {
timeout: EXECUTION_TIMEOUT_MS,
filename: 'forged-tool-exec.js',
});
// 如果返回 Promise-like,需特殊处理(vm 里一般不会有真正的 Promise)
return result;
};
}
/** 简单深度比较 */
static #deepEqual(a, b) {
if (a === b) {
return true;
}
if (a === null || b === null || typeof a !== typeof b) {
return false;
}
if (typeof a !== 'object') {
return false;
}
const aObj = a;
const bObj = b;
const keysA = Object.keys(aObj);
const keysB = Object.keys(bObj);
if (keysA.length !== keysB.length) {
return false;
}
for (const key of keysA) {
if (!SandboxRunner.#deepEqual(aObj[key], bObj[key])) {
return false;
}
}
return true;
}
}