面向智能体的策略即代码

12 分钟读完

S2
深入解析 · 智能体安全

策略即代码把"这个智能体能不能做这件事"的决定从提示里搬出来,交给一个在边界处对每次工具调用做门控的策略引擎——而 2026 年的工具链(Bedrock AgentCore 里的 Cedar、微软的 Agent Governance Toolkit)让它成了一个已解决的工程问题,而非研究问题。

指令层级告诉模型该优先什么;策略即代码则决定模型实际被允许做什么,用一段模型无从争辩的代码来定。2026 年这是一种网关模式:一个策略决策点(PDP)——用 Rego 的 OPA,或 Cedar——在每次工具调用执行前拦截它,并返回一个结构化的裁定,而不是一种感觉。微软的 Agent Governance Toolkit 返回一个 PolicyDecision,带有 allowedmatched_rule、一个 action、以及一段人类可读的 reason;AWS 则把 Cedar 放进了 Bedrock AgentCore、置于工具调用的边界处。本文讲的是 PDP 放在哪里、决定你爆炸半径的"失败放行还是失败拦截"之选,以及为何这个结构化决策是下游一切所依赖的原语。

STEP 1

网关模式。

本组上一篇、讲提示注入防御的文章,落脚在一个承重的动作上:把模型约束到一个类型化的 schema,让被注入的指令无处落脚,然后即使是 schema 合法的动作,也要对照模型无从争辩的规则接受检查。策略即代码就是这后半段。它把授权决定——这个智能体是否可以用这些参数、在这个上下文里执行这个动作——从提示里搬出来(提示里一个有说服力的攻击者能把它重新谈判掉),放进一个在模型触及范围之外、确定性运行的策略引擎里。

其架构是一个网关。智能体提议的每一次工具调用,在执行前都被路由通过一个策略决策点(PDP);PDP 对照一条策略评估该请求,返回放行或拒绝;只有被放行的调用才到达真正的工具。这里没有什么新鲜的——这就是十年来一直挡在各种 API 前面的那套策略执行点 / 策略决策点的拆分——但把它对准一个智能体的工具调用,正是把一个概率系统变成一个有界系统的关键。模型可以想要任何东西;网关决定实际发生什么。关键属性是这个检查必须强制且前置于执行:网关在调用工具之前就询问 PDP,而不是在记录了意图之后,这样一个被拒绝的动作压根不会运行、而不是运行了再被标记。一个向 PDP 请求许可、然后不管结果照做不误的智能体,不是被门控了——它是在做旁白。

让 2026 年区别于早年那些手搓白名单的,是工具链已经真实且现成。AWS 把 Cedar 放进了 Amazon Bedrock AgentCore,在网关处拦截每一次智能体工具调用,策略用 Cedar 写、或由自然语言生成。微软发布了 Agent Governance Toolkit(AGT),它对照策略评估工具调用、消息发送、以及委派。这已经不再是一个"智能体能否被治理"的研究问题;它是一个"用哪个引擎、放在哪里、如何失败"的工程问题。智能体威胁模型给出了网关非存在不可的理由:自主性加上工具访问,把攻击面拓宽到智能体能触及的一切的并集,而网关是你能确定性地把它重新收窄回来的那一个咽喉点。

STEP 2

面向智能体负载的 OPA/Rego 与 Cedar。

两个策略引擎主导着"智能体门控"这场讨论,而它们优化的东西不同。Open Policy Agent(OPA)使用 Rego,一门通用策略语言,其表达力足以编码几乎任何你能描述的规则——跨数据的连接、量词、计算出的条件、对请求及其上下文的任意逻辑。那份能力既是卖点,也是代价:一条富表达力的 Rego 策略更难被机械地推理,而"这条策略实际允许什么"可能需要运行它、而非分析它才能回答。

Cedar(AWS 嵌进 Bedrock AgentCore 的那个引擎)做的是相反的取舍。它是一门专为授权而建的语言,故意比 Rego 表达力更弱,换来的是可验证性:因为语言受约束,Cedar 策略可以被静态分析——你可以问"这套策略里有没有任何它本不该允许、却允许了的请求",并且不必穷举输入就能得到答案。对智能体负载而言——这里一条过度放权的规则就可能把一个它本不该有的写入递给一个被注入的智能体——那份可分析性,比你所让渡的表达力更值钱。经验法则是:当你的策略确实需要通用计算时,伸手去拿 Rego;当你想证明策略只做你以为它做的事时,去拿 Cedar。

