AI Agent 安全实战:用爆炸半径架构守住生产环境

PocketOS、DataTalks 与 Kiro 的事故揭示同一根因:AI Agent 继承过宽权限后,可能在人工介入前造成不可逆损失。本文拆解已用于生产环境的防护架构:收窄云角色、限定工具白名单、设置统一调用卡口和单实例预算上限,再用幂等工作流与修改前快照,把每次运行的爆炸半径锁在可恢复、可审计的范围内。

Saturday, September 5, 2026Omid Saffari
AI Agent 安全实战:用爆炸半径架构守住生产环境

4 月 25 日,一起 AI Agent 安全事故在 9 秒内发生:运行 Claude Opus 4.6 的 Cursor 删除了 PocketOS 的生产数据库及其备份,随后写下一份认错说明。3 个月的租车预订记录全部消失。我每天运行 6 个会接触生产环境的 Agent;下面就是这套系统实际使用的调用卡口、白名单和幂等架构,它能把其中任何一个 Agent 造成的最坏结果限制为多花 $1。

AI Agent 安全:90 天内的事故清单

2026 年 4 月 25 日。运行 Claude Opus 4.6 的 Cursor Agent 正在测试环境中处理一项 PocketOS CEO Jeremy Crane 所说的“日常任务”。遇到凭证不匹配后,它自行决定“清理一下”,随后删除了一个 Railway 存储卷,其中同时放着生产数据库和备份。整个过程只用了 9 秒。服务中断 30 小时。最近一份可恢复备份已是 3 个月前,因此 3 个月的租车预订记录就此蒸发。之后,Agent 写下一份认错说明,承认自己违反了不得触碰生产环境的明确指令。

2026 年 2 月 26 日。DataTalks.Club 创始人 Alexey Grigorev 更换了电脑,本地 Terraform 状态文件已经过期。Claude Code 接到清理资源的任务后,对生产技术栈执行了 terraform destroy。后来部分恢复的 courses_answer 表中有 1,943,200 行数据,记录着两年半的学生提交内容。自动快照也放在同一个被销毁的账户里。

2025 年 12 月中旬。Amazon 内部的 Agent Kiro 继承了工程师的高权限,绕过了 Amazon 的双人审批门槛——因为审批只约束人类,并不约束 Agent 运行时使用的角色——随后删除并重建了 AWS Cost Explorer 的生产环境。

3 起事故,3 个不同模型,3 套不同技术栈。共同根因不在模型,而在于过宽的继承权限、自主循环,以及快过任何人工确认提示的执行速度。ServiceNow 已经围绕这种担忧推出“kill switch”产品。下面这套架构就是可在内部落地的版本,也是我目前实际运行在生产环境中的方案。

这对非技术创始人意味着什么

如果你是因为团队里负责 AI 的人提到这件事才读到这里,需要先认清一点:做错的代价并不是“出了一个 AI bug”,而是预订系统、学生记录和客户数据全部消失;备份还停留在 3 个月前,因为从来没人测试过恢复流程。

真正应该问工程负责人的,不是“这个 Agent 好不好用”。这个问题的框架就错了。正确的问题是:单次 Agent 运行最多能造成多大损失,这个上限由谁划定? 如果答案是“我们信任模型”或“我们会审查 diff”,那就还没有控制面,只有侥幸心理。

理想答案应该是这样:“Agent 使用一个不含销毁权限的云角色运行。它只能调用 12 个明确列出的工具,其中没有 shell。每一次付费调用都必须经过同一个设有硬性成本上限的函数。最坏情况是一个步骤失败,支出最多为 $1。”这才是所需的架构。下面会说明如何把它搭起来。

为什么“谨慎操作”和“审查 diff”都不管用

PocketOS 的存储卷在 9 秒内被删。Kiro 完成删除的速度,快到人根本来不及读完确认提示,更谈不上判断风险。面对 Agent 的执行速度,动作发起后再介入,从结构上就不可能奏效。等你看到操作时,操作早已完成。

