Executor执行内核揭秘:QuickJS WASM沙箱如何安全运行LLM生成的代码
【免费下载链接】executorThe missing integration layer for AI agents. Let them call any OpenAPI / MCP / GraphQL / custom js functions in secure environment.项目地址: https://gitcode.com/gh_mirrors/executor14/executor
Executor 是一款面向 AI Agent 的开源集成层("The missing integration layer for AI agents"),让 Claude Code、Cursor、ChatGPT 等任意 MCP 客户端都能安全调用 OpenAPI、MCP、GraphQL 与自定义函数。而它最引人瞩目的技术底座,就是执行内核中基于QuickJS WASM 沙箱的安全代码执行引擎——LLM 生成的 TypeScript 代码被隔离编译成 WASM 后运行,即使代码"失控",也触碰不到宿主机的一丝一毫。本文带你逐层拆解这套沙箱的 5 大安全机制与完整执行流水线 🧩
为什么 AI Agent 需要"安全沙箱"?
现代 AI Agent 的工作模式正在从"逐个调用固定工具"演进为"Code Mode":LLM 直接写一段 TypeScript/JavaScript 代码,在里面自由组合调用几十个已注册的工具。灵活是灵活,但危险也来了:
- ⚠️代码是 LLM 生成的,天然不可信:可能写死循环、内存爆炸、甚至尝试
require("fs")读取密钥; - ⚠️直连宿主机执行(比如直接
eval)等于把服务器交了出去; - ⚠️资源无上限时,一段
while(true)就能拖垮整个服务。
Executor 的答案是:把模型生成的代码放进一个由 WASM 构建的独立 JavaScript 解释器里运行——这个解释器就是 QuickJS 的 WASM 移植版。沙箱里跑的每一行代码,都是由 WASM 模块内部的 QuickJS 引擎逐条解释的,与宿主机的 V8 引擎完全无关。
Executor 执行内核:三层可插拔架构
Executor 的执行内核位于packages/kernel/与packages/core/execution/,分为清晰的三层:
| 层级 | 包路径 | 职责 |
|---|---|---|
| 契约层 | packages/kernel/core/ | 定义CodeExecutor契约、工具代理、TS 类型剥离、代码恢复等共享原语 |
| 引擎层 | packages/core/execution/ | 执行引擎(engine.ts):编排工具桥接、暂停/恢复、审批流 |
| 运行时层 | packages/kernel/runtime-* | 具体沙箱实现:QuickJS WASM、Deno 子进程、workerd 动态 Worker 等 |
这种"契约先行"的设计意味着沙箱是可以替换的:
| 运行时 | 路径 | 隔离方式 | 适用宿主 |
|---|---|---|---|
| QuickJS WASM | packages/kernel/runtime-quickjs/ | WASM 解释器内隔离 | 所有宿主(含 Cloudflare Workers) |
| Deno 子进程 | packages/kernel/runtime-deno-subprocess/ | 独立 OS 进程 | 本地 CLI / 桌面端 |
| 动态 Worker | packages/kernel/runtime-dynamic-worker/ | workerd Worker 线程 | Cloudflare |
| workerd 子进程 | packages/kernel/runtime-workerd-subprocess/ | 独立进程 | 需要 workerd API 的场景 |
其中 QuickJS WASM 运行时(README)的定位非常直白:
"Runs untrusted TypeScript/JavaScript in a WASM-backed interpreter with configurable timeout, memory limit, and stack size — safe enough to execute LLM-generated code that calls your registered tools."
(在 WASM 支持的解释器中运行不可信的 TS/JS,可配置超时、内存与栈大小——足以安全执行调用你注册工具的 LLM 生成代码。)
QuickJS WASM 沙箱的 5 大安全机制
核心实现全部集中在 packages/kernel/runtime-quickjs/src/index.ts,值得新手重点关注的有 5 个机制:
1. 全新运行时:一次执行,一次销毁
每次执行都会QuickJS.newRuntime()创建一个全新的 WASM 运行时和上下文,执行完毕立即dispose()销毁。沙箱的初始全局环境里只有 QuickJS 自带的标准对象:
- 没有
process,没有require,没有宿主对象; fetch被显式替换为直接抛错的函数(fetch is disabled in QuickJS executor)。
2. 三重资源配额:超时 + 内存 + 栈深度
沙箱给 LLM 代码套上了"三重枷锁",任何一项越界都会立即终止执行:
| 配额 | 默认值 | 作用 |
|---|---|---|
timeoutMs墙钟超时 | 5 分钟 | 整体执行时长上限 |
memoryLimitBytes内存上限 | 64 MB | VM 可分配内存上限 |
maxStackSizeBytes栈深度 | 1 MB | 防止无限递归 |
更妙的是超时的实现方式:宿主通过setInterruptHandler注册了一个协作式抢占钩子,每次 JS 执行到检查点都会被询问"该停了吗?"。这意味着哪怕 LLM 写了一个同步死循环,宿主机也能把它精准掐断——而宿主进程对自己的主线程是无法做到这一点的(见 sealed-bundle.ts 中的注释解释)。
3. 类型剥离 + 代码恢复:LLM 写的"脏代码"也能跑
LLM 最爱输出两类"半成品"代码,Executor 在送入沙箱前分别处理:
- 类型剥离(strip-types.ts):QuickJS 只认纯 JavaScript,而模型输出的常带
: number之类的 TS 注解。Executor 用 Sucrase 做纯语法级类型剥离,as T、泛型、interface 统统去掉,成本低且零语义改动; - 代码恢复(code-recovery.ts):模型经常把代码塞在 Markdown 的 ``` 围栏里,或写成
export default async () => {...}的形式。恢复器会用 Babel 解析 AST,自动剥掉围栏、解包export default,把任意形态的代码"修复"成一段可直接 await 的执行体。
4. 唯一的合法出口:tools惰性代理
沙箱里没有网络、没有文件系统,那 LLM 代码如何调用外部工具?答案是宿主注入的唯一桥梁——tools代理对象:
// LLM 生成的代码可以在沙箱内这样写 const pets = await tools.petstore.findPetsByStatus({ status: "available" });tools是一个基于Proxy的惰性路径代理:tools.petstore.findPetsByStatus这样的点路径不会真的展开成对象,而是在"调用那一刻"把完整路径petstore.findPetsByStatus和参数序列化后,通过宿主函数__executor_invokeTool回传给 Executor 引擎,走完权限策略校验、凭据注入之后才真正发起 API 请求,结果再以 JSON 字符串的形式送回沙箱。
换句话说:沙箱代码永远只能"点名"调用已注册的工具,任何未注册路径的调用都无从抵达真实世界——这是"最小权限"原则在沙箱边界上的完美落地。
5. 暂停与恢复:人机审批流的"暂停键"
有些工具需要人来把关(OAuth 授权、危险操作审批、表单填写)。当 LLM 代码在沙箱里调用这类工具时,执行引擎(engine.ts 的executeWithPause)会:
- 将沙箱执行 fork 为后台 Fiber,生成全局唯一的
exec_xxx执行 ID; - 挂起并返回
PausedExecution,把审批请求呈现给 UI; - 用户批准后,通过
resume接口注入响应,沙箱代码从原处继续跑,直至完成或下一次暂停。
CLI 用户同样能体验这一流程:
executor resume --execution-id exec_123LLM 代码执行的 6 步流水线
把以上机制串起来,一段 LLM 代码从生成到出结果的完整旅程是:
- 代码恢复:剥离 Markdown 围栏、解包
export default(code-recovery.ts); - 类型剥离:Sucrase 转成纯 JavaScript(strip-types.ts);
- 源码包装:注入
tools代理、桥接版console(日志回传宿主)、emit()输出通道,并禁用fetch; - 创建沙箱:全新 QuickJS 运行时,套用超时/内存/栈三重配额与中断钩子;
- 异步调度:宿主循环执行 WASM 内的 microtask 队列,同时监控"截止线"——工具派发期间暂停计时(
DeadlineTracker),避免一次慢 API 调用挤占整体预算; - 结果回收:读回
result(返回值)、logs(console 输出)、output(emit 产物),随即销毁运行时。
整条链路对宿主机零侵入,执行结果是一个纯粹的{ result, logs, output }数据结构。
不止 QuickJS:一个契约驱动的运行时生态
因为CodeExecutor只是一个两行接口(execute(code, toolInvoker)),Executor 的"执行内核"实际上是一个运行时生态:
- 本地 CLI / 桌面端可以选择 Deno 子进程这种"操作系统级隔离";
- Cloudflare 部署则因平台禁止运行时代码生成(ban V8 codegen),必须走 QuickJS WASM——而 QuickJS 恰好是"WASM 里再跑一个 JS 引擎",天然绕开该限制;
- 甚至还有一个刻意更小的原语sealed-bundle(sealed-bundle.ts):不带工具桥、不走计量计费,专门用来在同一个沙箱里跑系统自带的校验渲染,与用户执行路径物理隔离。
自己动手:3 分钟体验 Executor 沙箱
想亲手验证"LLM 代码在沙箱里作恶会怎样"?本地跑起来只需 Node.js 20+:
npm install -g executor # 安装 Executor CLI executor install # 安装常驻后台服务 executor web # 浏览器打开 Web UI,添加集成并连接 Agent在 Web UI 的执行面板里粘贴一段含死循环的 TypeScript,你会亲眼看到它在 5 分钟墙钟上限(可自定义至 100ms)被 QuickJS 中断钩子精准打断,宿主服务纹丝不动。如果想深入源码,仓库克隆地址为https://gitcode.com/gh_mirrors/executor14/executor,重点阅读路径:
- 沙箱核心:
packages/kernel/runtime-quickjs/src/index.ts - 类型剥离与代码恢复:
packages/kernel/core/src/ - 执行引擎与暂停/恢复:
packages/core/execution/src/engine.ts
总结:把"信任边界"画在 WASM 里
Executor 执行内核的设计哲学可以浓缩为一句话:不信任一行模型生成的代码,但给它一张只写着一个出口的名片。WASM 隔离保证了"物理上够不着",三重配额保证了"作不了大事",tools代理保证了"只能走正门",暂停/恢复则把"人类最终审批权"完整地留给了你。
对于正在构建 AI Agent 平台的新手开发者而言,这套 QuickJS WASM 沙箱架构几乎是"LLM 代码执行"这一课题的参考级答案 🚀
【免费下载链接】executorThe missing integration layer for AI agents. Let them call any OpenAPI / MCP / GraphQL / custom js functions in secure environment.项目地址: https://gitcode.com/gh_mirrors/executor14/executor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考