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
