Skip to main content

sandbox-agent

把 LLM agent CLI 关进一次性 Docker 容器里跑。

agent 会写文件、跑命令、装依赖。让它在你的开发机上直接干这些事是个坏主意。 这个库给它一个用完就扔的容器,并把结果安全地取出来。

零运行时依赖,只要求宿主机有 docker。

from sandbox_agent import SandboxAgent, OpenCodeDriver

driver = OpenCodeDriver(provider="zhipuai-coding-plan", api_key=API_KEY)

with SandboxAgent(driver=driver, model="zhipuai-coding-plan/glm-5.3") as agent:
    print(agent.ask("用 Python 写个快排, 存到 /tmp/qs.py"))

    code, out = agent.container.exec(["python", "/tmp/qs.py"])  # 验收产出
    agent.container.download("/tmp/qs.py", "./qs.py")           # 取回宿主机
# 退出 with 块 -> 容器销毁

安装

uv add sandbox-agent      # 或 pip install sandbox-agent

还需要一个装好了 agent CLI 的镜像,见 docker/ 与 .ci/build_docker.sh。

包和镜像的对应关系

装了哪个版本的包,就唯一确定了该拉哪个镜像。 不靠约定,靠推导:

pip install sandbox-agent
  └─ wheel 里带着 sandbox_agent/versions.json
       claude_version = 2.1.235
       image_date     = 20260819
     └─ donaldtrump/sandbox-agent:claude-2.1.235-20260819

例子里的数字会过时。装好的包指向哪个镜像,直接问它:

python -c "from sandbox_agent import versions; print(versions.CLAUDE_IMAGE)"

tag 格式就是 {repo}:{cli}-{cli版本}-{image_date},三段都来自那一份 versions.json——构建脚本和 Python 运行时读的是 同一份,两边各存一份就会得到一个名字和内容对不上的镜像,而且不报错。

几条刻意的选择:

  • 包版本不进 tag。 快照内容已经唯一确定了 tag,再把「这是哪个快照」编码进去是 多绕一圈。反过来 pyproject.toml 也不读 versions.json:打包版本是打包的事, 镜像日期是运行时的事,粘在一个文件里只会互相绑架。
  • image_date 是手写的声明,不是构建时观测的。 它管的是「哪个 CLI 都没升, 但基础镜像要重建」那种情况——CLI 版本号挡不住,日期能,而且比 -1 -2 有信息量。 构建脚本里写 date +%Y%m%d 会让运行时算出的 tag 指向一个从没构建过的镜像, 且不报错,所以日期只读不算(有测试盯着)。
  • 不用 latest。 同一份代码在不同时间跑出不同行为,「验证过」三个字就没了意义。 构建脚本只推钉死的 tag。

代价是人得记得改日期,所以 build_docker.sh --push 会先查 registry,tag 已存在就 拒绝推送(同一天反复调试用 --force);build_pypi.sh --push 会反过来查每个镜像 是否都已就位,缺一个就拒绝发布。发布顺序被钉死了:先镜像,后包。

不传 image= 时用 driver 自己的默认镜像——每个 agent CLI 有自己的镜像,一个全局 默认值只能对其中一个是对的。优先级:显式 image= > SANDBOX_AGENT_IMAGE_NAME (换私有 registry 用)> driver.default_image > 内置默认值。

本地 LLM 服务(可选)

runtime/litellm/ 是一个独立的 compose 栈,把一个 OpenAI 兼容的上游 (智谱 GLM)包装成 chat/completions、Responses、Anthropic Messages 三种协议, codex 和 claude 都能直接接:

cd runtime/litellm && docker compose up -d   # 要先 export ZHIPUAI_CODING_TOKEN, 见那边 README

跟本库的 SandboxAgent 没有代码耦合,各起各的。想让沙箱容器用上它,见那边的 README。

能干什么

对话 chat() 拿可迭代的流式事件,推进主线;ask() 开旁支问一句,问完即扔
会话 sessions.list() / .fork() / .dump() / .load() —— 会话能跨容器搬
容器操作 container.upload() / .download() / .read() / .write() / .exec() —— 往外拿东西不信任容器:只落普通文件和目录,链接不落地
能力注入 skills.add() / .add_many() / .list(),rules.install()
可观测 token_usage、AgentEvent 流、按 tag 筛容器
可审计 每次会话自动归档到 /tmp/sandbox-agent-<uid>/(0700),默认开

驱动内置 OpenCodeDriver、CodexDriver、ClaudeCodeDriver、PiDriver 与 DshDriver。接别的 agent CLI 就继承 BaseDriver 实现那 11 个抽象成员。

