数据截至 (上游 commit 0f23e3c9395c)
运行时与入口
本章讲「这台机器怎么开机、怎么进门」:一条 docker run 启动什么、对外那个端口如何分流、启动过程的三段 hook、以及鉴权怎么做。
1. 一条命令启动整台机器
最小启动就一行(README.md:38-41):
docker run --security-opt seccomp=unconfined --rm -it \
-e SANDBOX_API_KEY=your-secret-key \
-p 127.0.0.1:8080:8080 ghcr.io/agent-infra/sandbox:latest
几个要点:
-
seccomp=unconfined:容器内要跑浏览器 / 多种运行时,需要放宽默认 seccomp 限制。 -
-p 127.0.0.1:8080:8080:容器内服务监听0.0.0.0:8080,但宿主侧故意只绑到127.0.0.1——默认只让本机访问。原文点明这是有意为之:These examples intentionally bind the host side to
127.0.0.1because the sandbox listens on0.0.0.0inside the container.(README.md:61) -
SANDBOX_API_KEY:设了就开启 API Key 鉴权(保护 API/JupyterLab/VNC);不设则服务开放(向后兼容)。
启动后,各入口都挂在这一个端口上(README.md:63-67):
| 入口 | URL | 给谁 |
|---|---|---|
| API 文档 | /v1/docs | 开发者 |
| VNC 浏览器 | /vnc/index.html?autoconnect=true | 人(看桌面) |
| VSCode | /code-server/ | 人(写代码) |
| MCP 服务 | /mcp | agent |
2. 一个端口,按路径前缀分流
整套设计的入口形态是:对外只有一个 :8080,内部按 URL 路径前缀把请求路由到不同子系统。这一点从 SDK 列出的全部子系统命名空间能直接看到——Sandbox 类把 20 个子客户端挂成属性(sdk/python/agent_sandbox/client.py:106-264):
sandbox shell bash file jupyter nodejs code
mcp browser browser_page browser_tabs browser_cookies
browser_state browser_network browser_captcha
util skills proxy display auth
每个子系统对应一组 /v1/<名字>/... 端点。例如 shell 子系统的真实路径(sdk/python/agent_sandbox/shell/raw_client.py,符号 RawShellClient):v1/shell/exec、v1/shell/sessions/create、v1/shell/terminal-url 等。SDK 是服务端 API 契约的精确镜像——它由 Fern 从 API 定义自动生成(sdk/python/agent_sandbox/client.py:1 的 auto-generated 注释 + sdk/fern/),所以读 SDK 的 raw_client.py 就等于读 API 的方法/路径/参数表。
3. 沙箱上下文与 llms.txt(给 agent 的「机器自述」)
agent 进门第一件事通常是问「这是台什么机器」。client.sandbox.get_context() 返回一段自然语言的环境描述(website/docs/en/guide/basic/sandbox.mdx:110-160),内容包括系统/用户/home 目录/已占用端口、已装的运行时(Python、Node、Git、uv…)和可用工具(vim、jq、yt-dlp…)。其中关键字段:
home_dir:沙箱里的 home 目录(如/home/gem),后续读写文件都基于它。- 已占用端口列表:让 agent 知道哪些端口被内部服务占了。
另外沙箱在 /llms.txt 暴露一份给 agent 的使用手册(website/docs/en/guide/basic/sandbox.mdx:99-103):
llms.txtis an usage manual for Agents. Through this manual, the Agent can better use the sandbox.
这是一种「让环境自我描述给模型」的约定:agent 读一遍 llms.txt 就知道该怎么用这台机器,而不必把用法硬编码进 prompt。
4. 启动生命周期:三段 hook
容器启动不是一锤子,而是分阶段,并在阶段之间留出hook 注入点(website/docs/en/guide/advanced/lifecycle.md):
容器启动
→ RUN_HOOK_INIT 最早的初始化(服务准备之前必须跑的)
→ 准备运行用户与 workspace
→ RUN_HOOK_PRE_SERVICES 托管服务启动前:装工具 / 写配置
→ 托管服务启动
→ 就绪检查通过
→ RUN_HOOK_POST_READY 预热:需要 API/浏览器/code-server 已可用时
三个 hook 各管一段(website/docs/en/guide/advanced/lifecycle.md「Hooks」表):
| Hook | 用于 |
|---|---|
RUN_HOOK_INIT | 必须在服务准备前跑的最早初始化 |
RUN_HOOK_PRE_SERVICES | 在受管服务启动前装工具 / 写配置 |
RUN_HOOK_POST_READY | 需要 API/浏览器/code-server 已就绪的预热任务 |
用法就是启动时塞环境变量,例如先装个 Python 包:
docker run --security-opt seccomp=unconfined --rm -it -p 127.0.0.1:8080:8080 \
-e RUN_HOOK_PRE_SERVICES="python -m pip install requests" \
ghcr.io/agent-infra/sandbox:latest
运行时还能注册关闭 hook。 沙箱跑起来后可以通过 API 注册 shutdown 事件 hook,用来在退出前刷状态 / 写标记(website/docs/en/guide/advanced/lifecycle.md「Runtime Shutdown Hooks」;对应端点 POST/GET/DELETE v1/sandbox/hooks,见 sdk/python/agent_sandbox/sandbox/raw_client.py):
curl -X POST "http://localhost:8080/v1/sandbox/hooks" \
-H "Content-Type: application/json" \
-d '{"name":"write-shutdown-marker","event":"shutdown",
"command":"printf stopped > /home/gem/workspace/shutdown.txt",
"timeout":10,"priority":100}'
注意: hook 的具体编排逻辑(谁先跑、超时怎么处理)在容器镜像内部,本 repo 看不到;上面是文档与 API 契约层面的事实,内部实现 (inferred)。
5. 鉴权:两条路
沙箱默认面向可信的本地开发;一旦要暴露给网络或别的服务,就得加鉴权(website/docs/en/guide/advanced/security.md)。有两种机制:
(a) API Key(简单)。 设 SANDBOX_API_KEY 后,三种方式都能带 key(README.md:35-40):
X-AIO-API-Key请求头Authorization: Bearer <key>请求头?api_key=查询参数(给那些没法带 header 的场景,如浏览器直开 URL)
(b) JWT 公钥验签(适合多服务架构)。 设 JWT_PUBLIC_KEY(base64 编码的公钥),你的业务后端用私钥签发 JWT,沙箱用公钥验签(website/docs/en/guide/basic/authentication.md、website/docs/en/guide/advanced/security.md):
# 1) 生成密钥对
openssl genrsa -out private_key.pem 2048
openssl rsa -in private_key.pem -pubout -out public_key.pem
# 2) 启动时把公钥交给沙箱
docker run ... -e JWT_PUBLIC_KEY="$(base64 -w 0 public_key.pem)" ghcr.io/agent-infra/sandbox:latest
# 3) 调 API 带 Bearer token
curl "http://localhost:8080/v1/sandbox" -H "Authorization: Bearer $SANDBOX_TOKEN"
短时票据(short-lived ticket)。 对于浏览器访问、临时交接这类「没法塞 header」的流程,文档建议用快速过期、最小权限的短时票据,通过 ?ticket= 传(website/docs/en/guide/basic/authentication.md「Short-lived Ticket」、website/docs/en/guide/advanced/security.md)。VNC 这种纯 URL 访问就靠它。
鉴权发生在入口层。 三种入口(REST API / JupyterLab / VNC)由同一把 key 保护(README.md:36),所以鉴权逻辑统一在 :8080 入口,而不是每个子系统各做一遍 (inferred)。
6. 网络与密钥的硬规矩
安全文档给的是一串「别做什么」(website/docs/en/guide/advanced/security.md):
- 非必要不暴露:默认绑 localhost;共享环境前面套带 TLS 的反向代理;按 IP/网络/服务身份限制入站。
- 绝不把未鉴权的沙箱 API 暴露到公网。
- 密钥走运行时环境变量或 secret 管理器,不要烤进镜像层;不要把长期密钥写进 Skills / hooks / notebook / 生成文件;优先短期凭证。
7. 代码地图
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 全部子系统命名空间 | sdk/python/agent_sandbox/client.py:106-264 | Sandbox 各 @property |
| 沙箱上下文 / hooks / observe 端点 | sdk/python/agent_sandbox/sandbox/raw_client.py | v1/sandbox, v1/sandbox/hooks, v1/sandbox/observe/* |
| 沙箱上下文与 llms.txt | website/docs/en/guide/basic/sandbox.mdx:97-160 | get_context, home_dir, /llms.txt |
| 生命周期 hook | website/docs/en/guide/advanced/lifecycle.md | RUN_HOOK_INIT/PRE_SERVICES/POST_READY |
| JWT 鉴权流程 | website/docs/en/guide/basic/authentication.md | JWT_PUBLIC_KEY |
| 安全边界 / 密钥规矩 | website/docs/en/guide/advanced/security.md | — |
| API Key 三种传法 | README.md:35-40 | SANDBOX_API_KEY, X-AIO-API-Key |