Skip to content

plugin-code-sandbox-os — 代码沙箱(OS 原生) ​

包名: @aalis/plugin-code-sandbox-os
契约: @aalis/api-code-sandbox
源码: packages/plugin-code-sandbox-os/src/index.ts(纯逻辑 sandbox.ts)

概述 ​

code-sandbox 服务的 OS 原生实现:用系统自带的隔离机制把不可信代码(code_runner 的 Python/JS)关进牢笼运行。不是容器(不依赖 Docker/守护进程),用 npm 装上即用。

平台机制是否需额外安装
macOSsandbox-exec(Seatbelt/SBPL)否(系统自带;Apple 标记 deprecated 但仍可用,Chromium 等仍在用)
Linuxbubblewrap(bwrap)是:apt/dnf install bubblewrap,且需启用 unprivileged user namespaces
其它—无后端 → available=false

插件声明 ​

ts
export default definePlugin({
  name: '@aalis/plugin-code-sandbox-os',
  provides: [codeSandbox],
  uses: {
    processService,
    doctor: optional(doctor),
    logger,
    lifecycle,
    provide,
  },
  apply(caps) { /* 见源码 */ },
});

设计要点 ​

  • 独立服务,不污染通用 process 契约:「隔离执行不可信代码」是 code_runner 独有诉求,故自成 code-sandbox 服务,而非给所有子进程共享的 ProcessService 加字段。
  • 经 caps.processService.use spawn:把命令包成 bwrap … / sandbox-exec … 后交给现有 process 服务执行—— 沙箱相关的逻辑不进 process-api / process-local;也不直接 import node:child_process/node:fs(后端探测用经 process 服务的功能性试跑)。 调用方的中止信号(SandboxRunRequest.signal)原样交给 execFile,由 process 服务按进程组停掉。
  • 功能性探测:启动时真跑一次最小沙箱命令(超时 5000ms),退出码为 0 才 available=true——一次覆盖「存在性」+「Linux userns 是否真能用」。 Linux 的探测命令是 bwrap --ro-bind / / --dev /dev --proc /proc --unshare-all true,与正式运行一样挂 /dev 与 /proc:只放开 seccomp、挂不了 /proc 的容器里探测即失败、按无后端,不会探测报可用、之后每次运行都失败。 探测带本次激活的 lifecycle.use.signal:启动器挂住时停用或停机会中止探测,激活随之落定,不必等满探测超时,也不会超过宽限仍没停下、拖住重新激活。 因此本插件依赖 lifecycle。
  • 可换可叠:未来 -docker / -wasm / -e2b 等不同机制各自提供 code-sandbox,经优先级/偏好替换。

没有后端时 ​

探测不通或平台没有后端时,本插件照样提供 code-sandbox(available=false),记一条 warn:「没有可用的 OS 沙箱,code_runner 的 auto 模式会拒绝执行代码」,后接修法与原因。修法按平台写:Linux 装上 bubblewrap、开启非特权用户命名空间,在容器里运行时还要能挂 /proc;macOS 确认 /usr/bin/sandbox-exec 能执行;其它平台在 macOS、Linux 或 WSL 上运行。原因是探测命令的退出码、终止信号或启动失败的报错。

装了诊断插件时登记检查项 env/code-sandbox.backend(「代码沙箱」):没有后端时报 warn,detail 是上面的修法与原因;有后端时报 ok,写出后端(seatbelt 或 bwrap)。没装 doctor 时不登记。

隔离语义(v1) ​

维度行为
文件写仅 workspace + 本次临时目录可写,其余只读/拒写
文件读放开(解释器需系统库;故不防读取本机其它文件——要防读需 WASM/microVM 实现)
网络默认断网(code_runner 的 sandbox.network=deny);可整体放开,但无法按域名过滤
环境变量仅放行白名单(--clearenv/env -i),宿主 secrets 不进沙箱
资源/超时沿用 code_runner 的 timeout(SIGKILL)
停止键回合中止时按 process 服务的中止语义停掉:POSIX 先 SIGTERM、宽限 2000ms 后 SIGKILL,打的是外层启动器所在的进程组

一句话:防「写出工作区 / 联网外泄 / 篡改系统」,是对原「裸子进程」的实打实安全升级;不防本机文件读取。

停止键与超时的边界 ​

停止键与超时都只打外层启动器所在的进程组:

  • macOS(Seatbelt):sandbox-exec → env → 解释器 逐级 exec,是同一个进程,脚本与它起的普通子进程在组里,能停干净。主动脱离进程组的后代(Python 的 os.setsid、Node 以 detached: true 起的子进程等守护化写法)不在组里,停止键与超时都停不掉。
  • Linux(bwrap):bwrap 带 --new-session,内层进程不在我们的进程组里,信号打到外层 bwrap;它同时带 --die-with-parent 与 --unshare-all(含 pid 命名空间),外层退出后命名空间里的进程随之结束,脱离进程组的后代也包括在内。

服务接口(code-sandbox) ​

typescript
interface CodeSandboxService {
  readonly available: boolean;  // 无后端 → 调用方应 fail-closed
  readonly backend: string;     // 'bwrap' | 'seatbelt' | 'none'
  run(req: { cmd; args; cwd?; env?; timeout?; signal?; policy }): Promise<ExecResult>;
}

code_runner 用法:取服务 → available 为假则拒绝执行(不裸跑)→ 为真则 run()。见 plugin-tool-code-runner 的「沙箱」节。

文档对应 dev@c4e046e41,core 0.21.0,构建于 2026-11-02
基于 MIT 许可发布