做 LLM Agent 开发时,Code Interpreter 是个绕不开的功能。让 AI 写代码容易,但让它安全地运行代码,却是个棘手的工程问题。
在设计 go-sandbox 之初,我们面临的最大抉择就是:到底是用 Docker,还是自己造轮子?
Docker 的诱惑与代价
最开始,我们自然而然地想到了 Docker。它成熟、安全、隔离性好。我们尝试为每个代码执行请求启动一个容器:
docker run --rm -it python:3.10 python -c "print('hello')"
但在高并发测试中,问题很快暴露了:
- 慢:即使是热启动,容器的创建和销毁也需要几百毫秒。对于用户来说,这意味着明显的卡顿。
- 重:每个容器都包含完整的用户空间,内存开销大。当并发量上来后,服务器资源消耗惊人。
我们意识到,对于“运行一段几行代码”这种短时、高频的任务,Docker 还是太重了。我们需要一种更轻量级的方案,一种能像启动普通进程一样快,但又能像容器一样安全的方案。
回归 Linux 原生:进程级沙箱
于是,我们将目光投向了 Linux 内核原生的安全特性:Seccomp, Chroot, 和 Namespaces。
我们的想法是:能不能直接在宿主机上启动 Python 进程,但在它“睁眼”看世界之前,给它戴上镣铐?
这就构成了 go-sandbox 的核心设计哲学:
- 不虚拟化硬件:直接利用宿主机 CPU,性能零损耗。
- 不完整虚拟化 OS:只隔离文件系统和系统调用,启动零延迟。
架构演进:如何控制一个“野”进程?
决定了方向,下一个问题是:如何实现?
我们需要一个“监管者” (Manager) 来管理这些“囚犯” (Runner)。
最初,我们考虑用 Go 的 os/exec 包装一下命令,但这远远不够。我们需要在 Python 进程内部进行精细的操作,比如在加载任何用户模块之前,先执行 chroot。
这就引出了我们架构中最关键的设计:特洛伊木马模式。
我们决定不修改 Python 解释器的源码(那样维护成本太高),而是利用 Python 的动态加载能力,注入一段我们的 Go 代码。
- Go Server: 负责接收请求,准备“牢房”(临时目录)。
- Shared Object (
.so): 这是我们特制的“手铐”。它是一个编译成 C 动态库的 Go 程序。 - Python Loader: 一个简单的 Python 脚本,负责把“手铐”戴上,然后才开始执行用户代码。
graph LR
A[Go Server] -->|1. 准备环境| B(临时目录)
A -->|2. 启动进程| C[Python 进程]
C -->|3. 加载| D[Sandbox.so]
D -->|4. 锁定| C
C -->|5. 执行| E[用户代码]
这种架构让我们既享受了 Go 语言开发系统工具的便利(处理 syscall),又保持了对目标语言(Python/Node.js)的零侵入性。
在接下来的文章中,我会详细分享我们是如何一步步实现这个“特洛伊木马”的,以及在利用 c-shared 模式时遇到的那些坑。