driver 能碰容器的只有三个原语(ContainerOps:run / read_file / write_file)——driver 不该知道 docker 存在。这也意味着 driver 的单测只要伪造三个 方法,不用 mock 一个上千行的类。

想连容器一起换(换成 podman、换成远程执行器),那是另一个扩展点:按 ContainerRuntime 那 11 个方法写一个,传给 SandboxAgent(runtime_factory=...)。

完整 API 见 docs/sandbox_agent.md,可跑的完整示例见 main.py:

uv run python main.py            # 全套 opencode demo, 十几个容器几分钟
uv run python main.py --fast     # 只跑第一个, 够验证"改完还能跑"
uv run python main.py --claude   # 只跑 ClaudeCodeDriver 那个
uv run python main.py --codex    # 只跑 CodexDriver 那个

前两档要 ZHIPUAI_CODING_TOKEN 和装好 opencode 的镜像;--claude / --codex 要对应镜像和 runtime/litellm/ 那套网关——main.py 本身不读 token,网关读。

会话审计

每次会话自动落盘,事后能查 agent 到底做了什么。默认开着——出事之后才想起来 打开的开关等于没有。

/tmp/sandbox-agent-1000/20260819/ses_9f2/
    events.jsonl    抽象事件,一行一个
    session.jsonl   原始 stdout 行,逐字节照抄
    status.json     元数据 + 每一轮的结果

两个文件回答两个不同的问题:

# agent 干了什么 —— 各 CLI 归一之后的形式,读这个就够
jq -r '"\(.kind)\t\(.payload.command // .payload.path // .payload.text)"' events.jsonl
tool.exec   printf 'sandbox-agent-io-ok\n' > /workspace/_claude_demo.txt
text        done
skill       Launching skill: zebra-protocol
text        zebra code 是 ZC-8842。house rule 是 HR-7391。

工具调用、参数、结果、最终回答——「agent 干了什么」一条不漏。

thinking 事件:取决于后端,真 Anthropic API 上拿不到

EventKind 里有 thinking,但它在真 Anthropic API 上一条都不会产生。实测 (非流式 / 流式 SSE / 真 CLI 过沙箱,三条路径一致):

content 块类型: ['thinking', 'text']
thinking 键   : ['signature', 'thinking', 'type']
thinking 长度 : 0                     ← 正文是空的
usage.thinking_tokens: 77             ← 但它确实思考了 77 个 token

模型思考了、计费了、块也返回了,正文被抹掉,只留一个 signature(那是给多轮 续接做完整性校验的,不是给人读的)。所以 thinking 事件只在后端肯给推理正文时 才有内容:直接接 DeepSeek 官方 API 的 PiDriver / DshDriver(reasoning_content 原样带回来),或者 runtime/litellm/ 那套把 OpenAI 兼容上游接成 Anthropic 协议的网关(接 GLM 时实测带 thinking 正文)。

有内容时,多个 thinking 块按原顺序各成一条事件,不合并:那个「想 → 调工具 → 再想」 的交错顺序本身就是信息。

# driver 归对了没有 —— 某条事件是从哪一行解析出来的
sed -n "$(jq -r 'select(.kind=="other") | .line' events.jsonl)p" session.jsonl

session.jsonl 是逐字节照抄,不做任何过滤。system/thinking_tokens 那种进度 心跳照样全量落盘——真 Anthropic API 下一轮只有个位数,但经网关转发推理模型时能占到 97% 的行。那是它的原始输出,我们不替你判断什么重要。要过滤是读的时候的事:

jq -c 'select(.subtype != "thinking_tokens")' session.jsonl

events.jsonl 的每条带一个 line 字段回指 session.jsonl 的行号——两个文件行数 必然对不上(step_start 这类不产出事件),一个整数把对应关系还回来。

同一个 session_id 的多轮对话落进同一个目录,两个 jsonl 跨轮追加。流式写入, 跑着的会话可以直接 tail -f。

SandboxAgent(driver=driver, audit=False)     # 显式关闭

目录和保留天数是 audit 模块的两个常量:

from sandbox_agent import audit
audit.AUDIT_ROOT = "/data/sandbox-audit"
audit.RETENTION_DAYS = 30                 # 0 = 永不清理

清理按天数:早于 today - N 的整个日期目录删掉,每进程跑一次。日期分区让这件事 退化成一次 listdir 加比字符串,不用 walk 也不用 stat;名字不像日期的目录一概不碰。

归档失败不会影响 chat。 可观测性功能不能反过来搞死被观测的东西:磁盘满了就发 一条 RuntimeWarning 然后自己降级成 no-op,该跑的照跑。