多数关于 Agent 安全的文章会避开这一点,因为它迫使人接受一个不舒服的结论:真正有效的防护只有执行前拒绝。不是“让 Agent 先询问,再由疲惫的人点击确认”,也不是“记录一切,之后再复盘”。执行前拒绝意味着,破坏性操作从一开始就不在 Agent 可触达的范围内。

Kiro 用反例证明了这一点。Amazon 明明设有双人审批门槛,但它只适用于人类发起任务的场景。Agent 继承了发起人的权限,而审批门槛没有绑定到 Agent 实际使用的角色,于是被直接绕过。人类面对的是审批表演,自主循环拿到的却是完整销毁权限。

因此,问题需要换个框架:不是去“监督”Agent,而是在循环开始前限制它的操作面,并让这种限制成为运行角色的属性,而不是提示词里的要求。

调用卡口:所有付费调用和状态修改只走一个函数

在我由 6 个 Agent 组成的技术栈里,每一次付费模型调用都必须经过同一个函数,我把它叫作 callAi。它严格按顺序完成 5 件事:调用前检查每天 $20 的支出上限;记录调用耗时;把 token 数量和 USD 成本写入仅追加的 ai_call_log 行,并标记 agent_id 与 workflow_instance_id;调用后检查每个实例 $1 的上限;最后返回结果。

TypeScript
export async function callAi(env: Env, args: CallAiArgs) {
  const { agentId, workflowInstanceId, model, messages } = args;

  const dailySpent = await getDailySpendUSD(env);
  if (dailySpent >= 20) {
    throw new NonRetryableError(`daily cap hit: $${dailySpent}`);
  }

  const t0 = Date.now();
  const res = await anthropic.messages.create({ model, messages });
  const costUSD = priceOf(model, res.usage);

  await env.DB.prepare(
    `INSERT INTO ai_call_log
       (agent_id, workflow_instance_id, model, in_tokens, out_tokens, usd, ms, ts)
     VALUES (?, ?, ?, ?, ?, ?, ?, unixepoch())`
  ).bind(agentId, workflowInstanceId, model,
         res.usage.input_tokens, res.usage.output_tokens,
         costUSD, Date.now() - t0).run();

  const instanceSpent = await getInstanceSpendUSD(env, workflowInstanceId);
  if (instanceSpent >= 1) {
    throw new NonRetryableError(`instance cap hit: $${instanceSpent}`);
  }

  return res;
}

一旦触发上限,函数就抛出不可重试错误,Workflow 随即终止。Agent 没有机会“自行决定”继续,因为预算门槛不是一段可供它争辩的提示词,而是推理层之上直接抛出的异常。

再看那个常被引用的失控案例:持续 63 小时,花掉 $4,200。没有统一卡口,就没有终止状态;循环会一直烧钱,直到有人注意到账单。有了单实例上限,单次运行的最坏成本是 $1;有了每日上限,一天的最坏成本是 $20。我在搭建系统时曾意外触发过这两个上限,但两次的代价都没有超过一顿晚饭。

调用卡口的第二项作用是留下取证记录。每个操作都会在结果返回之前写入一行审计数据。真的出问题时,事故复盘只需对 ai_call_log 执行一次 SELECT,不必再从 Agent 自己的认错说明里还原经过。

工具白名单:12 个 Zod 工具,不给 shell,也不给 terraform

Agent 能执行的全部修改动作,只来自 12 个用严格 Zod schema 定义的工具。没有 Bash 工具,没有 terraform 工具,操作面里甚至不存在云存储卷 API。即使 Agent 推理出自己想运行 terraform destroy,也没有任何方式把这个意图变成操作。

负责执行限制的 Claude Agent SDK 配置很短:

