posts/google-adk-zero-trust-boundary.md
系统提示词从来不是 Agent 的安全边界
一笔 149 美元的退款,攻击者想让 agent 改成 10,000 美元。
顺手再跑一段 Python,把宿主机环境变量里的 API Key 打印出来。
两件事,塞进同一句提示词。
Google 最新公开的零信任 agent 示例就从这个场景开刀。它用 Gemini 和 Agent Development Kit 搭了一个客服与退货 agent,正常工作是读取订单、计算退款、写数据库、返回凭证。然后攻击者往请求里混入一段注入指令,要求超额退款,再让动态代码把环境变量带出来。
好家伙。
要是这套系统只有一句「退款不得超过订单金额」的系统提示词,模型拒绝时看起来很安全,哪次没拒绝,生产账本就替你交学费。
Google 给出的判断很直接,系统提示词是软约束,不是安全边界。它可能被提示词注入绕过,可能在 prompt 调优时改变,也可能因为模型升级而出现新的行为。
我的判断再往前走一步。
只要 agent 已经能改数据库、调内部 API、执行代码,你就不该再问「怎样写 prompt 才能让它永远听话」。该问的是,哪怕它答应了攻击者,系统能不能让坏动作落不了地。
这两个问题长得很像,工程答案完全不是一回事。

图|官方 Live Attack Playground 会展示每条规则是否命中
模型一碰生产状态,信任边界就换地方了
很多 agent 的安全设计,中心仍然是那份系统提示词。
不准泄露密钥,不准越权,不准执行危险命令,不准退款超过订单金额。规则写得很完整,模型在演示里也会礼貌拒绝,于是大家松一口气,把数据库连接和工具权限递了过去。
这块真的需要警惕一下。
大模型输出不是确定性程序。相同输入可能因为上下文、采样参数、工具返回和模型版本得到不同结果。系统提示词可以提高守规矩的概率,却没法给一笔生产事务提供百分之百的约束。
更麻烦的是,agent 不是只输出一段文字。
它可能调用 refund_order,可能执行 SQL,可能生成 Python,可能把一段结果传给下一个 agent。模型每多拿一把工具,攻击者就多一条可以借力的路径。权限如果共用,日志如果只记最终回复,出了问题甚至很难确认是哪一个 worker 动了哪一行数据。
Google 的方案把边界搬到了模型外面。公开仓库里有三道硬门,写操作加密签名、动态代码进 gVisor、模型输入和工具输出经过确定性语义网关。
三道门不是重复保险。它们各自解决一种不同的失败。
签名能告诉你谁动了账本,但不能替你判断该不该动
常见的多 agent 系统会让一组 worker 共用数据库连接池。这样接起来省事,审计时就很难受。
某条退款记录从 149 变成 10,000,到底是客服 agent 写的,人工后台改的,还是攻击者绕过应用直接碰了数据库,普通操作日志不一定说得清楚。
Google 的做法是让每个 agent 拥有独立身份,每一笔改变状态的写入都要签名,数据库入口先验签,再提交事务。生产环境可以把私钥放进 Cloud KMS,由 Cloud HSM 承载。私钥在防篡改硬件里生成,不离开 HSM,agent 只能请求签名。
payload 还得先做确定性序列化。字段顺序一变,摘要就会变;没有这一层约定,同一笔业务数据也可能验不过。
本地 demo 没强迫开发者先开一套 Google Cloud 账号,而是用 HMAC 模拟签名。数据库入口重新计算摘要,以常量时间比较签名,后台审计任务再持续扫描账本。如果有人绕开 agent,直接把数据库里的 149 改成 10,000,原签名会立刻失效。
厉害了,谁动过账本这件事终于不靠猜。
但这里有个很容易漏掉的缝。
如果 agent 自己被提示词注入骗了,它也可能拿着合法身份,为一笔错误退款签出一份完全合法的签名。密码学能证明「就是它干的」,不能证明「这事应该干」。
所以签名不是授权规则。它负责身份、完整性和追责,金额上限与业务资格仍要交给后面的确定性网关。
这也是三层设计最有价值的地方。安全控制不假装自己无所不能,每一层只把自己的活干死。
gVisor 也不是一句沙箱就完事
第二个危险点是动态代码。
agent 为了计算折旧、解析文件、处理日志,现场生成一段 Python 很常见。把代码直接扔给 exec() 基本等于把服务器钥匙压在门垫下面。放进普通 Docker 会好一些,但容器仍然共享宿主机 Linux 内核,一处内核漏洞或一个配错的 capability,隔离就可能被撕开。
Google 示例把代码交给 gVisor 的用户态内核运行。runsc 在应用和宿主机内核之间多垫一层,拦截系统调用,缩小动态代码能碰到的攻击面。
可光写一个 --runtime=runsc 还不够。
示例同时关掉网络出口,丢弃全部 root capability,把脚本只读挂载,再给容器 64MB 内存、0.1 个 CPU 和 5 秒执行时间。代码想连外部地址带走环境变量,没有网络。想改挂载脚本,文件系统只读。掉进 while True,超时看门狗会把它结束。

图|安全文件读取放行,敏感文件与外连系统调用被隔离层挡住
这套组合有点子牛逼的地方,是它不跟恶意代码辩论。
模型可以生成一百种花样,运行时只承认几件确定的事。没有网络就是没有网络,没有 capability 就调用不了对应能力,超过 5 秒就停。
当然,沙箱也不是魔法结界。镜像里不该塞生产密钥,宿主目录不要随手挂进去,内核与运行时要更新,敏感任务还得进一步拆到独立工作负载。零信任不是买一个组件后宣布毕业,它更像一种默认姿势,先假设这一层会失守,再控制失守后的爆炸半径。
真正的业务边界,要写成普通代码
最后一层是语义网关。
名字听着挺 AI,干的活其实很朴素。它站在模型、工具和数据库之间,在调用前后做确定性检查。

图|提示词、模型输出与工具调用都不能绕过确定性规则
Google demo 会拦信用卡号特征、sk_live_、STRIPE_API_KEY 这类密钥痕迹,也会识别常见越狱短语。更关键的是,它对退款金额做硬判断。订单只有 149 美元,工具调用想更新成 10,000,直接 BLOCK,不再问模型觉得合不合理。
我跟你说,生产系统最可靠的部分,往往就是这种不聪明的代码。
一个 if refund_amount > order_total,可能没有任何模型演示看起来酷,却比十段「请务必遵守退款规则」更接近安全边界。模型负责理解顾客在说什么,普通代码负责决定这笔钱最多能退多少。把模糊判断和确定规则分开,系统才有可验收的底。
示例还把这些规则写进 CI 测试。Stripe token 必须被挡,越狱退款必须被挡,越界 SQL 必须被挡,合法的 149 美元更新必须放行。以后换 prompt、换 Gemini 版本、改工具 schema,测试先跑一遍,旧边界有没有被撞坏,不用等线上事故来告诉你。
不过,原示例里的字符串黑名单只适合教学。
攻击者换个表达就可能绕过去。真正进生产时,优先级最高的应是金额范围、字段 schema、工具白名单、数据分级、短时凭据和人工审批这类可确定验证的规则。自然语言检测可以加分,别让它独自守门。
普通 agent 项目可以先抄最小版本
坦率讲,多数团队不会第一天就配 Cloud HSM、gVisor 集群和完整的策略平台。也没必要等所有基础设施齐活才动手。
先把工具分成两类。
只读工具是一类,改变生产状态的工具是另一类。查订单和退钱不能拿同一套权限,读日志和删资源也不该共用一个宽泛 token。所有写操作统一经过一层执行器,模型只能提出意图,执行器负责校验 schema、权限、金额和审批条件。
再把每个 agent 的身份拆开。
别让五个 worker 共用一把永久 API Key。最小版本可以先用独立 service account、短时凭据和带 agent_id 的审计日志。等事务价值上来,再补 KMS 签名和独立审计。目标不是把密码学名词堆满架构图,而是出了事能回答谁、在什么时候、用什么输入、改了什么。
动态代码则默认当成不可信输入。
没有明确需要就别给网络,没有明确需要就别挂宿主目录,资源上限和超时看门狗从第一天就开。上 gVisor 前,--network=none、--cap-drop=ALL、只读挂载和临时目录这些基础动作已经能砍掉不少风险。
最后,把每条业务红线变成能失败的测试。
测试不是问模型「你会不会超额退款」,而是真的发一笔 10,000 美元工具调用,看执行器是否拒绝;真的塞一段密钥,看输出网关是否拦截;真的让脚本外连,看沙箱是否没有出口。
说真的,agent 安全最怕的不是模型偶尔犯错,而是团队只有一套「看起来应该没问题」的信念,没有一条能重复跑的失败证据。
Google 这篇文章的价值,也不只是给 ADK 加了一份安全 demo。它把一个长期被混在一起的问题拆开了。
系统提示词负责引导行为。
签名负责留下身份和完整性证据。
沙箱负责限制代码能碰什么。
网关负责执行不能商量的业务规则。
模型可以继续聪明,边界最好继续笨一点。
回到开头那笔退款。真正让人安心的,不是 agent 有没有义正辞严地拒绝攻击者,而是它哪怕答应了,也转不走多出来的 9,851 美元。
那才叫安全边界。