ByteNoteByteNote
让 Agent 跑不可信代码:Sandbox 抽象
字

字节笔记本

2026年10月24日 · 约 12 分钟读完

让 Agent 跑不可信代码:Sandbox 抽象

API中转
¥120

沙箱:限制文件与网络访问

本文是《从 0 写 Agent》系列第 32 章。这个系列基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版),用 Go 从零实现一个生产级 Agent 运行时;本章讲「Sandbox 抽象:让 Agent 跑不可信代码」。

Agent 经常需要执行模型生成的代码,数据分析、自动化脚本、生成图表。但模型生成的代码不可信,可能有 bug、可能恶意、可能死循环。

直接在本机跑?太危险。agents-go 的 Sandbox 接口定义了"隔离执行代码"的抽象,有三个后端(local/docker/ssh)。这一章讲抽象设计和一个关键的错误语义反转。


引子:为什么需要沙箱

想象 Agent 帮用户分析数据,生成了这段 Python:

python
import os
os.system("rm -rf /")  # 模型"不小心"生成的危险代码

直接在本机跑 → 灾难。 沙箱解决,在隔离环境里跑,即使代码恶意,也不影响宿主。

或者这段:

python
while True: print("spam")  # 死循环

没超时限制 → 占满 CPU/磁盘,Agent 卡死。 沙箱强制超时和资源限制。

沙箱的核心价值,"不管代码多恶意,都不能逃出沙箱伤害宿主"。


安全实践

Sandbox 接口

极简(sandbox.go:24):

go
// sandbox/sandbox.go:24
type Sandbox interface {
    Exec(ctx context.Context, req ExecRequest) (*ExecResult, error)
    Close() error
}

两个方法,Exec(执行)和 Close(释放资源)。 简洁到极致。

ExecRequest,执行什么

go
// sandbox/sandbox.go:36
type ExecRequest struct {
    Cmd            []string          // 命令,如 ["python", "main.py"]
    Files          map[string]string // 要写入工作目录的文件
    Stdin          string            // 标准输入
    Env            map[string]string // 环境变量
    Timeout        time.Duration     // 超时(默认 30s)
    MaxOutputBytes int64             // 输出上限(默认 1MiB)
}

一次执行 = 写文件 + 跑命令 + 捕获输出。 Files 在命令执行前写入工作目录。

默认限制

go
// sandbox/sandbox.go:17
const DefaultTimeout = 30 * time.Second
const DefaultMaxOutputBytes int64 = 1 << 20  // 1 MiB

默认 30 秒超时、1MB 输出上限。 这是"防御性默认值",即使不配置,也有基本保护。

ExecResult,执行结果

go
// sandbox/sandbox.go:76
type ExecResult struct {
    Stdout   string
    Stderr   string
    ExitCode int
    TimedOut bool
}

ExitCode 非 0 不算 error,这是关键设计(下面讲)。


CodeTool:把 Sandbox 变成 Agent 工具

CodeTool(sandbox/tool.go:62)把任意 Sandbox 包装成 Agent 工具:

go
sb, _ := docker.New(docker.Options{Image: "python:3.12-slim"})
agent := &agents.Agent{
    Name: "coder",
    Tools: []agents.Tool{
        sandbox.CodeTool(sb, sandbox.CodeToolConfig{Name: "run_python"}),
    },
}

Agent 调 run_python 工具 → CodeTool 把代码写进沙箱 → 执行 → 返回 stdout/stderr。


关键设计:错误语义反转

这是 Sandbox 最有意思的设计。CodeTool 的错误处理(tool.go:84-89):

go
// sandbox/tool.go:84(概念)
if err != nil {  // 基础设施错误(Docker 挂了、容器启动失败)
    return nil, err  // → abort 整个 run
}
// ExitCode 非 0(代码本身的错误)
return &ExecResult{ExitCode: result.ExitCode, Stdout: result.Stdout, Stderr: result.Stderr}, nil
// → 作为"正常输出"喂回模型,让模型修代码

两种错误,两种处理:

