附录 2A:Agent 沙箱
Agent 能调工具这件事,一旦工具是"执行任意代码"或"运行 shell 命令",安全问题就从理论变成了现实。模型输出一段 Python 代码,你的运行时去执行了——这段代码能读写文件系统、发网络请求、安装恶意包、甚至删除整个目录。
沙箱的目的只有一个:即使 Agent 执行了恶意或错误的代码,损害也被限制在一个隔离环境内,不波及宿主机和生产系统。
为什么需要沙箱
不是所有工具都需要沙箱。get_weather 这种函数,参数是字符串、返回是字典,你自己写的代码,不需要隔离。
需要沙箱的场景是 Agent 执行不可预测的代码:
- 代码执行工具:Agent 生成 Python/JS 代码并运行
- Shell 命令工具:Agent 生成 bash 命令并执行
- 插件系统:第三方提供的工具代码,你没审计过
- 文件操作:Agent 可以读写任意路径
没有沙箱时,一段看起来无害的代码就可能造成问题:
# Agent 生成的"分析数据"代码
import os
# 实际在遍历你的文件系统
for root, dirs, files in os.walk("/"):
for f in files:
if f.endswith(".env"):
print(open(os.path.join(root, f)).read())
这段代码会找到并打印你机器上所有 .env 文件的内容——数据库连接串、API 密钥全暴露。
四种沙箱方案
Docker 容器
最常见的方案。把 Agent 的代码执行放在 Docker 容器里,容器和宿主机之间有文件系统隔离和网络隔离。
import docker
def run_in_docker(code: str, timeout: int = 30) -> dict:
"""在 Docker 容器中执行 Agent 生成的代码。"""
client = docker.from_env()
try:
container = client.containers.run(
image="python:3.12-slim",
command=["python", "-c", code],
mem_limit="256m", # 内存限制
cpu_period=100000,
cpu_quota=50000, # 50% CPU
network_disabled=True, # 禁用网络
read_only=True, # 只读文件系统
remove=True, # 执行完自动删除
timeout=timeout,
)
return {"stdout": container.decode("utf-8"), "error": None}
except docker.errors.ContainerError as e:
return {"stdout": "", "error": str(e)}
except Exception as e:
return {"stdout": "", "error": f"容器异常: {e}"}
这段代码启动一个临时容器,在里面运行 Agent 生成的 Python 代码。network_disabled=True 阻止代码访问网络,read_only=True 阻止写入文件系统,mem_limit 和 cpu_quota 限制资源消耗。
优点:生态成熟,大多数开发者熟悉,镜像可自定义。
缺点:容器启动需要几百毫秒到几秒,如果 Agent 频繁调用代码工具,延迟会累积。容器共享宿主机内核,隔离级别不如虚拟机——容器逃逸漏洞虽然少见但存在。
microVM(Firecracker)
AWS 做 Lambda 时遇到一个问题:Docker 容器的隔离对于多租户执行任意代码不够强。于是他们开源了 Firecracker——一个极轻量的虚拟机监控器(VMM),每个 microVM 有独立的内核。
宿主机
+-- microVM 1 (Agent A 的代码执行)
| +-- 独立 Linux 内核 + 最小 rootfs
+-- microVM 2 (Agent B 的代码执行)
| +-- 独立 Linux 内核 + 最小 rootfs
+-- ...
Firecracker microVM 的启动时间在 125ms 左右,内存开销约 5MB。比传统虚拟机轻很多,但比 Docker 容器的隔离更强——每个 VM 有自己的内核,容器逃逸类的攻击不适用。
实际使用时,一般不直接调 Firecracker API,而是通过封装好的平台:
- AWS Lambda:底层就是 Firecracker
- Fly.io Machines:每个 Machine 是一个 microVM
- E2B:专门给 AI Agent 做的代码沙箱服务,底层是 Firecracker
# 使用 E2B 的 Code Interpreter(基于 Firecracker microVM)
from e2b_code_interpreter import Sandbox
def run_in_microvm(code: str) -> dict:
"""在 E2B microVM 沙箱中执行代码。"""
sandbox = Sandbox()
try:
execution = sandbox.run_code(code)
return {
"stdout": execution.text,
"error": execution.error.value if execution.error else None,
}
finally:
sandbox.close()
优点:强隔离(内核级),启动快,AWS Lambda 已经大规模验证。
缺点:比 Docker 复杂,需要额外基础设施或第三方服务。本地开发不如 Docker 方便。
WebAssembly(Wasm)
WebAssembly 不是虚拟机,是一个指令集。代码编译成 Wasm 字节码后,在 Wasm 运行时(Wasmtime、Wasmer)里执行。Wasm 的安全模型是"默认什么都不能做"——没有文件系统访问、没有网络、没有系统调用,除非宿主显式授予。
Agent 代码 --> 编译为 .wasm --> Wasm 运行时执行
↑
宿主决定授予哪些能力:
- 允许读 /tmp 目录?
- 允许 HTTP 请求?
- 允许多少内存?
优点:启动极快(微秒级),内存开销极小,安全模型干净。Cloudflare Workers 就跑在 Wasm 之上。
缺点:生态限制大。不是所有 Python 库都能编译成 Wasm(比如依赖 C 扩展的库)。如果 Agent 需要跑 pandas 或 numpy,Wasm 目前不是好选择。更适合执行简单、确定的计算逻辑。
gVisor
Google 的方案。gVisor 是一个用户态内核——它拦截容器内的所有系统调用,在用户空间模拟内核行为,不直接传给宿主机内核。
普通 Docker: 容器代码 --> Linux 内核(共享)
Docker+gVisor: 容器代码 --> gVisor(用户态内核) --> Linux 内核
Google Cloud Run 和 Google 内部很多服务用 gVisor。它比纯 Docker 更安全(系统调用被过滤),比 Firecracker 更轻(不需要独立内核镜像),但 syscall 兼容性不是 100%——某些程序可能跑不起来。
对比表
| 方案 | 隔离级别 | 启动时间 | 内存开销 | 生态兼容性 | 适合场景 |
|---|---|---|---|---|---|
| Docker 容器 | 中(共享内核) | 百毫秒至秒 | 中 | 好 | 开发测试、非多租户 |
| Firecracker microVM | 高(独立内核) | 约125ms | 约5MB | 好 | 多租户生产、敏感代码 |
| WebAssembly | 高(无 syscall) | 微秒级 | 极小 | 有限 | 简单计算、边缘计算 |
| gVisor | 中高(syscall 过滤) | 和 Docker 接近 | 中 | 大部分兼容 | Google Cloud、需要额外防护 |
生产实践建议
开发阶段:Docker 容器够用。设置好 network_disabled、read_only、mem_limit,跑 Agent 生成的代码不会搞坏你的开发机。
上线初期:如果你的 Agent 面向用户执行代码(比如代码助手、数据分析工具),用 Firecracker 类服务(E2B、Fly Machines)或 Docker + gVisor。关键是多租户隔离——用户 A 的代码不能影响用户 B。
长期架构:把"执行不可信代码"这件事独立成一个服务。Agent 主进程和代码执行进程分开,通过 API 通信。这样即使沙箱方案以后要换(比如从 Docker 换到 Firecracker),Agent 核心逻辑不用改。
Agent 主进程 代码执行服务
(工具调度、对话管理) --API--> (Docker/Firecracker/Wasm)
|
隔离执行 Agent 生成的代码
|
<--API-- 返回 stdout/stderr
不管用哪种沙箱,有三条底线:
- 限制资源:内存、CPU、执行时间都要有上限。Agent 生成死循环是常事。
- 限制网络:除非有明确需求,默认禁用网络访问。防止代码外传数据。
- 限制文件系统:只读或只开放特定目录。Agent 不需要读
/etc/passwd。