首页
/ claude-cookbooks 实战:用纯 Docker 自托管 Sandbox 跑通 Claude Managed Agents 完整轮询链路

claude-cookbooks 实战:用纯 Docker 自托管 Sandbox 跑通 Claude Managed Agents 完整轮询链路

2026-09-07 16:13:24作者:蔡丛锟

导读

本文以 managed_agents/self_hosted_sandboxes/docker/ 目录下的参考实现为主线,完整讲解如何在你自己控制的主机上,用纯 Docker 为 Claude Managed Agents(CMA)会话提供自托管执行沙箱:宿主机运行 ant beta:worker poll 持续认领工作项,每个会话通过 --on-work 脚本拉起一个隔离的 per-session 容器来执行 ant beta:worker run。读完本文,你将掌握这一无云方案的架构分工、三份关键脚本的实现细节、环境变量契约、空闲回收策略,以及如何在沙箱内直接用 bash 工具查询 MongoDB,并能照搬文中命令在一台仅装有 Docker 的机器上完整跑通。

一图看懂运行模型:宿主机轮询 + 每会话容器

docker/ 变体的核心思路非常直观:把「轮询认领」和「真正干活」拆到两个进程层级

  • **宿主机(host)**直接运行 ant beta:worker poll。它是控制面侧的常驻轮询器,负责长轮询当前自托管环境(environment)下分配给它的工作项(work item),并在每认领到一个工作项时调用 --on-work 指定的脚本(即 on-work.sh)。
  • 每个被认领的工作项on-work.sh 通过 docker run 拉起一个 per-session 容器,容器入口是镜像内置的 ant beta:worker run。工具执行(bash/read/write/edit/glob/grep)、租约心跳、SSE 事件流、工作项退出时的强制停止等,全部由容器内的 CLI 负责。
  • 每个容器都会挂载一个 /workspace,即 Agent 的工作树(技能 skills 也下载到这里)。/workspace每会话独立的 Docker volume 提供支撑,因此同一会话跨多个容器仍然保留工作树与已下载的技能。

这份实现是 managed_agents/self_hosted_sandboxes/cf/(Cloudflare Containers 变体,运行同一个 ant beta:worker run 入口)的无云孪生版本:两者共用同一套 CLI、同一套环境变量契约,区别只在于前者跑在 Cloudflare Containers 上,而这里跑在你自己掌控的主机 Docker 上。整个 docker/ 目录只有三个文件,职责清晰:

文件 作用
managed_agents/self_hosted_sandboxes/docker/Dockerfile 定义 per-session 镜像:内置固定版本的 ant CLI、WORKDIR /workspaceENTRYPOINT ["ant","beta:worker","run",…]
managed_agents/self_hosted_sandboxes/docker/on-work.sh 被轮询器按工作项调用;读取环境变量、吞掉 stdin 上的 work JSON,然后前台拉起 --rm 的 per-session 容器
managed_agents/self_hosted_sandboxes/docker/start.sh 宿主侧启动脚本:先构建镜像,再以 --on-work on-work.sh 方式 exec 宿主轮询器

Dockerfile:构建一个「自带工具链」的 per-session 镜像

managed_agents/self_hosted_sandboxes/docker/Dockerfiledebian:12-slim 为基础,通过 ARGant CLI 版本固化在镜像层里:

FROM debian:12-slim
ARG ANT_VERSION=1.10.0
ARG TARGETOS=linux
ARG TARGETARCH=amd64

几点值得注意的设计:

  1. 版本锁定靠 ARG ANT_VERSION。镜像从 anthropics/anthropic-cli 的 GitHub Releases 下载 ant_${ANT_VERSION}_${TARGETOS}_${TARGETARCH}.tar.gz 并解压到 /usr/local/bin。宿主机上安装的 ant 必须与 Dockerfile 中这个 ANT_VERSION 保持一致(即同为 1.10.0),否则宿主与容器两端 CLI 契约可能错位,这是 start.sh 检查 ant 存在后仍建议你核对 ANT_VERSION 的原因。
  2. 预装 MongoDB 客户端python3python3-pymongo 让 Agent 能直接从 bash 工具里查询 MongoDB;python3-dnspython 也是必装的——因为 Atlas 的连接串几乎都是 mongodb+srv:// 格式,缺少它 pymongo 会直接抛 "The dnspython module must be installed to use mongodb+srv:// URIs"(该说明直接写在 Dockerfile 注释中)。
  3. 工作目录约定WORKDIR /workspace 把 Agent 的工作树锚定在容器内 /workspace,文件类工具的作用范围也以此为界。
  4. 入口即契约:容器入口固定为