错误类型例子处理
基础设施错误Docker 容器启动失败、SSH 连不上、沙箱崩溃abort run(模型修不了)
代码错误exit code 非 0、语法错、运行时异常回喂模型(模型可以修代码)

为什么这样分?

  • 基础设施错误是"系统级"的,Docker 挂了,模型再怎么改代码也没用。应该 abort,让人处理。
  • 代码错误是"模型可修的",SyntaxError、NameError,模型看到 stderr 能自己改代码重试。

这和第 29 章的 MCP 错误策略形成对比,MCP 错误默认全回喂,Sandbox 分两种。因为语义不同,MCP 工具的错误大多是"逻辑层"的(可自愈),Sandbox 的错误可能是"基础设施层"的(不可自愈)。

错误信息友好化

代码错误回喂时,不是直接丢 stderr,而是格式化成模型易懂的消息(tool.go:130 的 truncate):

text
代码执行失败(exit code 1):
Traceback (most recent call last):
  File "main.py", line 3
    print(x
          ^
SyntaxError: unexpected EOF

模型看到这个,知道是语法错误,下轮会修。 而且 stdout/stderr 用 UTF-8 安全截断(truncate 不切断多字节字符),防止输出含中文时被截成半个字。


三个后端

后端隔离性适合
LocalSandbox❌ 无隔离(直接在本机跑)可信代码、开发测试
Docker✅ 强隔离(容器)生产、不可信代码
SSH⚠️ 无本地隔离(但跑在远程机器)卸载到一次性 VM

LocalSandbox

go
sb := sandbox.NewLocal()

无隔离,直接在本机跑。只做了最小环境(PATH/HOME/TMPDIR,防 secret 泄漏)。明确标注"仅供可信代码/测试"。

它有个跨平台细节,Unix 上用 Setpgid 启动进程组(local_unix.go),超时时 kill 整个组防孙进程存活。这是 Unix 进程管理的经典技巧。

Docker(第 33 章详讲)

go
sb, _ := docker.New(docker.Options{Image: "python:3.12-slim"})

强隔离,容器内跑。默认无网络、只读 fs、丢弃 capabilities、非 root。第 33 章详讲纵深防御清单。

SSH

go
sb, _ := ssh.New(ssh.Options{Host: "vm.example.com", User: "agent"})

远程执行,代码传到远程机器跑(SFTP 传文件)。无本地隔离(代码在远程机器上自由跑),但好处是,即使代码搞破坏,也是搞坏远程的一次性 VM,不影响你的本机。


独立 module 设计

和 sessions 包一样,Docker 和 SSH 后端是独立 module(sandbox/docker、sandbox/ssh):

text
sandbox/          # 核心接口 + LocalSandbox(零依赖)
sandbox/docker/   # 独立 module(依赖 Docker SDK)
sandbox/ssh/      # 独立 module(依赖 golang.org/x/crypto/ssh)

不用 Docker/SSH 的人,只 go get sandbox,零额外依赖。 用 Docker 的人额外 go get sandbox/docker。这是 agents-go 一贯的"重依赖隔离"原则(第 10 章讲过)。


小结

读完这一章,你应该理解:

  1. Sandbox 接口极简,Exec + Close,ExecRequest 携带命令/文件/超时/限制
  2. 默认防御,30s 超时、1MB 输出上限
  3. 错误语义反转,基础设施错 abort,代码错回喂(模型可修)
  4. 三个后端,Local(无隔离)、Docker(强隔离)、SSH(远程)
  5. 独立 module,Docker/SSH 依赖不污染核心

Sandbox 让 Agent 安全地执行不可信代码。 下一章深入 Docker 后端的纵深防御清单。


下一篇

第 33 章,Docker 沙箱:纵深防御清单。Docker 后端的每一行 HostConfig 都有注释,无网络、只读 fs、CapDrop、nobody 用户、PID 限制、gVisor……这是容器安全的教科书级实现。


本文是《从 0 写 Agent》系列第 32 章,案例代码基于开源项目 agents-go(OpenAI Agents SDK 的 Go 移植版)。全系列目录见总览。

相关文章

分享: