Skip to content
围炉聊科技微信公众号二维码关注公众号,获取新文章推送
添加我为微信朋友扫描二维码,添加我为朋友

Agent 的权限是怎么管的:AgentScope 权限引擎源码拆解——智能体基建系列

你一定遇见过WorkBuddy或者其他Agent 正准备删掉一个目录、往某个路径写文件,弹出来一个确认框,你点一下"允许",它才继续。这个"确认框"就是权限系统的外衣。上一期聊了沙箱(沙箱隔离的是"执行环境"),这一期拆权限——它管的是"Agent 到底能不能调用某个工具"。这次以 AgentScope 的 Java 版为解剖对象,拉源码看它的决策链。


一、先想清楚:权限系统到底在管什么

沙箱回答的问题是"代码在哪里跑、副作用往哪隔离";权限回答的则是"这个工具调用,允不允许发生",它发生在调用之前,是一道闸门。

要理解 AgentScope 的设计,先抓住它眼里工具的定位:工具 = 权力bash 能执行代码、write_file 能改文件、edit_file 能覆写内容——每条工具都是模型可以命令机器做的动作。PermissionEngine 要解决的,就是给这些"权力"加上一道可编程、可审计、可人工介入的判断流程。

它的设计目标很直白,源码里一行注释说清了(PermissionEngine.java):

Evaluates tool execution requests against configured permission rules.

(按已配置的权限规则,评估工具执行请求。)

下面从四个零件讲起:行为三态、运行模式、引擎六层决策链、轻量/完整双路径


二、四个"行为值":一个枚举讲清 Agent 的三种权限命运

先看最小的单元。权限的最终裁决结果,只有几种可能,AgentScope 用一个枚举定义(PermissionBehavior.java):

java
public enum PermissionBehavior {
    ALLOW("allow"),        // 放行,不再检查
    DENY("deny"),          // 拒绝,这次调用作废
    ASK("ask"),            // 转人工,弹确认框
    PASSTHROUGH("passthrough"); // 我不裁决,交回引擎
}

这里有个看上去"多出来"的值:PASSTHROUGH。它代表的语义是"本层不表态,把球踢回给引擎"。这个值很关键——正是它让"工具自身的判断"和"引擎的规则表"能叠在一起工作:

  • 一个工具如果对自己的行为"心里有数"(比如文件工具知道自己在写危险路径),它可以自己给出 ALLOW / ASK / DENY;
  • 如果它觉得"我不该管这事",就返回 PASSTHROUGH,把决策权交还给引擎去匹配规则表。

所以严格说这是 "三态裁决 + 一态委托"。三态对应人类最熟悉的口令:放行、拒绝、询问。


三、五种"运行模式":当 Agent 在无人值守时怎么办

三态解决"单次裁决",但不同场景需要不同的整体倾向。AgentScope 用 PermissionMode 做这件事(PermissionMode.java):

模式语义典型场景
DEFAULT默认:所有操作都要有明确规则才放行常规开发
ACCEPT_EDITS工作目录内的文件编辑自动放行让 Agent 放手改代码
EXPLORE只读模式:改动作一律拒绝检索、调研、问答
BYPASS全部放行,不做规则评估完全可信环境 / 调试
DONT_ASK遇到本该 ASK 的,降级为 DENY无人值守、半夜定时任务

两个模式和常识有出入,值得单独说:

  • DONT_ASK 不是放行,而是"没人可问就别干"。 它在源码里的语义是 "user not available"——定时任务夜里跑,弹了确认框也没人点,与其挂着,不如直接拒绝。这是"宁可不做也不错"的设计,对无人值守场景尤其重要。
  • ACCEPT_EDITS 只自动放行工作目录内的编辑。 目录由配置里的 working_directories 定义,目录之外照旧走完整检查。

四、核心:PermissionEngine 的六层决策链

引擎的入口是 checkPermission(ToolBase tool, Map<String, Object> toolInput),返回一个 Mono<PermissionDecision>。决策主体是一个优先级链路:

  1. 工具级 deny 规则(最高优先级)
  2. 工具级 ask 规则
  3. 工具自检(bypass-immune):EXPLORE/ACCEPT_EDITS 的只读处理、危险路径检查、以及工具自己 checkPermissions 的返回
  4. 工具级 allow 规则
  5. BYPASS 兜底
  6. 默认 ASK(DONT_ASK 模式下转为 DENY)

这一段可以作为审视任何 Agent 权限框架的"骨架"来对照。