ask() 的旁支在容器里删了,归档留着。 审计的意义就是留痕。

容器生命周期

容器闲置 keepalive_seconds(默认 3600s)后自杀,不是"最多活一小时"—— 只要还有命令在跑就会一直续期。正常路径下 close() / with 退出即销毁, 自杀只是兜底,防止调用方崩溃后留下垃圾。

配置

优先级:环境变量 > ~/.config/sandbox-agent/config.json > 内置默认值。

环境变量 JSON key 默认值
SANDBOX_AGENT_IMAGE_NAME image driver 自己的 default_image(见下)
SANDBOX_AGENT_RESOURCE_PREFIX prefix sandbox-agent
SANDBOX_AGENT_CONTAINER_MEMORY memory 1g
SANDBOX_AGENT_CONTAINER_CPUS cpus 1.0
SANDBOX_AGENT_KEEPALIVE_SECONDS keepalive_seconds 3600(0 = 不自杀)
SANDBOX_AGENT_MAX_CONTAINERS max_containers 20
SANDBOX_AGENT_START_TIMEOUT start_timeout 120
SANDBOX_AGENT_STOP_TIMEOUT stop_timeout 30
SANDBOX_AGENT_EXEC_TIMEOUT exec_timeout 60
SANDBOX_AGENT_CHAT_TIMEOUT chat_timeout 300
SANDBOX_AGENT_SESSION_TIMEOUT session_timeout 120
SANDBOX_AGENT_TRANSFER_TIMEOUT transfer_timeout 300

所有带 timeout 的方法参数一律 None = 用这类操作的配置预算。落到哪一项取决于 操作的性质:短命令是常数耗时,会话操作随会话长度增长,传输随字节数增长 —— 三种预算, 一条规则。

开发

uv sync --all-extras

uv run pytest                    # 单元测试, 全 mock, 不碰 docker
uv run pytest -m integration     # 集成测试, 起真容器, 约 100s

集成测试默认跳过。它们用 debian:stable-slim(不需要 agent CLI), 可用 SANDBOX_AGENT_TEST_IMAGE 换掉。busybox 的 timeout 超时不返回 124, 别拿 alpine 顶替。

镜像必须事先拉好——测试不会替你 pull(那会让一次"只跑单测"的命令悄悄卡在 一个几百秒的下载上)。没拉的话集成测试会跳过,并在输出里给出要敲的命令:

docker pull debian:stable-slim

已知限制与后续计划见 TODO.md,变更历史见 CHANGELOG.md。

发布

顺序不能反——包里的 versions.json 指向镜像,镜像还没推包就发不出去 (build_pypi.sh 会拒绝):

# 0. 改了 CLI 版本或动了 Dockerfile.base? 先 bump src/sandbox_agent/versions.json
#    的 image_date —— 它是手写的,没人替你算
./.ci/build_docker.sh --push     # 1. 镜像(tag 已存在会拒绝,加 --force 覆盖)
./.ci/build_pypi.sh   --push     # 2. 包(镜像不齐会拒绝)

License

GPL-3.0-or-later

Metadata

Release files for sandbox-agent 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sandbox-agent 0.2.1
File Size Uploaded
sandbox_agent-0.2.1.tar.gz 153.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sandbox-agent 0.2.1
File Interpreter ABI Platform
sandbox_agent-0.2.1-py3-none-any.whl Python 3 none any Details

Total release size: 304.7 kB

Release files / sandbox_agent-0.2.1.tar.gz

Download URL sandbox_agent-0.2.1.tar.gz
Size 153.2 kB
Tags Source
SHA-256 checksum
How to use checksums
162a97b9d7fb9c0a58a3cc8e1000fa59e62eab99911979cea388c959d2485f90
BLAKE2b-256 checksum
How to use checksums
636a103a3aa0df872b7f24e455a50801f6db219eef26ff014e25aa97db809cc9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / sandbox_agent-0.2.1-py3-none-any.whl

Download URL sandbox_agent-0.2.1-py3-none-any.whl
Size 151.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bf328242637190e53ffd1fd236a25f2a83d88e3a2584a3cad159e73721885e7c
BLAKE2b-256 checksum
How to use checksums
0c019f1d19452901fb993b55d102b21275f62c5e533253702237ff1d5265385e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.13.1 {"installer":{"name":"uv","version":"0.13.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

0.2.2

2 release files

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.0.14

2 release files

0.0.13

2 release files

0.0.12

2 release files

0.0.11

2 release files

0.0.10

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.2

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page