第十四章 人机交互:Permission 系统 ALLOW/DENY/ASK 规则,运行时 HITL 自动拦截
"用户让 agent 删数据库——我们不希望它'问都不问就执行'。1.x 时代用
Hook.stopAgent()在onActing阶段抛异常中断;2.0 推荐用Permission系统——为每个 tool 配 ALLOW / DENY / ASK 规则,运行时自动拦截、提示用户、收集决策。这比 hook 优雅得多。"本章你将学到:
PermissionMode5 种模式、PermissionRule4 个字段、ASK模式下如何给前端推送确认请求、以及Middleware在 HITL 中的辅助角色。
14.1 1.x 时代怎么拦截工具?
1.x 时代要在工具调用前拦下来,业务方在 Hook.onActing 里 throw new StopAgentException()。问题:
-
异常语义不直观
-
不能区分"想确认" vs "直接拒绝"
-
没法让前端弹"是否放行"对话框,只能字符串错误
2.0 用
Permission系统替代了这套写法:
| 决策 | 含义 |
|---|---|
ALLOW | 直接执行 |
DENY | 直接拒绝 + 错误反馈给 LLM |
ASK | 暂停工具执行,推送确认请求给用户;用户回 ALLOW/DENY 后继续 |
PASSTHROUGH | 跳过本条规则,看下一条 |
14.2 第一个 Permission 例子
这个例子在演示什么?
你有一个 DB 管理员 agent,它有
drop_table这种危险工具。你不希望它"问都不问就删表"——所以给drop_table配了一条ASK规则:agent 想删表时,Permission 引擎会暂停执行,把确认请求推给前端,等用户点了"允许"才继续。
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/PASSTHROUGHsource— 规则来源,便于审计("userSettings"/"projectSettings"/"session"/"suggested")
14.3 5 种 PermissionMode
| Mode | 行为 | 适用场景 |
|---|---|---|
DEFAULT | 所有未命中规则都 ASK | 最安全,推荐默认值 |
ACCEPT_EDITS | 自动放行工作目录内的文件操作 | 用户在场的活跃开发 |
EXPLORE | 只读:放行读、拒绝所有写与命令 | 代码探索、规划 |
BYPASS | 放行一切(deny / ask 规则仍生效) | 完全可信的沙箱 |
DONT_ASK | 把所有 ASK 转为 DENY | 无人值守 / 计划任务 |
注意:
EXPLORE模式下 deny 是不可绕过的——即使在BYPASS模式下也照常生效。这是 2.0 的"危险工具不可绕过"原则。
14.3.1 PermissionContextState——整个权限系统的"配置单"
前面一直用 .builder().mode(...).addAskRule(...).build(),这个构建出来的对象就是 PermissionContextState。它是 agent 权限的全部配置,一张对象里包了三样东西:
PermissionContextState
├── mode ← 一个 PermissionMode(DEFAULT / ACCEPT_EDITS / EXPLORE / BYPASS / DONT_ASK)
├── allowRules ← Map<工具名, List<PermissionRule>> — 哪些工具直接放行
├── denyRules ← Map<工具名, List<PermissionRule>> — 哪些工具直接拒绝
└── askRules ← Map<工具名, List<PermissionRule>> — 哪些工具暂停问用户
核心地位:PermissionContextState 是 agent 能做什么、不能做什么的唯一配置文件。它不属于某个 session,不属于某个工具——它是整个 agent 的权限配置,在 HarnessAgent.builder().permissionContext(perms) 时注入,之后不可变。
你可以把它理解为"agent 的权限清单"——清单上写了哪些工具要问、哪些直接放、哪些打死不能用。PermissionEngine(权限引擎)在每次工具调用前照着清单判定。
14.4 ASK 的完整流程
- LLM 想调
drop_table - Permission 引擎查规则 →
ASK - 引擎生成"建议规则"——基于本次调用入参(
drop_table('orders_2024')) - 引擎发出
RequireUserConfirmEvent,前端通过streamEvents()订阅到 - 前端弹"是否放行"对话框
- 用户回 ALLOW + 接受建议规则
- 后端构造
ConfirmResult,通过UserConfirmResultEvent回传给 agent - 工具执行 + 自动把"建议规则"加入
PermissionContextState
import io.agentscope.core.event.ConfirmResult;
import io.agentscope.core.event.RequireUserConfirmEvent;
// 前端收到 RequireUserConfirmEvent 后,等用户决策再构造 ConfirmResult 回传:
public ConfirmResult handleConfirm(RequireUserConfirmEvent event) {
boolean userAllowed = showDialogAndWait(
event.getToolCalls().get(0).getName()); // 弹窗问用户
ToolUseBlock toolCall = event.getToolCalls().get(0); // 原始工具调用
return new ConfirmResult(
userAllowed,
userAllowed ? toolCall : null, // 允许时带原始调用,拒绝时为 null
userAllowed ? event.getSuggestedRules() : null // 允许时接受建议规则
);
}
实际框架中
RequireUserConfirmEvent不包含getSuggestedRules()方法,以上是概念示意。建议规则由PermissionEngine内部管理,通过ConfirmResult的rules字段透传回引擎。
14.5 Middleware 在 HITL 中的辅助角色
Permission 只解决"能不能跑",Middleware 解决"跑之前 / 之后还要做什么"。HITL 场景里常见的 Middleware 用法:
class HitlAuditMiddleware implements MiddlewareBase { // MiddlewareBase 是接口
@Override
public Flux<AgentEvent> onActing(
Agent agent,
RuntimeContext ctx,
ActingInput input,
Function<ActingInput, Flux<AgentEvent>> next) {
input.toolCalls().forEach(tc -> { // input.toolCalls() 获取工具调用列表
auditLog.info("user={} tool={}",
ctx.getUserId(), // ctx 直接取 userId
tc.getName());
});
return next.apply(input); // 必须调 next,否则工具不执行
}
}
Permission 给"能不能跑"的答案;Middleware 记"谁、什么时候、跑了什么"。
14.6 与前端 SSE 的协作
streamEvents() 会发出 RequireUserConfirmEvent:
event: REQUIRE_USER_CONFIRM
data: {"toolCalls":[{"name":"drop_table","input":{"table":"orders_2024"}}]}
前端订阅后弹窗;用户决策通过 UserConfirmResultEvent 回传(内含 ConfirmResult 列表),agent 自动继续执行,无需额外 API。
14.7 1.x Hook.stopAgent 还能用吗?
能,但不推荐。io.agentscope.core.hook.Hook 在 2.0 标 @Deprecated,但语义保留:
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 完整可运行示例
这个例子在演示什么?
你有一个客服 agent,配了 3 个工具:
query_order(查订单)、refund_order(退款)、drop_table(删表——危险操作)。我们希望:
- 查订单:直接放行,不打扰用户(ALLOW)
- 退款:弹窗问用户"是否确认退款?"(ASK)
- 删表:直接拒绝,agent 根本没机会删(DENY)
下面用一个 agent 配 3 条 PermissionRule,连续问 3 次,每次触发不同行为:
public class Chapter14_FullHitl {
// 业务工具类:三个工具对应三种 Permission 行为
public static class OrderTools {
@Tool(name = "query_order", description = "查询订单状态")
public String queryOrder(String orderId) {
return "订单 " + orderId + ":已发货。";
}
@Tool(name = "refund_order", description = "处理订单退款")
public String refundOrder(String orderId, String amount) {
return "订单 " + orderId + " 退款 " + amount + " 元已处理。";
}
@Tool(name = "drop_table", description = "删除数据库表(危险操作)")
public String dropTable(String tableName) {
return "表 " + tableName + " 已删除。";
}
}
public static void main(String[] args) {
// 1. 注册工具
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new OrderTools());
// 2. 配置权限规则
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();
// 3. 构造 agent(禁用 shell 工具避免 Windows 兼容问题)
HarnessAgent agent = HarnessAgent.builder()
.name("customer_service")
.sysPrompt("你是客服,只能调用 query_order、refund_order、drop_table 工具。")
.model(model())
.workspace(Path.of("./workspace"))
.toolkit(toolkit)
.permissionContext(perms)
.disableShellTool()
.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();
}
}
注意:
addAllowRule/addAskRule/addDenyRule的第一个参数是精确工具名,不支持通配符。必须写"query_order"而非"query_*"。
14.9 本章小结
- 2.0 用
Permission系统做工具调用级 HITL,比 1.xHook.stopAgent优雅。 - 5 种
PermissionMode适配不同部署场景;4 种行为ALLOW/DENY/ASK/PASSTHROUGH。 ASK模式:PermissionAskEvent推前端 + 用户回ConfirmResult。Deny规则和危险路径检查不可绕过——BYPASS模式也拦不住。Middleware留给"日志/埋点"等不需要决策的副作用。