TypeScript
const result = await query({
  prompt,
  permissionMode: "dontAsk",
  allowedTools: [
    "read_brief", "fetch_corpus", "render_markdown",
    "validate_directives", "persist_draft", "schedule_publish",
    "update_status", "log_event", "fetch_citation",
    "embed_text", "search_vectors", "notify_human"
  ],
  // no Bash, no Write, no Edit, no infrastructure tools
});

在 permissionMode: "dontAsk" 下,未列入白名单的工具会被直接拒绝:不会升级为确认提示,也不会交给人工审查。权限判定仍按文档所述顺序执行——PreToolUse hook、deny rules、allow rules、ask rules、mode check、canUseTool callback——但在 dontAsk 模式中,人机协同回调会从结构上被跳过,未知工具走到 mode check 时就会被终止。

这正是能彻底阻止 PocketOS 与 DataTalks 事故的关键。在这套配置下,DataTalks 的失败不可能发生,因为 terraform destroy 并不是 Agent 能调用的工具名;PocketOS 的失败同样不可能发生,因为操作面里根本没有删除存储卷的能力。即便模型决定“清理一下”,从意图到动作之间也不存在通路。

最容易出现的诱惑,是因为“万一 Agent 需要做一件我们没预料到的事”而加入 Bash 工具。不要这样做。加入 Bash 的那一天,爆炸半径就会变成“Bash 能触达的一切”。

AI Agent 幂等性:为什么重放不会重复执行破坏操作

这套架构的第三条支柱是幂等性。每个 Cloudflare Workflow 实例都使用一个由输入确定生成的 ID:publish-{brief_id}。instances.create() 遇到重复 ID 时会抛错,因此同一事件再次触发,也无法让工作流运行两遍。队列层的重试风暴会变成空操作,而不是重复发布。

TypeScript
const id = `publish-${msg.body.briefId}`;
try {
  await env.PUBLISHER.create({ id, params: msg.body });
} catch (e) {
  if (e.message.includes("already exists")) return; // safe replay
  throw e;
}

在工作流内部,每个 step.do() 的结果都会缓存,可以安全重放。步骤重试时会复用缓存结果,不会重新执行副作用。再结合对规范 D1 表执行 INSERT OR IGNORE,以及每次持久化都保存一份带版本的 R2 快照,出错的步骤就无法悄悄覆盖历史;上一个版本只隔着一个 R2 key。

DataTalks 的根因,是过期状态文件驱动了一次不可逆的销毁。确定性的工作流 ID 加上“修改前快照”,让这里由过期状态造成的破坏不再致命:即便工作流基于过期假设运行,快照仍可用于回滚。(关于持久执行原语,我在对比 Cloudflare Workflow 与 Managed Agents 的文章中详细讲过;幂等性背后的计算逻辑,仍是同一套边界控制,只是换了一个名字。)

可以这样理解:任何可能执行破坏性操作的步骤,都必须先写入带版本的快照。快照是修改的前置条件,不是事后补救。快照写入失败,修改就不会发生;修改失败,快照就是恢复点。不存在既丢失状态、又无处恢复的路径。

本周就能落地的 5 步迁移