ENTRYPOINT ["ant", "beta:worker", "run", "--workdir", "/workspace", "--unrestricted-paths", "--max-idle", "60s", "--log-format", "json"]

其中 --max-idle 60s 定义空闲回收策略,--log-format jsondocker logs 输出的运行日志是结构化 JSON,便于检索工具执行与技能下载记录。

on-work.sh:为什么必须「前台阻塞」而不是 docker run -d

managed_agents/self_hosted_sandboxes/docker/on-work.sh 是整个方案里最容易写错、也最有讲究的一个脚本。它的完整逻辑如下:

#!/usr/bin/env bash
set -euo pipefail

cat >/dev/null  # drain the work JSON on stdin

: "${ANTHROPIC_SESSION_ID:?on-work: ANTHROPIC_SESSION_ID not set by poller}"
: "${ANTHROPIC_ENVIRONMENT_ID:?on-work: ANTHROPIC_ENVIRONMENT_ID not set by poller}"
: "${ANTHROPIC_WORK_ID:?on-work: ANTHROPIC_WORK_ID not set by poller}"
: "${ANTHROPIC_ENVIRONMENT_KEY:?on-work: ANTHROPIC_ENVIRONMENT_KEY not set by poller}"

IMAGE="${CMA_IMAGE:-cma-self-hosted-sandbox-docker}"
NAME="cma-${ANTHROPIC_SESSION_ID}"     # session ids are docker-name-safe
VOLUME="cma-ws-${ANTHROPIC_SESSION_ID}"

if [ -n "$(docker ps -q --filter "name=^${NAME}$" 2>/dev/null)" ]; then
  echo "[on-work] session=${ANTHROPIC_SESSION_ID} already has a live container; skipping" >&2
  exit 0
fi
docker rm -f "$NAME" >/dev/null 2>&1 || true  # clear any exited leftover

echo "[on-work] session=${ANTHROPIC_SESSION_ID} work=${ANTHROPIC_WORK_ID} (running container in foreground)" >&2
exec docker run --rm --name "$NAME" \
  -v "${VOLUME}:/workspace" \
  -e "ANTHROPIC_BASE_URL=${ANTHROPIC_BASE_URL:-https://api.anthropic.com}" \
  -e "ANTHROPIC_ENVIRONMENT_KEY=${ANTHROPIC_ENVIRONMENT_KEY}" \
  -e "ANTHROPIC_AUTH_TOKEN=${ANTHROPIC_ENVIRONMENT_KEY}" \
  -e "ANTHROPIC_SESSION_ID=${ANTHROPIC_SESSION_ID}" \
  -e "ANTHROPIC_ENVIRONMENT_ID=${ANTHROPIC_ENVIRONMENT_ID}" \
  -e "ANTHROPIC_WORK_ID=${ANTHROPIC_WORK_ID}" \
  -e "MONGO_URI=${MONGO_URI:-}" \
  "$IMAGE"

它被轮询器按工作项调用的前后契约如下:

  • 轮询器把 ANTHROPIC_{WORK_ID,ENVIRONMENT_ID,SESSION_ID,ENVIRONMENT_KEY} 通过环境变量传入,把原始 work JSON 放在 stdin 上(脚本第一行 cat >/dev/null 将其排空;本实现用不到,只是保持契约兼容)。
  • ANTHROPIC_BASE_URL 从轮询器进程继承,未设置时回退到 https://api.anthropic.com

幂等保护与清理

