前一篇讲 Capability 时留下了一个故意没展开的问题。

会话拥有 file_read,说明 Agent 具备读文件的能力;用户账号可以访问某个仓库,说明 IAM 允许这个用户访问资源。但这两件事加起来,仍然不能推出 Agent 在当前任务里应该读取仓库中的任意文件。

传统权限系统习惯回答:

主体能不能对资源执行某个动作?

Agent 还要多回答一句:

主体为什么要在这个任务里执行这个动作?

这不是给 RBAC 再加几个 Role 就能补上的差异。传统 IAM 管的是静态或半静态授权,Agent 执行的是由自然语言任务动态生成的委托。

一个“有权访问”的越权动作

假设用户让 Agent 修改一个单元测试。

用户本来就有代码仓库、内部文档和部署系统的权限,Agent 也拿到了对应 Token。为了完成修改,它读取源代码、运行测试都很正常。

如果它随后读取部署凭据并触发发布,传统权限检查可能全部通过:

  • Token 是真的。
  • 用户有发布权限。
  • API 调用格式正确。
  • 目标项目也属于用户。

但用户授权的任务只是“修改测试”,不是“发布版本”。

这里发生的不是账户越权,而是目的越权。Agent 没有超出用户这个人能做的范围,却超出了用户这一次交给它做的范围。

这类问题很难被传统 IAM 看见,因为 IAM 通常没有“当前自然语言任务”这个输入。它只知道 User、Role、Resource、Action,不知道为什么今天这次 Action 合理。

User、Agent、Session、Task 不能揉成一个主体

很多系统接入 Agent 时,会直接复用用户 Token。日志里最后只剩一句:

user_A called api_X

至于是谁发起的、哪个 Agent 规划的、属于哪次会话、服务于什么任务,全都丢了。

Agent 执行至少有四类主体:

User:最终授权的人
Agent:代表用户做事的软件主体
Session:一段连续交互和状态边界
Task:这次委托真正允许完成的目标

User 决定资源的最大访问边界,Agent 决定执行者身份,Session 承载连续上下文,Task 则限定权限为什么被使用。

它们不能互相替代。

同一个用户可以同时运行多个 Agent;同一个 Agent 可以开多个 Session;同一个 Session 里也可能出现多个 Task。只记录 User,会把所有机器动作伪装成人的直接操作;只记录 Agent,又无法说明它代表谁;只记录 Session,则很难解释一次权限扩张属于哪个任务。

更实际的审计事件应该同时保留:

{
  "user": "user_A",
  "agent": "coding_agent",
  "session": "session_42",
  "task": "fix_unit_test",
  "tool": "deploy_api",
  "action": "release"
}

这并不会自动解决授权,但至少让“谁代表谁、因为什么、做了什么”不再混成一句模糊日志。

Agent 需要的是委托权限,不是用户权限的复制品

最危险的做法,是把用户的长期凭据直接放进 Agent Context 或运行环境。

从工程上看很方便:用户能做什么,Agent 就能做什么;少一次换 Token,也少一个中间服务。但这等于把人的完整权限复制给一个会读取不可信内容、会自主规划、还会循环重试的软件体。

更合适的关系不是继承,而是委托。

用户最大权限
    ∩ Agent 允许能力
    ∩ 当前 Task 所需范围
    ∩ Runtime 环境约束
    = 本次可执行权限

这不是一个严格数学公式,但它说明每一层都只能收窄,不能因为 Agent 开始执行任务就自动放大。

任务授权最好有明确的时间和资源边界。修改代码可以允许读写当前仓库,在会话结束后失效;查询数据可以绑定数据集和查询类型,不自动获得导出能力;需要发布时再单独升级权限,而不是从一开始就把发布 Token 塞进去。

从用户权限到任务短期凭据的授权收窄链

开放式任务当然无法预先列出每一个文件和 API。Task Scope 不是为了准确预测未来,而是让权限扩张变成一个显式事件。Agent 发现新需求时,可以请求新的 Capability,安全系统再根据影响范围选择 Allow、Confirm 或 Block。

凭据不应该进入模型上下文

只要凭据以明文出现在 Context 里,它就同时面临三个问题。

第一,模型可能在输出、日志或 Tool 参数中无意复述;第二,不可信内容可能诱导 Agent 读取并外发;第三,长期会话、缓存和 Trace 会扩大凭据副本数量。

Credential Broker 的价值就在这里。

模型不直接拿到长期 Token,只提交一个结构化动作请求:

以 user_A 的委托身份
在 task_42 中
调用 repo.read
资源范围为 project_X
有效期 5 分钟

Broker 在模型之外检查用户、Agent、Task、资源和策略,再换取短期凭据或直接代为调用。模型知道自己请求了什么能力,但不需要看到可以被复用的秘密。

短期凭据也不是万能药。一个五分钟 Token 仍然足够完成一次数据外传。它降低的是泄露后的复用价值,不是消除错误授权。因此它必须绑定 Task、Audience、资源范围和 Runtime,最好还要限制可执行动作。

Sub-agent 最容易把权限重新放大

多 Agent 系统经常把父 Agent 的上下文和工具直接交给子 Agent,理由是“子 Agent 只是帮忙完成一个子任务”。

问题是,任务变小了,权限却没变小。

父 Agent 负责“修复并发布版本”,可能拥有代码、测试和发布能力;它创建一个只负责查资料的子 Agent。如果子 Agent 默认继承全部工具和凭据,那么一次本应低风险的搜索任务,已经获得了发布权限。

权限下发应跟着子任务收敛:

父任务:修复并发布
  ├─ 子任务 A:定位问题 → code_read + test_read
  ├─ 子任务 B:修改代码 → code_write + test_run
  └─ 子任务 C:发布版本 → release_request + confirm

跨 Agent Handoff 也需要记录委托关系。最终是谁执行了动作、权限从哪里来、中间是否被再次转交,这些信息如果断掉,出了问题只能看到最后一个 Worker,找不到授权链。

“父 Agent 批准子 Agent”仍然不够。父 Agent 和子 Agent 都属于不确定系统,不能因为换了一个模型实例就获得更高信任。真正的权限根仍然应该在模型之外。

模型可以判断相关性,不能批准权限

任务授权不可避免地包含语义。

“修改单元测试”是否需要读取某个配置,“分析日志”是否需要访问用户数据,很多时候无法靠静态 Policy 完整表达。模型可以帮助判断动作和目标是否相关,也可以解释为什么请求扩权。

但它只能提供 Risk Signal。

如果同一个模型先规划“我要读这个文件”,再回答“这个文件与任务相关”,安全系统只是要求申请人给自己写了一份批准意见。即使换成第二个模型,也只是降低同源失败的概率,没有创造新的信任根。

最终强制执行仍应落在数据源、Tool Gateway、Credential Broker 或 Runtime 上。模型负责解释开放语义,确定性系统负责限制资源和 Side Effect。

授权的真正难点,是目的会变化

Agent 执行过程中,任务目标可能被正常发现的新信息改变,也可能被不可信上下文悄悄改写。

这使 Task Scope 不能只在会话开始时写一次,然后永远不变。它需要保留原始目标,也要记录每次合法变更由谁提出、谁确认、扩张了哪些能力。

否则所谓“动态规划”会变成动态扩权。

所以“Agent 有权限”最多说明动作没有突破账户边界;它不能证明动作符合用户这次委托。真正需要保护的是一条委托链:

User Intent
  → Task Scope
  → Agent Capability
  → Temporary Credential
  → Tool Enforcement
  → Runtime Evidence

而这条链最容易被污染的位置,恰好是 Context。

身份、凭据和 Task 都做对了,只要一段网页、一份文档或一个 Tool Output 能把不可信数据伪装成高优先级指令,Agent 仍然会拿着正确的权限去做错误的事。

下一篇回到 Prompt Injection,但不再只盯着 Prompt。

评论