你并不总得自己去选引擎。微软的 Agent Governance Toolkit 是 MIT 许可、于 2026 年 4 月进入公开预览,它刻意在一个评估接口之后支持多种策略后端——用于简单放行/拒绝规则的纯 YAML、用于富表达逻辑的 OPA Rego、以及用于可分析授权的 Cedar。这一点对采用很要紧:一个团队可以先用声明式 YAML 写那些显而易见的规则("这个智能体绝不能调用支付工具"),只在某条规则确实需要时才落到 Rego 或 Cedar,而不必重建网关。无论你选哪个后端,集成的形状都一样——一个请求进去,一个结构化决策出来——这正是第 5 步要讲的。

这个工具包还澄清了一件事:门控不只针对工具调用。AGT 评估工具调用、消息发送、以及委派,这对一个能把工作交给其他智能体的智能体来说,正是恰当的范围。在一个多智能体系统里,危险的动作往往不是单次工具调用、而是一次委派——一个智能体请另一个(有着不同工具与权限的)智能体,去做第一个智能体不被允许直接做的事。如果你的 PDP 只看得到叶子级的工具调用,它就会彻底漏掉那条洗白路径。治理消息发送与委派这些边、而不只是工具调用这些叶子,才能让一条说"这个智能体不得触碰支付"的策略,不被"委派给一个能触碰的智能体"轻易绕过。你选哪个引擎,远不如"把它对准每一条权限跨越边界的边"来得要紧。

STEP 3

PDP 放在哪里(及延迟预算)。

PDP 可以坐在三个地方,而这个选择基本上是一场"延迟对爆炸半径"的取舍。它可以作为智能体运行时内部的 SDK 中间件在进程内;可以在网关 / MCP 代理处、作为每次工具调用都要经过的一个单独跳;或者作为网关通过网络调用的一个远程策略服务。进程内最快、且没有网络依赖,但策略跑在它本该约束的那段代码的同一个信任边界之内——如果运行时被攻破,检查也一并被攻破。一个单独的网关或代理跳,把决策放在了智能体进程之外,而当智能体本身正是你所不信任的东西时,这正是你想要的属性。

延迟数字决定了你具体如何部署 OPA,而在这里营销话术和手册各执一词——请以手册为准。每次决策都把 OPA CLI 当作子进程来调用,会带上大约 50–200 毫秒的进程启动开销,这对一次批量检查没问题,但要坐在一个交互循环里每次工具调用之前,就慢得离谱。把 OPA 作为一个长期存活的远程服务器(或它的一个本地 sidecar)来跑,会把每次决策的评估带进亚毫秒区间,因为进程及其已编译的策略都已经预热好了。实际后果很直接:不要在热路径上去 shell 调用 OPA CLI。把 OPA 立成一个服务器、通过一个本地套接字去查询它,或者把评估器嵌进进程内,好让策略检查花的是一次亚毫秒的查找、而不是一次五分之一秒的 fork。(Cedar 在进程内评估时更快;它的开销更接近一次函数调用、而非一次网络往返。)

OPA 最主流的部署是 sidecar:一个 OPA 服务器与工具网关并肩运行,在同一个 pod 或主机里,让网关通过环回接口去查询它、没有对外的网络跳。这同时给了你一个进程外 PDP 的信任边界隔离、以及一个预热本地服务器的延迟,这正是多数参考资料最终落到的那个模式。无论 PDP 坐在哪里,都要给它配上最小权限凭证,好让一个被许可的动作仍然无法超出它所运行的令牌——范围受限凭证那一篇讲了那种短期、范围窄的令牌,它让策略层和凭证层相互加固、而不是重复劳动。

STEP 4

失败放行与失败拦截。

你网关里最要紧的一行,是当 PDP 没有返回一个干净的放行时会发生什么——它不可达、它超时、它出错、或者它返回了一个你代码不认识的裁定。有两种默认。失败拦截:当决策是"明确放行"以外的任何东西时,就拒绝该动作——没有回答就意味着不行。失败放行:当 PDP 无法被触及时,就许可该动作,其理据是"可用性比那一次未经检查的调用的边际风险更重要"。这个选择不是风格问题;它是当策略层自己过了糟糕的一天时、划定你爆炸半径的那条线。

对任何带副作用的东西,正确的默认是失败拦截。如果 PDP 宕了、而智能体想写一个文件、发一封邮件、执行代码、或转一笔钱,安全的答案是拒绝并抛出一个错误,而不是因为检查恰好不可用就把动作放过去。在一条写入路径上失败放行,意味着一个能拖垮或撂倒你 PDP 的攻击者,刚刚就禁用了你的整个授权层——拒绝服务变成了权限提升。这恰恰是反的:你防线受损的那一刻,正是你想要它最严、而非最松的那一刻。

