# 第十四章 人机交互:Permission 系统详解——ALLOW/DENY/ASK 规则与运行时HITL自动拦截机制
## 14.1 1.x 时代怎么拦截工具?
先回顾下历史。在AgentScope 1.x版本中,要在工具调用前实现人工拦截,业务方只能通过在`Hook.onActing`里抛出`StopAgentException()`来处理。表面上看简单,实际存在不少隐患:
图片内容:AgentScope Ja va权限系统版本演进对比示意图,展示从1.x的异常抛方式到2.0的Permission系统升级路径。
最大的问题在于:异常语义完全不清晰。你无法区分这个中断是“用户想确认一下”还是“用户直接拒绝了”。更麻烦的是,前端无法根据这个异常弹出“是否放行”的对话框——因为传递的只是一个字符串级别的错误信息,缺乏结构化的决策空间。
到了2.0,`Permission`系统彻底解决了这些痛点,核心是一套清晰的决策模型:
| 决策 |
含义 |
ALLOW |
直接允许执行 |
DENY |
直接拒绝,并将错误信息反馈给LLM |
ASK |
暂停工具执行,向用户推送确认请求;用户回复ALLOW/DENY后继续 |
PASSTHROUGH |
跳过当前规则,继续评估下一条 |
这四种决策的组合几乎可以覆盖所有HITL场景。
## 14.2 第一个 Permission 例子
这个例子的核心演示目的是什么?
```java
import io.agentscope.core.agent.RuntimeContext;
import io.agentscope.core.message.UserMessage;
import io.agentscope.core.model.DashScopeChatModel;
import io.agentscope.core.permission.*;
import io.agentscope.harness.HarnessAgent;
import java.util.List;
public class Chapter14_Permission {
public static void main(String[] args) {
// 1. 配置 PermissionContext:采用 ACCEPT_EDITS 模式,默认允许大部分操作
// 但针对 drop_table 必须人工确认(ASK)
PermissionContextState perms = PermissionContextState.builder()
.mode(PermissionMode.ACCEPT_EDITS)
.addAskRule("drop_table",
new PermissionRule("drop_table",null, // null 表示匹配所有 drop_table 调用
PermissionBehavior.ASK,"userSettings"))
.build();
// 2. 构建 agent
HarnessAgent agent = HarnessAgent.builder()
.name("db_admin")
.sysPrompt("你是一个 DB 管理员;可以查表,但删表必须先问。")
.model(DashScopeChatModel.builder()
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.modelName("qwen-plus")
.build())
.workspace(Path.of("./workspace"))
.permissionContext(perms)
.build();
agent.call(List.of(new UserMessage("user", "把 orders_2024 表 drop 掉。")),
RuntimeContext.empty()).block();
// 执行到 drop_table 时,Permission 会发出 ConfirmRequest,前端需要回应
}
}
```
你可以看到,`PermissionRule`的四个字段分工很明确:
- `toolName` — 工具名,告诉系统这条规则针对哪个工具
- `ruleContent` — 匹配模式,null意味着对所有调用都生效
- `behavior` — `ALLOW`/`DENY`/`ASK`/`PASSTHROUGH`,决策核心
- `source` — 规则来源,便于审计(`"userSettings"` / `"projectSettings"` / `"session"` / `"suggested"`)
## 14.3 5 种 PermissionMode
不同的部署场景对权限的宽容度自然不同。系统内置了五种模式来应对:
| Mode |
行为 |
适用场景 |
DEFAULT |
所有未命中规则都ASK |
最安全,推荐默认值 |
ACCEPT_EDITS |
自动放行工作目录内的文件操作 |
用户在场的活跃开发 |
EXPLORE |
只读:放行读操作,拒绝所有写操作与命令 |
代码探索、规划 |
BYPASS |
放行一切(deny / ask 规则仍生效) |
完全可信的沙箱 |
DONT_ASK |
把所有 ASK 转为 DENY |
无人值守 / 计划任务 |
其实这些模式的选取原则很简单:你在什么场景下信任Agent到什么程度。
### 14.3.1 `PermissionContextState`——整个权限系统的“配置单”
前面一直用`.builder().mode(...).addAskRule(...).build()`,这个构建出来的就是`PermissionContextState`。它是agent权限的全部配置,一个对象里封装了三样东西:
```
PermissionContextState
├── mode ← 一个 PermissionMode(DEFAULT / ACCEPT_EDITS / EXPLORE / BYPASS / DONT_ASK)
├── allowRules ← Map<工具名, List
> — 哪些工具直接放行
├── denyRules ← Map<工具名, List> — 哪些工具直接拒绝
└── askRules ← Map<工具名, List> — 哪些工具暂停问用户
```
它的核心地位很明确:`PermissionContextState`是agent能做什么、不能做什么的唯一配置文件。它不属于某个session,不属于某个工具——它是整个agent的权限配置,在`HarnessAgent.builder().permissionContext(perms)`时注入,之后不可变。
你可以把它理解为“agent的权限清单”——清单上明确了哪些工具需要询问、哪些直接放行、哪些禁止使用。`PermissionEngine`(权限引擎)在每次工具调用前依据这份清单进行判定。
## 14.4 ASK 的完整流程
当LLM想调用`drop_table`时,系统内部走的是如下流程:
1. Permission引擎查询规则 → 命中`ASK`
2. 引擎生成“建议规则”——基于本次调用入参(`drop_table('orders_2024')`)
3. 引擎通过`streamEvents()`的`PermissionAskEvent`将`ConfirmRequest`推送给前端
4. 前端弹出“是否放行”对话框
5. 用户回复ALLOW → 接受建议规则
6. 后端调用`agent.call(...)`时将`ConfirmResult`注入回session
7. 工具执行 → 自动将“建议规则”加入`PermissionContextState`
```java
import io.agentscope.core.event.ConfirmResult;
import io.agentscope.core.event.ConfirmRequest;
public Map handleAsk(ConfirmRequest req) {
// 1. 推送给前端
sendToFrontend(req);
// 2. 等待用户回复 ALLOW
boolean userAllowed = waitForUserDecision();
// 3. 构造 ConfirmResult
ConfirmResult result = new ConfirmResult(
userAllowed,
req.getToolCall(),
userAllowed ? req.getSuggestedRules() : List.of() // 接受建议
);
return Map.of("decision", result);
}
```
这里的重点是:建议规则的自动附加机制。如果用户点了“放行”,系统会把这次调用的入参打包成一条规则,自动加入配置清单。也就是说,下次同样的操作就不会再问了——这是一种渐进式的信任建立。
## 14.5 Middleware 在 HITL 中的辅助角色
`Permission`解决的是“能不能跑”,`Middleware`解决的是“跑之前/之后还要做什么”。在HITL场景里,Middleware最常见的用法是做审计日志:
```java
class HitlAuditMiddleware extends MiddlewareBase {
@Override
public Mono onActing(MiddlewareContext ctx, HookEvent event) {
event.getToolCalls().forEach(tc -> {
auditLog.info("user={} tool={} input={}",
ctx.runtime().getUserId(),
tc.getName(),
tc.getInput());
});
return Mono.just(event);
}
}
```
一句话总结两者的分工:`Permission`给出“能不能跑”的答案;`Middleware`记录“谁、什么时候、跑了什么”。
## 14.6 与前端 SSE 的协作
`streamEvents()`会发出`PermissionAskEvent`,SSE事件格式如下:
```
event: permission-ask
data: {"toolCallId":"tc-1","toolName":"drop_table","input":{"table":"orders_2024"},"suggestedRules":[...]}
```
前端订阅后弹窗;用户决策通过另一个HTTP端点回传(`POST /api/agent/permission/respond`),后端用`agent.resume(sessionId, confirmResult)`继续执行。
注意,`agent.resume(...)`是2.0新增的——专门用于ASK暂停后的恢复。这是整个HITL流程中最后一个关键拼图。
## 14.7 1.x `Hook.stopAgent` 还能用吗?
能用,但真的不推荐了。`io.agentscope.core.hook.Hook`在2.0已标记为`@Deprecated`,但语义保留,旧代码仍然可以运行:
```java
class LegacyHitlHook implements Hook {
@Override
public void onActing(HookEvent event) {
if (event.getToolCalls().stream()
.anyMatch(t -> "drop_table".equals(t.getName()))) {
throw new StopAgentException("drop_table is forbidden");
}
}
}
```
新代码请统一使用`Permission`系统;`Hook`只适用于“写日志/埋点”这类不需要ASK的场景。这体现了一种渐进式的平滑迁移策略——老项目不必一次性重写,但新项目一定要走新路。
## 14.8 完整可运行示例
这个例子演示的是一套完整的权限控制体系:
```java
public class Chapter14_FullHitl {
public static void main(String[] args) {
PermissionContextState perms = PermissionContextState.builder()
.mode(PermissionMode.DEFAULT)
.addAllowRule("query_order",
new PermissionRule("query_order", null,
PermissionBehavior.ALLOW, "userSettings"))
.addAskRule("refund_order",
new PermissionRule("refund_order", null,
PermissionBehavior.ASK, "userSettings"))
.addDenyRule("drop_table",
new PermissionRule("drop_table", null,
PermissionBehavior.DENY, "userSettings"))
.build();
HarnessAgent agent = HarnessAgent.builder()
.name("customer_service")
.sysPrompt("你是客服,可以查订单,但退款需要用户确认,删表绝对禁止。")
.model(model())
.workspace(Path.of("./workspace"))
.permissionContext(perms)
.build();
// 查订单 → ALLOW:工具直接执行,无任何中断
agent.call(List.of(new UserMessage("user", "查订单 123")),
RuntimeContext.empty()).block();
// 退款 → ASK:Permission 引擎暂停,发出 ConfirmRequest 等待前端回应
agent.call(List.of(new UserMessage("user", "给订单 123 退款 100 元")),
RuntimeContext.empty()).block();
// 删表 → DENY:工具调用被直接拒绝,agent 收到错误反馈
agent.call(List.of(new UserMessage("user", "删掉 orders 表")),
RuntimeContext.empty()).block();
}
}
```
三种行为对应三种结果,完美呼应了前文讲述的所有规则机制。
## 14.9 本章小结
- 2.0采用`Permission`系统进行工具调用级别的HITL,相比1.x的`Hook.stopAgent`更加优雅,语义清晰,前端友好。
- 5种`PermissionMode`适配不同部署场景;4种行为`ALLOW`/`DENY`/`ASK`/`PASSTHROUGH`覆盖所有决策需求。
- `ASK`模式的完整链路:`PermissionAskEvent`推送到前端 → 用户回复`ConfirmResult` → 自动附加建议规则。
- `Deny`规则和危险路径检查不可绕过——即便`BYPASS`模式也无法忽略。
- `Middleware`留给“日志/埋点”这类无需决策的副作用,与`Permission`各司其职。
这套权限系统的设计思路其实很简单:将决策权从开发者的异常处理逻辑中解放出来,让业务方专注于配置,让运行时引擎自动处理拦截、确认和恢复。这是从“人肉防御”到“系统化防御”的关键一步。
游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。