AgentScope 学习系列 - 07:Permission System 精准化控制 Tool 权限

1. 从一个"事故"说起

前面 MCP 那章的代码里有这么几行,当时没细说:

java 复制代码
PermissionContextState permCtx = PermissionContextState.builder()
        .mode(PermissionMode.BYPASS)  // 跳过所有权限检查
        .build();

我当时真跑过一次没加 BYPASS 的版本,高德的 MCP 工具直接被拦了。agent 调用工具一直卡在 acking 状态,因为我们没做用户确认功能,最后返回空结果。模型不管工具是干什么的,列表里有就敢调,权限系统就拦在调用前面。

这个设计用过 Claude Code 的同学会眼熟,deny/ask/allow 三种规则加几种模式,基本一一对应,本章讲它。

2. 三个核心概念

2.1 模式

PermissionMode 一共五种:

模式 行为
DEFAULT 规则没接住的调用,需要用户确认
ACCEPT_EDITS 自动放行工作目录内的文件操作
EXPLORE 只读模式。只放行 readOnly 的工具,其他直接拒绝
DONT_ASK 规则没接住的调用不问用户,直接拒绝
BYPASS 放行未命中规则的调用。deny 规则和危险路径检查照样生效

ACCEPT_EDITS 配合工作目录使用,文件操作只在声明的目录范围内自动放行:

java 复制代码
PermissionContextState permCtx = PermissionContextState.builder()
        .mode(PermissionMode.ACCEPT_EDITS)
        .addWorkingDirectory("/my/project",
                new AdditionalWorkingDirectory("/my/project", "userSettings"))
        .build();

我的建议:本地调试用 BYPASS,测试环境用 EXPLORE,生产环境用 DEFAULT 或 DONT_ASK 加规则。

2.2 规则

PermissionRule 是个 record,四个字段:

java 复制代码
public PermissionRule(String toolName,      // 规则作用在哪个工具
                      String ruleContent,   // 匹配条件,null 表示匹配所有调用
                      PermissionBehavior behavior,  // 命中后怎么办
                      String source)        // 规则来源标记,自己定义

behavior 有四种:ALLOW 放行、DENY 拒绝、ASK 询问用户、PASSTHROUGH 不表态。

规则通过 PermissionContextState.builder() 添加:

java 复制代码
PermissionContextState permCtx = PermissionContextState.builder()
        .mode(PermissionMode.DEFAULT)
        .addDenyRule("send_notification_tool", new PermissionRule(
                "send_notification_tool",   // toolName
                null,                       // null 匹配该工具的所有调用
                PermissionBehavior.DENY,
                "userSettings"
        ))
        .build();

对应有 addAllowRule、addDenyRule、addAskRule 三个方法,第一个参数写工具名,和规则里的 toolName 保持一致。

2.3 决策

PermissionDecision 是一次权限检查的结论:

java 复制代码
PermissionDecision.allow("理由")        // 放行
PermissionDecision.deny("理由")        // 拒绝
PermissionDecision.ask("理由")         // 需要用户确认
PermissionDecision.passthrough("理由") // 不表态

拒绝时模型会看到 message 里的理由,然后调整行为,要么向用户解释,要么换个方案。

3. 判定顺序

工具调用发生时,PermissionEngine 按下面的顺序检查,任何一层给出结论就直接生效,后面的层不再执行:

复制代码
工具调用
  ① deny 规则     命中 → DENY
  ② ask 规则      命中 → ASK
  ③ 工具自己的 checkPermissions(第04章那个"门卫")
       返回 PASSTHROUGH → 继续往下
       返回 ALLOW/DENY/ASK → 直接生效
  ④ allow 规则    命中 → ALLOW
  ⑤ 模式兜底
       BYPASS → ALLOW
       DONT_ASK → DENY
       其他 → ASK

deny 放在最前面,而且是铁律。哪怕 allow 规则写得再多、模式开成 BYPASS,deny 命中就是死。危险路径检查同样拦得住 BYPASS。

工具门卫一旦表态(返回 ALLOW/DENY/ASK),mode 和 rules 都覆盖不了,allow 规则救不回一个工具自己拒绝的调用。

兜底是 ASK。规则没接住、工具也不表态时,默认问用户,而不是放行。没有用户的场景(定时任务、无人值守)用 DONT_ASK,兜底变成直接拒绝。开头说的 acking 卡住,就是这个兜底在等一个永远不会来的确认。

4. 实战:三个实验

场景:一个办公助手,两个工具,查天气(只读)和发通知(写操作、敏感)。三种权限配置跑一遍。

4.1 准备工具

java 复制代码
package com.maple.agent.demo.demo_07;

import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;

public class BizTool {

    @Tool(
            name = "query_weather_tool",
            description = "当用户查询天气预报信息时,调用该工具",
            readOnly = true,
            concurrencySafe = true)
    public String queryWeatherTool(
            @ToolParam(name = "location", description = "查询地址") String location) {
        return location + "晴,18-28摄氏度";
    }