会话 id 本身是 docker 命名安全的,因此脚本用 cma-${ANTHROPIC_SESSION_ID} 作为容器名、cma-ws-${ANTHROPIC_SESSION_ID} 作为 volume 名。首先检查同名单例容器是否仍在运行:

  • 仍存活 → 说明该会话已被正在跑的 ant beta:worker run 占有,重复工作项直接跳过(no-op),工作项租约自然过期,等待容器空闲退出后由下一次轮询重新认领;
  • 已退出残留docker rm -f 清掉,再重新拉起。

前台阻塞是硬约束,不是风格偏好

脚本最后用 exec docker run不带 -d)前台执行,并阻塞到容器退出。这一点在 on-work.sh 的注释与目录 README 中都给出了明确理由:ant beta:worker poll--on-work 脚本返回的那一刻就会对工作项发出停止指令(CLI 不提供关闭该行为的参数)。假如改用 docker run -d 让脚本立即返回,轮询器会抢在刚拉起的容器认领工作之前就把它停掉(表现为「heartbeat reports shutdown, state stopped」),bash 工具调用永远不会真正执行。因此 on-work.sh 必须替容器「续命」到 ant beta:worker run 自然结束。

--rm 保证容器空闲退出后被 Docker 自动移除;而挂载到 /workspace 的 per-session volume 会保留下来,供该会话的下一条消息使用。

双环境变量的玄机:为什么还要设 ANTHROPIC_AUTH_TOKEN

容器拿到的环境变量里,ANTHROPIC_ENVIRONMENT_KEYANTHROPIC_AUTH_TOKEN 被设成同一个值。原因在目录 README 与脚本注释中都有说明:

  • 轮询器用它认领工作,会话级调用(事件流、租约心跳、强制停止)都以 ANTHROPIC_ENVIRONMENT_KEY 为凭据,因此它必须原样传入容器;
  • 但 CLI 的技能下载客户端只解析 ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN,并不认识 ANTHROPIC_ENVIRONMENT_KEY。若不额外设置 ANTHROPIC_AUTH_TOKEN,技能会静默下载失败,且不容易察觉。

start.sh:一次构建、永久轮询的宿主启动器

managed_agents/self_hosted_sandboxes/docker/start.sh 负责宿主侧的全部装配:

set -euo pipefail
cd "$(dirname "$0")"

: "${ANTHROPIC_ENVIRONMENT_ID:?set ANTHROPIC_ENVIRONMENT_ID (env_...)}"
: "${ANTHROPIC_ENVIRONMENT_KEY:?set ANTHROPIC_ENVIRONMENT_KEY (sk-ant-oat...)}"
export ANTHROPIC_BASE_URL="${ANTHROPIC_BASE_URL:-https://api.anthropic.com}"

command -v docker >/dev/null || { echo "docker not found on PATH" >&2; exit 1; }
command -v ant >/dev/null || { ... }

IMAGE="${CMA_IMAGE:-cma-self-hosted-sandbox-docker}"
docker build -t "$IMAGE" .

exec env CMA_IMAGE="$IMAGE" MONGO_URI="${MONGO_URI:-}" \
  ant beta:worker poll \
    --on-work "$PWD/on-work.sh" \
    --workdir /tmp \
    --log-format json

它的职责与细节包括:

  1. 前置校验:要求 ANTHROPIC_ENVIRONMENT_ID(形如 env_...)与 ANTHROPIC_ENVIRONMENT_KEY(形如 sk-ant-oat...)必须显式设置;校验宿主机存在 dockerant 两个命令。
  2. 构建镜像:镜像名可用 CMA_IMAGE 覆盖,默认 cma-self-hosted-sandbox-docker
  3. 启动轮询器:用 exec 把自身替换为 ant beta:worker poll,通过 --on-work 把每个工作项委托给 on-work.sh。轮询进程本身不执行任何工具,因此它的 --workdir 指向一次性目录 /tmp 即可。CMA_IMAGEMONGO_URI 以环境变量的形式被继承进 on-work.sh 再到容器。轮询器收到 SIGTERM/SIGINT 会干净退出,并级联到正在运行的外层脚本。