mermaid
flowchart TD
    A[工具调用请求] --> B{L1 deny 规则命中?}
    B -->|是| X1[DENY 拒绝]
    B -->|否| C{L2 ask 规则命中?}
    C -->|是| X2[ASK 弹确认 + 建议规则]
    C -->|否| D[L3 工具自检<br/>bypass-immune]
    D --> D1{工具给出 DENY/ALLOW?}
    D1 -->|是| XA[直接按工具裁决]
    D1 -->|否| E{L4 allow 规则命中?}
    E -->|是| X3[ALLOW 放行]
    E -->|否| F{模式 = BYPASS?}
    F -->|是| X4[ALLOW 兜底放行]
    F -->|否| G[L6 默认: ASK<br/>DONT_ASK 下转 DENY]
    D -.PASSTHROUGH.-> E

4.1 第一、二层:deny 与 ask 规则表——"规则优先于一切"

规则以"规则表"的形式存在,PermissionEngine 构造时从上下文里快照三张表(allowRules / denyRules / askRules)。每条规则是一个 record(PermissionRule.java):

java
public record PermissionRule(
        String toolName,     // 规则作用在哪个工具上
        String ruleContent,  // 规则内容,由工具的 matchRule 解释;null 表示该工具所有调用
        PermissionBehavior behavior, // ALLOW / DENY / ASK
        String source) { }   // 来源,如 userSettings / suggested

规则匹配的脑筋急转弯在于:规则内容怎么解释,取决于工具自己。引擎不解析规则语法,它把 ruleContent 交给工具的 matchRule 方法去比对参数。这叫"规则引擎不越权"——只有工具才懂自己的参数长什么样,比如 bash 工具能认出"这条 rule 说的是不能执行 rm -rf"。

引擎侧的动作很朴素:deny 表有命中直接 DENY,ask 表有命中直接 ASK(见 checkDenyRules / checkAskRules)。deny 永远第一,这是"禁止优先"的经典安全设计——即使你同时配了 allow 和 deny 两条规则,deny 也先审。

4.2 第三层:工具自检——谁都绕不过的保险

这层叫 bypass-immune,意思是即使你开了 BYPASS 也绕不过它。源码注释原话:"Tool-specific checks (bypass-immune): EXPLORE/ACCEPT_EDITS read-only handling, dangerous path checks, plus whatever the tool's own checkPermissions returns."

它做两件事(toolCheckPermissions):

  1. 只读模式先行EXPLORE 模式下,只读工具直接 ALLOW、非只读工具直接 DENY;ACCEPT_EDITS 模式下只读工具直接 ALLOW,非只读工具放行给工具自检去判断(checkExploreMode)。
  2. 工具自己的 checkPermissions:每个继承 ToolBase 的工具可以覆写这个方法。基类的默认实现返回 PASSTHROUGH(ToolBase.java):
java
public Mono<PermissionDecision> checkPermissions(
        Map<String, Object> toolInput, PermissionContextState context) {
    return Mono.just(PermissionDecision.passthrough(name));
}

也就是说:默认情况下工具不表态,交回引擎。而有精细语义的工具——bash、文件写入、MCP 工具——会自己覆写,产出自己的 ALLOW / ASK / DENY。

工具自检的结果怎么处理,藏着一个细节:如果工具自检返回 DENY 或 ALLOW,直接定案,不再往下走;如果返回 ASK 且 decisionReason 里带 "safety",也直接 ASK(并附带建议规则);否则继续往下走规则表(checkPermission 里对 decisionReason 的判定)。

ToolBase 还内置了一套危险路径检查 isDangerousPath,有两个值得讲的点:它不光匹配文件名/目录名黑名单,还会解析 symlink 后对真实路径再查一遍,防止"软链接指向危险目标"这种绕过手法。

4.3 第四、五层:allow 规则与 BYPASS 兜底

走到这儿,说明 deny 没命中、ask 没命中、工具也没直接定案。此时查 allow 规则表:命中就 ALLOW,且裁决结果会带着改写后的入参一并返回PermissionDecision.updatedInput 字段)。

allow 也没命中,才轮到模式兜底:BYPASS 模式直接放行(源码注释:"Bypass mode allows all operations")。

4.4 第六层:默认 ASK,无人值守自动转 DENY

什么都不命中、也不是 BYPASS——默认行为是什么?不是 DENY,而是 ASK(defaultDecisionAsk)。这个设计判断很有味道:框架默认"不信任但不拒绝",把决定权交给用户。 宁可多弹一次确认,也不擅自放行一个没被规则覆盖的工具。