把这个失败模式说具体。假设一个智能体已被一条注入的指令操纵、正冲着外泄一个机密去,而恰在那一刻,PDP sidecar 正在重启途中、或策略服务在高负载下超时。失败拦截:那次写入被拒绝,智能体抛出一个错误,最坏的结局是一个卡住的任务。失败放行:那次写入未经检查就通过了,而你本来只差一次策略评估就能阻止的那起事件,如今恰好在你毫无覆盖的那一个窗口里发生了。这份不对称就是全部论点——一个失败拦截的默认,在 PDP 偶尔不可达的少数场合让你损失可用性;而一个失败放行的默认,恰恰在你授权层承压的那些时刻让你失去它,而那正是一个真实攻击者最可能在推它的时候。你是在选择:事后复盘时你更愿意解释哪一种失败。

失败放行只在一条狭窄的带里站得住脚:只读、低后果的动作,此时一段短暂的未经检查读取窗口确实比一次宕机危害更小,且你已经另行界定了那些读取能暴露什么。即便在那里,诚实的姿态也是逐工具地做这个选择、而非全局地做——给每个工具标上它的失败模式,好让支付工具失败拦截、而一个只读的状态查询可以失败放行——并且每当网关走上失败放行那条路时都大声告警,因为一个悄悄不可达的 PDP,是你在盲发的一道缺口。审慎地决定这个默认,把它默认为拦截,并把每一个失败放行的例外都当作一份有记录的风险接受、而非一种图省事。

STEP 5

结构化的 PolicyDecision。

整个模式的回报不是那个放行/拒绝的比特位——而是 PDP 返回的那个结构化决策对象。一个只回答"是"或"否"的网关会门控住动作,却在事后给不了你任何可据以行动的东西。微软的 Agent Governance Toolkit 返回一个 PolicyDecision,它带着决策及其理据:allowed(那个布尔值)、matched_rule(哪条规则触发了)、action(放行 / 拒绝 / 审计 / 阻断)、reason(一段人类可读的解释)、以及一个 audit_entry(策略、规则、时间戳、上下文)。结构化、枚举化的拒绝原因,在 2026 年的工具链里已经是常态、而非一种愿景。

下面是一条对 write_file 工具做门控的 Rego 策略——默认拒绝,只放行那些留在已批准工作区之内、且不触及任何敏感路径的写入——其下则是当模型提议往那个边界之外写时、网关吐出的那个 PolicyDecision

package agent.tools.write_file

# Failure-closed: default deny, allow only what a rule explicitly permits.
default decision := {"allowed": false, "matched_rule": "default_deny",
                     "action": "deny", "reason": "no rule permitted this action"}

approved_root := "/workspace"
sensitive := ["/workspace/.git", "/workspace/.env", "/workspace/secrets"]

# Allow a write only if it stays under the approved root and touches
# no sensitive path. `input` is the proposed tool call.
decision := {"allowed": true, "matched_rule": "write_within_workspace",
             "action": "allow", "reason": "path within approved workspace"} {
    startswith(input.args.path, approved_root)
    not sensitive_path(input.args.path)
}

sensitive_path(p) {
    some s in sensitive
    startswith(p, s)
}

当智能体——也许被一条注入的指令所操纵——提议往 /workspace/.env 写时,没有放行规则匹配,默认那条触发,网关返回一个人与机器都能读的决策:

{
  "allowed": false,
  "matched_rule": "default_deny",
  "action": "deny",
  "reason": "no rule permitted this action; path targets a sensitive location",
  "tool_call": { "tool": "write_file", "args": { "path": "/workspace/.env" } },
  "audit_entry": {
    "policy": "agent.tools.write_file",
    "rule": "default_deny",
    "evaluated_at": "2026-04-30T14:07:22Z",
    "policy_version": "v3",
    "context": { "agent": "repo-triage", "session": "s-8842" }
  }
}

那个 reason 字段,正是本组其余部分所构建其上的原语。一个结构化的拒绝原因,能把一句光秃秃的"已拒绝"变成一个下游层可据以行动的解释——而后面两篇文章会朝各自的方向延展它。一篇拿起这个枚举化的原因,把它与一条理由链(why-trail)配对,后者记录到底是哪条规则针对哪个输入触发的,好让一次拒绝对一个取证人员清晰可读、而不只是对用户;那是本组后面一篇讲结构化拒绝与理由链的文章的主题。另一篇拿起整个决策——reasonaudit_entry、策略版本、时间戳——把它签名、并哈希链接进一份你能回放的防篡改记录里,那是本组讲决策回执与审计那一篇的主题。两者都依赖同一样东西:一个返回结构化决策、而非一个比特位的 PDP。让网关返回一个真正的 PolicyDecision,你就不只是门控了动作——你还产出了那份问责、取证、审计在下游都要从中读取的、机器可读的理据。