需要提醒的是,关于 ant beta:worker pollant beta:worker run 的完整参数语义(环境变量契约、--on-work 的自定义外部脚本模式、--max-idle 等),可进一步参考 managed_agents/self_hosted_sandboxes/docs/usage-guide.md 中的 Flag 表格与示例。

空闲回收策略:60 秒后自动退出

空闲策略走的是 SDK/CLI 默认语义:每个容器在 session.status_idlestop_reasonend_turn 之后 60 秒退出(对应镜像入口的 --max-idle 60s);期间任何其他事件都会重置计时器。容器退出后:

  • --rm 自动移除该容器;
  • 名为 cma-ws-<session_id> 的 per-session volume 继续保留,供该会话下一条消息复用(/workspace 工作树与已下载技能不丢失);
  • 想要丢弃这份持久化状态,用 docker volume rm cma-ws-<session_id> 手动删除即可。

安全模型:全程只有一枚环境密钥

这套实现贯穿始终的原则是:任何地方都不出现组织级 API key,唯一凭据就是环境密钥(environment key)

  • 宿主轮询器用它认领工作;
  • 它被注入每个容器:既作为 ANTHROPIC_ENVIRONMENT_KEY(服务事件流、租约心跳、强制停止等会话级调用),又作为 ANTHROPIC_AUTH_TOKEN(让技能下载客户端能鉴权,见前文)。

也就是说,即便某个会话容器被攻破,攻击者拿到的也只是受限到单一环境的密钥,而非组织级凭据。宿主与容器之间的所有通信(长轮询、事件流)都走 ANTHROPIC_BASE_URL,默认指向 https://api.anthropic.com,也支持通过环境变量指向代理/中转网关做网络边界控制。

从沙箱查询 MongoDB:把 MONGO_URI 送进容器

自托管沙箱的典型增值场景是让 Agent 直接操作你的数据库。该镜像打包了 python3 + pymongo,而 on-work.sh 会把可选的 MONGO_URI 转发进每个 per-session 容器,于是 Agent 可以纯粹通过 bash 工具完成查询:

python3 -c 'import os; from pymongo import MongoClient; \
c = MongoClient(os.environ["MONGO_URI"]); \
print(list(c["mydb"]["mycoll"].find({}, {"_id": 0}).limit(5)))'

环境变量的传递链路

MONGO_URI 的流动路径是宿主 → 轮询器 → on-work.sh → 容器。只需在运行 ./start.sh 之前在宿主侧导出:

export MONGO_URI="mongodb+srv://<user>:<password>@<cluster>/"

注意 start.sh 会把 MONGO_URI="${MONGO_URI:-}" 注入轮询器环境,on-work.sh 再以 -e "MONGO_URI=${MONGO_URI:-}" 转发;未设置时以空字符串透传,容器内读取为空,属于无害的 no-op。

为什么这是「自托管」的独特优势

MONGO_URI你自己的数据库密钥,不属于 Anthropic。因为这是你自己构建并运行的容器,它只是一个普通的进程环境变量,永远不会到达控制面,也不会进入会话事件历史——这是自托管方案相对云沙箱的核心收益之一。对比之下,云端沙箱没有可用的环境变量或 vault 通道来放置数据库密钥,通常只能把凭据留在宿主侧、包进一个自定义工具中暴露。三种连接路径的完整对照,参见 managed_agents/self_hosted_sandboxes/docker/README.md 引用的 MongoDB on Claude Managed Agents 着陆页 以及 CMA_with_mongodb_atlas.ipynb(其 Section 1 对全部三条连接路径均有演示)。

最小权限与替代方案

Agent 的 bash 运行在容器内,所以它能读取 MONGO_URI——在信任任务本身的前提下这是没问题的。若需要最小权限,两个可选方向:

  1. 换掉内置工具集:不暴露原始 URI,而是用你自己的 worker 工具暴露一个窄接口 mongo_query(...),对 Agent 隐藏连接串;
  2. 使用 mongosh:更习惯 MongoDB Shell 的话,可在镜像中额外加入 mongosh(MongoDB 官方 apt 源),然后在容器内调用 mongosh "$MONGO_URI" --eval '…'

前置条件:Docker 与版本对齐的 ant CLI