唯一的例外是 DONT_ASK:没人可问,直接 DENY。


五、轻量 / 完整双路径:一个 boolean 切换两套世界观

引擎之外还有个容易被忽略的设计:不是所有场景都值得跑完整的六层链PermissionContextState 有个 isTrivial() 判定——当模式是 DEFAULT、没有工作目录、三张规则表全空时,它认为"用户根本没启用权限系统"。

ReActAgent 的工具执行前,用这行代码决定走哪条路(ReActAgent.java):

java
boolean useEngine = !state.getPermissionContext().isTrivial();
  • trivial(轻量路径):沿用"前 2.0"的旧行为,只调工具的 checkPermissions 自检。自检里只有显式 ASK 才会拦下来弹确认,ALLOW / PASSTHROUGH / DENY 照常(DENY 也会被拦)。没有规则表、没有模式、没有 DONT_ASK 降级——最简单。
  • 非 trivial(完整引擎):才走上面那套 deny / ask / allow / 模式 / 默认 的全套流水线。

这背后的工程取舍是:不要为一上来什么都不配的用户付出完整引擎的复杂度,但一旦你配了第一条规则,整套保险自动接管。 你配了规则,框架才"认真起来"。

ReActAgent 里还有个和用户体验强相关的细节:裁决完,DENY 的工具调用不会真的去执行,而是合成一个"被拒绝"的结果回给模型(源码里叫 autoDeniedIds,agent 循环为它们合成拒绝结果);ASK 的进待确认队列。如果需要弹确认,事件流里会发出 RequireUserConfirmEvent——这就是你屏幕上那个确认框的来源。


六、父 Agent 的 deny,子 Agent 逃不掉

多 Agent 场景是权限系统最容易漏的地方:父 Agent 配了一条"不许用 bash",结果它 spawn 一个子 Agent 出来干活——子 Agent 是不是就绕过去了?

AgentScope 在 AgentSpawnTool 里做了规则下放(mergeParentDenyRulesIntoSlot):创建子代理时,把父的 deny 规则合并进子的权限上下文里。注释强调了两个行为:

  1. deny 只增不减:合并只加 parent 的 deny 规则,从不放宽 child 已有的配置;
  2. trivial 子代理接种第一条 deny 时,模式自动置为 BYPASS ——这是有意为之的兼容设计:子代理原本走轻量路径(PASSTHROUGH 默认放行),加上 deny 后第一次变得"非 trivial",为了不打破它原有的放行行为,把模式设为 BYPASS(全部放行),但显式 deny 仍然优先。这样"连同放行、唯独禁掉这些"的语义被保留了下来。

一句话:禁令是可继承的,权限不可放宽。"父管得住子"是这套系统的底线。


七、怎么用,才能不踩坑

源码梳理完,回到落地。基于这个六层链的设计得到几条实操建议:

  1. 最省事的安全起步EXPLORE 模式 + 白名单规则。给 Agent 一个只读模式跑通流程,再针对必要的写操作逐条配 allow——比"全放开再封口"安全得多。
  2. 把 DONT_ASK 留给无人值守:定时任务、批处理场景下,DONT_ASK 把"挂着等没人点的确认框"变成"直接拒绝",杜绝半夜卡死。
  3. 给工具写好自检,别依赖默认 PASSTHROUGH:有副作用语义的工具(执行命令、写文件、联网)务必覆写 checkPermissions,因为只有工具自己最懂"哪些参数是危险的"。
  4. 规则内容尽量具体:规则匹配走 tool.matchRule,你配的 ruleContent 会被工具解释。别配太宽的规则(比如"禁 bash"不如"禁 bash 里的 delete 命令"),规则越精细,误伤越少。
  5. 警惕 deny 的继承效应:父 Agent 的 deny 会下放给子 Agent。想要"子代理放开某个父禁掉的工具"——做不到,这也是设计如此。

小结

回到最初那个确认框:它背后,是一条deny → ask → 工具自检 → allow → 模式兜底 → 默认 ASK 的六层决策链,再叠加"轻量/完整"双路径、可继承的 deny 规则、以及 DONT_ASK 这类面向无人值守的模式。

这套东西的骨架,和上一期沙箱文章里的一个思想一脉相承:不信任,但要能用。 沙箱把"干砸了"的代价兜住,权限把"不该干的"拦在门外。两件套配上,才敢让 Agent 真正放手干活。

源码都放在 agentscope-core/src/main/java/io/agentscope/core/permission/core/ReActAgent.java,感兴趣的可以照着 PermissionEngine.checkPermission 从头读一遍。