AI生成代码应视为不受信任输入,不应在存有SSH密钥的本地机器执行;作者提供了一个约60行JavaScript的远程沙箱执行框架,含超时与断言。
AI 生成的代码应该被视为不可信输入:永远不要在与你的 SSH 密钥同一台机器上执行它。这篇文章提供了一个约 60 行的 JavaScript 工具链,可以在Disposable remote server 上以 Scrubbed environment、硬超时和预先写好的 pass/fail 断言来运行模型输出——让评估变得可复现,而不是"看起来没问题"。
上周我让一个模型写了一个"清理超过 7 天的临时文件"的小工具。输出看起来没问题——直到我逐行阅读时发现它开心地递归遍历了 $HOME,因为我在 prompt 里忘了固定工作目录。什么都没运行,什么都没坏。但这提醒了我一个我一直在违反的规则:AI 生成的代码是不可信输入,而我一直在自己的开发笔记本上执行它。
最近有很多关于 Agent 工具边界失效时会发生什么的讨论——OWASP Top 10 for LLM Applications 将过度授权和不安全的输出处理列为一级风险。我没有关于 Agent 安全的宏大理论。我只有一套无聊但可复现的工作流:在一侧生成,在 Disposable remote server 上执行,用一个小工具链评估。这篇文章就是这个工具链,加上它失效地方的诚实清单。
这个工作流有三大约束是我在意的:
对于约束 1 和 3,我使用一个免费的远程服务器作为执行沙箱。我一直在配合 MonkeyCode 使用,它目前为生成侧提供免费模型访问,为执行侧提供免费服务器选项,因此整个循环保持零成本。
披露:本文作为 MonkeyCode 产品推广的一部分而准备。
任何等效设置都可以——一个可丢弃的 VPS、云免费套餐、一个容器主机。下面的工具链不依赖任何特定提供商;它依赖的是代码生成地点和代码运行地点之间的分离。
核心思想:模型输出写入文件,文件在带超时的子进程中运行,无网络相关行为、Scrubbed environment 和 scratch 工作目录。然后一组断言决定 pass/fail——而不是我盯着 diff 的眼睛。机制完全依赖 Node.js 内置的 child_process 模块,因此需要审计的依赖为零。
// sandbox-run.mjs — executes one generated snippet with hard boundaries.
// Run this ON the sandbox server, never on your dev machine.
import { spawn } from "node:child_process";
import { mkdtemp, writeFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
const TIMEOUT_MS = 5_000;
export async function runInSandbox(code) {
const dir = await mkdtemp(join(tmpdir(), "aigen-"));
const entry = join(dir, "snippet.mjs");
await writeFile(entry, code);
const result = await new Promise((resolve) => {
const child = spawn("node", [entry], {
cwd: dir, // confined working directory
env: { PATH: process.env.PATH }, // scrubbed env: no tokens, no keys
stdio: ["ignore", "pipe", "pipe"],
});
let stdout = "", stderr = "";
child.stdout.on("data", (d) => (stdout += d));
child.stderr.on("data", (d) => (stderr += d));
const killer = setTimeout(() => {
child.kill("SIGKILL");
resolve({ status: "timeout", stdout, stderr });
}, TIMEOUT_MS);
child.on("close", (code) => {
clearTimeout(killer);
resolve({ status: code === 0 ? "ok" : "error", exitCode: code, stdout, stderr });
});
});
await rm(dir, { recursive: true, force: true });
return result;
}
然后是评估侧——一个我在查看任何模型输出之前就写好的测试用例固定表,这样我就不会为了迁就模型产生的结果而弯曲标准:
// eval.mjs — one prompt, many runs, fixed assertions.
import { runInSandbox } from "./sandbox-run.mjs";
const cases = [
{ name: "sorts numbers ascending", input: [3, 1, 2], expect: [1, 2, 3] },
{ name: "handles empty array", input: [], expect: [] },
{ name: "does not mutate input", input: [2, 1], expect: "no-mutation" },
];
async function evaluate(generatedCode) {
const wrapped = `
import fs from "node:fs";
const input = JSON.parse(fs.readFileSync(0, "utf8"));
${generatedCode}
console.log(JSON.stringify(solution(input)));
`;
const results = [];
for (const c of cases) {
const r = await runInSandbox(wrapped);
results.push({ case: c.name, ...r });
}
return results;
}
// Feed in code produced by whatever model you're evaluating.
const codeFromModel = process.argv[2]
? await (await import("node:fs/promises")).readFile(process.argv[2], "utf8")
: `const solution = (a) => [...a].sort((x, y) => x - y);`;
console.table(await evaluate(codeFromModel));
生成前写断言。 我先写测试表,把任务粘贴进模型(在我的运行中通过 MonkeyCode 的免费模型访问,但任何模型都可以),把原始输出保存到文件,然后才运行 node eval.mjs output-attempt-1.js。这把"看起来没问题"变成了"3/3 全通过,零 mutation"。
临时目录就是爆炸半径。 Scrubbed env 意味着即使怀有恶意的输出也无法读取我的凭证,因为在沙箱机器上没有凭证在作用域内。结合 cwd 固定,我开篇轶事中的 $HOME 递归 bug 物理上无法逃出 scratch 目录。
子进程不是安全沙箱。它阻止事故,但不阻止对手。如果你在一个从互联网获取指令的 Agent 循环中运行代码,请加上容器隔离、只读文件系统和网络出口规则。我的工具链是安全带,不是气囊系统。
免费套餐是便利,不是保证。免费模型访问和免费服务器选项会随时间变化;把成本视为今天为零,但要设计成明天可以换提供商。这就是为什么工具链里没有提供商特定代码。
Pass/fail 表可以被模型作弊。如果任务泄露进了模型的训练数据,高分可能反映的是记忆。轮换使用新颖的测试用例——污染是基准评估中一个已被记录的问题,NIST 的 AI 风险管理指南中有相关讨论。
这测量的是一件狭窄的事——输出在约束下是否正确运行。它没有说明可读性、可维护性,或者你是否应该上线它。
如果你在运行任何东西之前审查每一行生成的代码,而且你的任务风险低,那么这个开销是不值得的。如果你已经有基于 CI 的评估基础设施,你也不需要我的 60 行版本。这个工作流适用于中间的缝隙:人们把模型输出直接粘贴到主终端并祈祷的那种情况。
如果你正处于那个缝隙中,最小化而有用的步骤不是采用任何特定平台,而是今晚就把执行移出你的笔记本,并在你的下一个 prompt 之前写三个断言。把上面两个文件复制到任何可丢弃的机器上,用 node eval.mjs 对抗你的下一个模型输出,看看 pass/fail 表告诉你的东西是你的眼睛没看到的。如果你想找一个零成本的地方尝试这个循环,MonkeyCode 的免费模型访问和免费服务器就是我上面运行所用的;这个工具链本身会跟着你到任何你接下来用的平台。