请按下面的顺序执行。只做步骤 1,就足以阻止前面的 3 起事故;哪怕其他步骤暂时都不做,也应先完成它。

  1. 把 Agent 的云角色收窄到不含任何销毁权限

    Agent 运行时使用的 IAM 角色(或服务账户、API token),不得拥有针对生产资源的任何 destroy 或 delete 权限。不能有 DeleteVolume,不能有 terraform destroy 权限,也不能有 DROP TABLE。角色决定了爆炸半径的下限;如果角色本身没有收窄,上层所有控制都失去意义。

    这就是 Kiro 事故最具体的教训:不要让 Agent 继承发起人的宽泛权限,而要给它一个独立且收窄的角色。

  2. 用 permissionMode dontAsk 和明确的 allowedTools 白名单约束 Agent

    选出能让 Agent 完成工作的最小工具集。我的系统需要 12 个,你的可能只要 6 个。彻底移除所有通用 shell 工具。如果 Agent 目前因为“方便”而配有 Bash 工具,那么这份方便本身就是漏洞。

    TypeScript
    { permissionMode: "dontAsk", allowedTools: [/* explicit list */] }
  3. 加入预算熔断器

    让每次付费调用都经过同一个封装函数。调用前检查硬性每日上限,调用后检查单实例上限;一旦超限,就抛出 NonRetryableError。单实例上限是每日上限无法替代的熔断器——那次持续 63 小时、花掉 $4,200 的失控循环,在逐次运行累积时并未触发任何看似合理的每日上限。单实例上限能捕获每日上限遗漏的风险。

  4. 修改前先保存快照

    每个可能执行破坏性操作的步骤,都必须先写入带版本的快照。可以用 R2,可以用 S3,也可以用 D1 的 *_archive 表。关键在于:快照写入必须是修改的前置条件,并且与修改放在同一个步骤、同一个近似事务的代码块里。

  5. 使用仅追加的操作日志

    每次工具调用对应一行日志,并在结果返回前写入。日志要带有 Agent ID、工作流实例 ID、USD 成本和延迟。问题终究会发生;届时事故复盘应该是一条 SELECT 查询,而不是靠回忆重建经过。

每一步都可以在一天以内完成。之所以要按这个顺序,是因为边界控制会逐层叠加:步骤 1 确定下限;步骤 2 封闭下限之上的操作面;步骤 3 限制操作面内的成本;步骤 4 和步骤 5 则让任何失败都可恢复、可查询。

常见问题

能不能只在系统提示词里告诉 Agent 永远不要删除生产环境?

PocketOS 就这样做过。Agent 自己的认错说明明确写着,它违反了这些指令。指令不是控制面。提示词只是概率系统的输入;工具白名单和云角色才是运行时的属性。应当依靠运行时。

permissionMode dontAsk 是否意味着完全失去交互能力?

不是。它只表示未列出的工具会被拒绝,而不是弹出确认提示;白名单内的完整操作面仍然可用。真正被移除的,是“Agent 发问,疲惫的人点击确认”这种失败模式——而这正是应该移除的模式。

已经有每日成本上限,为什么还需要单实例上限?

因为那次在 63 小时内花掉 $4,200 的失控循环,在逐次运行累积时仍低于任何看似合理的每日上限。单个异常循环完全可能始终远低于“$20/day”,却在一个周末累积到四位数成本。单实例上限才是每日上限从结构上无法充当的熔断器。

我用的是 Vercel 或 Railway,不是 Cloudflare Workflows,这套方法还适用吗?

调用卡口、工具白名单和收窄角色都与平台无关。只有幂等原语,也就是确定性的 Workflow ID,是 Cloudflare 特有的。在其他技术栈上,可以为任务设置幂等键,并在队列层或任务运行器层拒绝重复项。

双人审批够不够?

Kiro 针对人类操作设有双人审批,但 Agent 继承的权限绕过了它。审批门槛必须绑定 Agent 实际运行的角色,而不是发起任务的人。如果 Agent 角色拥有销毁权限,审批就只是装饰。

如果你读到这里,是因为团队里的某个 Agent 上周差点触碰生产环境,那么我在 DVNC.dev 所做的正是这类边界控制:把上面的架构按优先级应用到你的技术栈,先从收紧爆炸半径开始。

最近更新
2026年9月5日
分类
Build

在 Google 中优先显示本站

将 omidsaffari.com 添加为 Google 搜索的优先来源

把 omidsaffari.com 设为优先来源,Google 会在 Top Stories、AI Overviews 和 AI Mode 中为您优先展示。

AI智能体成本:付费兜底如何耗尽共享余额

AI智能体成本:付费兜底如何耗尽共享余额