运行本方案需要两样东西:

  1. Docker(宿主机可用 docker 命令即可);
  2. ant 在宿主 PATH 上,且必须与 DockerfileARG ANT_VERSION 锁定的构建一致(当前为 1.10.0)。

ant CLI 的官方安装片段如下(VERSION 必须与 Dockerfile 的 ANT_VERSION 保持一致):

VERSION=1.10.0   # must match ANT_VERSION in Dockerfile
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/')
curl -fsSL "https://github.com/anthropics/anthropic-cli/releases/download/v${VERSION}/ant_${VERSION}_${OS}_${ARCH}.tar.gz" \
  | sudo tar -xz -C /usr/local/bin ant
ant --version

脚本会根据操作系统与 CPU 架构自动拼出对应的下载地址(x86_64 → amd64,aarch64 → arm64),解压后单个 ant 二进制落到 /usr/local/bin

运行:两条命令看到容器被拉起

在完成「创建 self-hosted environment 并生成 environment key」的前置动作后(具体流程见 usage-guide),运行方式非常简短:

export ANTHROPIC_ENVIRONMENT_ID=env_...
export ANTHROPIC_ENVIRONMENT_KEY=sk-ant-oat...
./start.sh

随后创建一个指向该 ANTHROPIC_ENVIRONMENT_ID 的会话并向它发送一条消息。宿主终端应当按顺序看到两类日志:

[start] polling env=env_... base=https://api.anthropic.com
[on-work] session=sesn_... work=work_... container=... (started)

验证与观察手段

  • 容器日志用 docker logs cma-sesn_... 查看,里面是 ant beta:worker run 输出的 JSON(--log-format json),能看到类似 downloaded skill …executing tool … 的记录;
  • 容器在空闲超时后自我移除;cma-ws-<session_id> volume 保留给该会话下一条消息;确认不再需要时可 docker volume rm 丢弃。

无需暴露任何公网端口

与基于 webhook 驱动的演示(Modal / Daytona / Vercel / Cloudflare)不同,本方案没有任何 webhookant beta:worker poll 直接长轮询环境本身,因此宿主机不需要向公网暴露任何端口。这是它适合「受控主机 + 内网数据」场景的根本原因。

与仓库其他变体的关系

docker/ 只是 self-hosted sandbox 参考实现的一支。目录 README(managed_agents/self_hosted_sandboxes/README.md)给出了完整变体矩阵:docker/(本机 Docker)、cf/(Cloudflare Containers)、cf-worker/(无容器的 Cloudflare Workers + 进程内伪文件系统)、modal/daytona/vercel/。所有变体实现的是同一套契约:接收 session.status_run_started webhook → 排空环境工作队列以兜底错过的工作项 → 按工作项拉起一个以 SDK/CLI 工具运行器为核心的 per-session 沙箱。若想对比「同一入口、换一个计算供应商」的写法,docker/ 的无云对照物是 managed_agents/self_hosted_sandboxes/cf/README.md

如果不需要容器级隔离、希望进一步理解 poll/run 两阶段 worker 在 SDK 层的 library 用法(.worker(...) 组合循环、beta_agent_toolset_20260401 工具集定制等),可继续阅读 managed_agents/self_hosted_sandboxes/docs/usage-guide.md。升级 SDK 版本时的迁移注意点则在 managed_agents/self_hosted_sandboxes/docs/upgrade-guide.md

小结

把本文的内容浓缩成一张可直接照做的清单:一份锁定 CLI 版本的 Dockerfile 决定容器内能做什么(内置工具链 + MongoDB 客户端 + 60 秒空闲回收);一个前台阻塞的 on-work.sh 保证工作项不会被轮询器过早停掉,同时用双环境变量打通技能下载鉴权;一个只跑轮询不跑工具的 start.sh 负责装配与启动。三者组合,就得到了一套「零公网暴露、单密钥鉴权、每会话隔离、状态跨容器持久化」的自托管 CMA 沙箱,并且可以直接利用注入的 MONGO_URI 让 Agent 在 bash 里安全地查询你的数据库。

登录后查看全文
热门项目推荐
相关项目推荐