    @Tool(
            name = "send_notification_tool",
            description = "当用户需要发送通知消息时,调用该工具",
            readOnly = false,
            concurrencySafe = false)
    public String sendNotificationTool(
            @ToolParam(name = "receiver", description = "接收人邮箱") String receiver,
            @ToolParam(name = "content", description = "通知内容") String content) {
        // 实际项目这里是调用邮件/IM接口,demo里直接返回成功
        return "通知已发送给" + receiver + ":" + content;
    }
}

4.2 实验一:BYPASS

java 复制代码
PermissionContextState permCtx = PermissionContextState.builder()
        .mode(PermissionMode.BYPASS)
        .build();
// 问:给 boss@maple.com 发一条通知:明天上午十点开复盘会

send_notification_tool 被调用,state 是 SUCCESS,通知发出去了。

4.3 实验二:deny 规则拦截发通知

java 复制代码
PermissionContextState permCtx = PermissionContextState.builder()
        .mode(PermissionMode.DEFAULT)
        .addDenyRule("send_notification_tool", new PermissionRule(
                "send_notification_tool",
                null,
                PermissionBehavior.DENY,
                "userSettings"
        ))
        .build();
// 问:给 boss@maple.com 发一条通知:明天上午十点开复盘会

同样的问题再问一遍:

事件流里 send_notification_tool 的 state 变成了 DENIED,模型收到了拒绝理由,没有重试,而是在回复里告诉你通知发不出去。

deny 的理由写得越清楚,模型的应对越得体。写"禁止发送"和写"发送通知需要管理员审批,请告知用户自行审批",效果完全不一样。

4.4 实验三:allow + deny 组合

白名单加黑名单,生产里最常见的配置。查天气直接放行,发通知坚决拦住:

java 复制代码
PermissionContextState permCtx = PermissionContextState.builder()
        .mode(PermissionMode.DEFAULT)
        .addAllowRule("query_weather_tool", new PermissionRule(
                "query_weather_tool", null, PermissionBehavior.ALLOW, "userSettings"))
        .addDenyRule("send_notification_tool", new PermissionRule(
                "send_notification_tool", null, PermissionBehavior.DENY, "userSettings"))
        .build();
// 问:上海今天天气怎么样?顺便给 boss@maple.com 发一条通知:明天上午十点开复盘会

一句话里带了一个安全操作一个敏感操作:

query_weather_tool 正常执行,send_notification_tool 被拒绝。同一轮对话里两个工具两种结果。

5. ruleContent:把规则细到参数级

上面的规则都是整个工具一刀切。想更细呢?比如发通知的工具可以用,但只允许发给公司内邮箱。

规则里 ruleContent 不传 null,引擎就会调工具的 matchRule 方法判断是否命中。默认实现是 ruleContent != null 一律不匹配,所以按参数过滤得自己重写:

java 复制代码
// 在 ToolBase 子类里重写
@Override
public boolean matchRule(String ruleContent, Map<String, Object> toolInput) {
    // 约定 ruleContent 的格式就是域名,比如 "maple.com"
    String receiver = (String) toolInput.get("receiver");
    return receiver != null && receiver.endsWith("@" + ruleContent);
}

规则这么写:

java 复制代码
.addAskRule("send_notification_tool", new PermissionRule(
        "send_notification_tool",
        "maple.com",    // 只有发给 @maple.com 的调用命中这条规则
        PermissionBehavior.ASK,
        "userSettings"))

发给内部邮箱的需要确认,发给外部的没这条规则管。ruleContent 的格式完全由你和 matchRule 自定义,JSON、正则、DSL 都行。

6. 危险路径保护

规则配漏了怎么办?ToolBase 内置了一份危险路径清单(ToolDangerousPathConstants),文件类工具碰到这些路径,不管什么模式什么规则,强制走 ASK,连 BYPASS 都不放行。

类别 默认受保护的路径
Shell 配置 .bashrc、.zshrc、.bash_profile、.profile
Git 配置 .gitconfig、.gitmodules
SSH .ssh/config、.ssh/authorized_keys、id_rsa、id_ed25519
凭证 .env、.env.local、.npmrc、.pypirc、.aws/credentials
目录 .git/、.ssh/、.aws/、.kube/

模型要读 .env 里的密钥、改 id_rsa,不用自己配规则拦,框架默认就会拦住。自己的项目有别的敏感路径,在 @Tool 注解上追加 dangerousFiles / dangerousDirectories 即可。

7. ASK 了然后呢

ASK 是"问用户",问了之后怎么把答案传回去?这就是 HITL(Human-in-the-Loop),也是打通开头 acking 卡住的关键。

复制代码
权限引擎返回 ASK
  → agent 不执行工具,暂停本轮推理
  → 返回 PERMISSION_ASKING 标记和 ASKING 状态的 ToolUseBlock
  → 调用方展示待确认的操作,等用户拍板
  → 构建 ConfirmResult 传回 agent
  → 同意就继续执行,拒绝就把 "Permission denied by user" 回给模型