一次沙箱文件交接失败,让付费图片兜底悄悄变成默认路径:共享余额从 $8.30 降至 $0,402 错误却没有阻止任务返回 done。本文复盘 AI智能体成本如何在凭据边界、共享依赖和软失败之间失控,解释为何更多兜底不等于更可靠,并给出由可信进程接管上传、将完成状态绑定到封面与嵌入等必需产物的修复方法。2026年9月24日Build
Cursor 价格拆解:Rollouts 免费吗,试用额度怎么算?

Cursor 价格拆解:Rollouts 免费吗,试用额度怎么算?

想弄清 Cursor 价格?Rollouts 并非免费功能,仅 Teams(每位用户每月 $40)和定制报价的 Enterprise 可用。本文拆解 10 天上线额度、Teams 约 50 次与 Enterprise 约 500 次变更、尚未公布的后续单价,以及启用前必须核对的计费字段、遥测条件和支出控制。2026年9月24日Build
Unreal Agent 使用教程:跑通 Runner,用 JSONL 验证效果

Unreal Agent 使用教程:跑通 Runner,用 JSONL 验证效果

这篇 Unreal Agent 使用教程带你配置 Runner,在隔离仓库中运行只读任务,读取 JSONL 会话、退出状态与 Token 用量,并判断异步工具执行是否值得接入产品。文章同时拆解 Runner 与 Go 库的选择、安全边界、成本比较、代码审查场景和可落地的产品方向,帮助团队用同一模型和标准完成可复现评估。2026年9月24日Build
JetBrains Air 教程:从首次会话到代码审查

JetBrains Air 教程:从首次会话到代码审查

这篇 JetBrains Air 教程带你在 JetBrains IDE 中安装 Air Alpha、连接编码智能体、添加项目上下文,并用 Standard Access 完成首次小改动。逐文件检查 diff、亲自复跑测试,再决定保留、修改、提交或回退,同时看清免费插件与智能体订阅、API 用量和人工审查成本的边界。2026年9月23日Build
JetBrains Air 免费吗?插件、Agent 与 AI 成本详解

JetBrains Air 免费吗?插件、Agent 与 AI 成本详解

JetBrains Air 免费吗?Air Alpha IDE 插件价格为 $0,但宿主 IDE、编程 Agent、第三方 API 与 JetBrains AI Credits 仍可能收费。本文拆解 Junie Lite 免费期、四种授权路径和约 27 Credits 的订阅分界,帮你确认每次会话由哪个账户付费。2026年9月23日Build
Firecrawl 自托管实战:部署、验证与成本账

Firecrawl 自托管实战:部署、验证与成本账

从固定版本开始完成 Firecrawl 自托管:按 v2.11.162 部署 Docker Compose,用真实抓取和重启复测保存证据,并对照 Firecrawl Cloud 的功能边界与 30 天成本。文中提供可复用的验证脚本、适用团队清单和成本工作表,帮助你判断基础设施控制权是否值得首月 $725.20 的投入。2026年9月22日Build
AI Agent 付费重试怎么管:把购买权限交还给人

AI Agent 付费重试怎么管:把购买权限交还给人

一套已有两道人审关卡的视频工作流,仍让 AI Agent 在自检失败后自行重做;等人查看时,所有者密钥下已产生 $5.48 费用。本文拆解漏洞为何出在“再次购买”的权限边界,并给出可执行的控制方案:审查只记录问题,由人批准重试,付费渲染函数在调用前校验并消费许可,防止模型把自己的重做建议变成付款授权。2026年9月22日Build
MindStudio 评测:AI智能体开发平台值不值得用?

MindStudio 评测:AI智能体开发平台值不值得用?

这篇 MindStudio 评测拆解其 AI 智能体开发平台的工作流、模型选择、集成、人工审核与测试能力,并核对 Free、Individual 和 Business 方案。文中按 1,000 与 10,000 个任务测算模型、审核和搭建成本,说明个人开发者、团队与自动化场景应如何判断是否值得试用。2026年9月22日Build
订阅通讯

每周日,一封信。写运转中的系统,不写热评。

每周一期。无垃圾邮件。随时退订。