streamEvents 模式下不用去翻 Msg,事件流里会直接推一个 RequireUserConfirmEvent:

java 复制代码
agent.streamEvents(new UserMessage("user", "删除 /tmp/important.txt"))
        .doOnNext(event -> {
            if (event instanceof RequireUserConfirmEvent confirmEvent) {
                confirmEvent.getToolCalls().forEach(t ->
                        System.out.println("待确认: " + t.getName() + " " + t.getInput()));
            }
        })
        .blockLast();

用户拍板后,构建 ConfirmResult 传回去,agent 从暂停的地方接着跑:

java 复制代码
// 同意执行
ConfirmResult result = new ConfirmResult(true, toolCall);
// 拒绝,模型会收到拒绝结果并调整行为
ConfirmResult result = new ConfirmResult(false, toolCall);

7.1 建议规则

权限系统返回 ASK 的时候,会基于本次调用自动生成建议规则(ToolBase#generateSuggestions,默认建议放行当前工具)。用户点了"总是允许",把建议规则塞进 ConfirmResult 传回去:

java 复制代码
// agent 会自动写入引擎,下次相同的调用不再询问
ConfirmResult result = new ConfirmResult(true, toolCall, toolCall.getSuggestedRules());

用过 Claude Code 的 "Always allow" 应该秒懂,"记住我的选择"就是这么实现的。

另外,如果用户把本轮所有工具调用都拒了,agent 默认还会继续下一轮推理,模型看到的只有一排 "Permission denied by user",浪费 token。要止住的话,装一个 onActing middleware 监听 AllToolsDeniedEvent 发出停止事件,细节见官方 Middleware 文档。

8. 常见配方

只读探索

测试环境首选,agent 可以随便看,写操作全被拒:

java 复制代码
PermissionContextState explore = PermissionContextState.builder()
        .mode(PermissionMode.EXPLORE)
        .build();
无人值守自动化

定时任务、CI 里用。白名单之外一律静默拒绝:

java 复制代码
PermissionContextState ci = PermissionContextState.builder()
        .mode(PermissionMode.DONT_ASK)
        .addAllowRule("deploy", new PermissionRule("deploy", "staging",
                PermissionBehavior.ALLOW, "project"))
        .addAllowRule("git_commit", new PermissionRule("git_commit", null,
                PermissionBehavior.ALLOW, "project"))
        .build();
沙箱全放开 + 黑名单

可信环境想省事,又怕误伤关键操作:

java 复制代码
PermissionContextState sandbox = PermissionContextState.builder()
        .mode(PermissionMode.BYPASS)
        .addDenyRule("drop_table", new PermissionRule("drop_table", null,
                PermissionBehavior.DENY, "userSettings"))
        .addDenyRule("force_push", new PermissionRule("force_push", null,
                PermissionBehavior.DENY, "userSettings"))
        .build();

9. 本章小结

权限系统三块:模式、规则、决策。

判定顺序是 deny 规则、ask 规则、工具门卫、allow 规则、模式兜底,先到先得。deny 和危险路径连 BYPASS 都拦得住。兜底默认问用户,没有用户可问就拒绝。

ruleContent 加 matchRule 可以把控制做到参数级。ASK 的确认闭环靠 RequireUserConfirmEvent 和 ConfirmResult,建议规则让"记住我的选择"一行实现。

到这里,Tool 的定义(04)、接入(05 MCP)、管控(本章)就齐了。下一章聊聊怎么让 agent 自己变得更有条理。

本系列源码https://gitee.com/hack-feng/maple-agent-demo

相关推荐
小沈同学呀3 天前
【Agent开发第十三期】多模型路由:给 Agent 装一个“调度台“,实测省 86% 成本
人工智能·agent开发·多模型路由
笑小枫8 天前
AgentScope 学习系列 - 04:Tool 工具调用实战
agent·智能体开发·agentscope·java开发智能体
制造数据与AI践行者老蒋14 天前
排坑笔记:LangChain 多工具 Agent 完整性校验 return_intermediate_steps 事后核对方案
langchain·ai agent·工具调用·agent开发·排坑笔记·多工具协同·工程化 质量保障
制造数据与AI践行者老蒋15 天前
排坑笔记:LangChain 1.x API 迁移:create_react_agent 与 ChatOllama 导入错误完整解决方案
langchain·依赖管理·importerror·agent开发·ai开发环境·排坑笔记·版本兼容
可乐ea19 天前
从第一性原理构建 AI Agent:提示词、工具、技能与记忆全解剖
数据库·人工智能·工具调用·ai智能体·提示词工程·agent开发·智能体记忆
笑小枫21 天前
AgentScope 学习系列 - 01:环境搭建与初体验
智能体开发·agent开发·agentscope·java开发智能体·企业级智能体开发
爱学习的程序媛21 天前
2. 智能应用开发技术栈清单
ai·架构·系统架构·ai应用·智能体开发·智能应用开发
SLD_Allen21 天前
从 Agent Framework 到 Agent Harness,开源 AgentScope 项目新定位
agent framework·agentscope·agent harness