mirror of https://gitee.com/dromara/liteFlow
feat(agent): PermissionConfigMapper 把 ShellConfig 映射到 v2 PermissionContextState(命令级规则)
Task 3.1:把 liteflow ShellConfig(mode/whitelist/blacklist)翻译成 v2 PermissionContextState 的命令级规则,替代 1.0 在 ManagedShellCommandTool 内 手写的命令过滤。执行期由 v2 PermissionEngine 在工具调用前裁决。 探针(findings R6-ext,直接读 RC3 源码): - PermissionEngine 不解析 ruleContent;null/空=catch-all,非空完全委派 tool.matchRule(content, input)。RC3 core 无任何 ToolBase 子类覆写 matchRule,故 matcher DSL 由 Task 3.2 的自建工具定义。 - 默认裁决顺序:denyRules→askRules→工具自检→allowRules→BYPASS→default (DEFAULT=ASK,DONT_ASK=DENY)。有 allowRules 不隐含其余 deny。 ruleContent 约定(Task 3.1 + Task 3.2 共同): - 精确匹配首 token = 裸字符串(如 "ls") - catch-all = null 各 mode 映射: - DISABLED → DEFAULT + catch-all deny - WHITELIST → DONT_ASK + 每条白名单 allow(非白名单经 default 转 DENY; 不能用 catch-all deny——denyRules 先于 allowRules 会吞掉白名单) - BLACKLIST → DEFAULT + 每条黑名单 deny + catch-all allow 兜底 ReactAgentFactory.build 加 .permissionContext(PermissionConfigMapper.map(cfg))。 map() 永非 null(null/缺省/未识别枚举回退安全默认)。 校验:core compile SUCCESS;test-compile SUCCESS; PermissionConfigMapperTest 5/5(javac+JUnit Launcher,sibling 测试致模块 mvn test 不能跑,沿用 V2ApiProbe 手法);ProcessIntegrationTest 1/1 (permission 不破坏非 shell 调用)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
01cd0ddc32
commit
39d20d38f9
|
|
@ -228,6 +228,55 @@ PermissionContextState.builder()
|
|||
|
||||
---
|
||||
|
||||
## R6-ext:matcher DSL 精确语义 + 默认裁决(Task 3.1 探针实测)
|
||||
|
||||
**来源:** 对 `agentscope-2.0.0-RC3-sources.jar` 中 `io/agentscope/core/permission/PermissionEngine.java`、`PermissionContextState.java`、`PermissionRule.java` 与 `io/agentscope/core/tool/ToolBase.java` 全量源码通读(JDK 21)。**非反射探针——直接读 RC3 源码,比反射更权威。**
|
||||
|
||||
**核心修正(推翻 R6 "由 PermissionEngine 实现 OpenAI 风格 DSL" 的推测):**
|
||||
|
||||
1. **`PermissionEngine` 不解析 `ruleContent`。** 全 jar 唯一读 `rule.ruleContent()` 的地方是 `PermissionEngine.ruleMatches(tool, rule, input)`:
|
||||
```java
|
||||
private boolean ruleMatches(ToolBase tool, PermissionRule rule, Map<String,Object> input) {
|
||||
String content = rule.ruleContent();
|
||||
if (content == null || content.isEmpty()) {
|
||||
return true; // ← catch-all:null/空 content 匹配一切
|
||||
}
|
||||
return tool.matchRule(content, input); // ← 否则完全委托给工具自己解析
|
||||
}
|
||||
```
|
||||
**matcher DSL 不是 agentscope 内置,而是各工具(`ToolBase` 子类)自己实现 `matchRule(content, input)` 时定义。**
|
||||
|
||||
2. **RC3 core 里没有任何 `ToolBase` 子类覆写 `matchRule`。** `ToolBase` 的实现是 `return ruleContent == null;`(只认 catch-all)。三个 `extends ToolBase` 的类(`SchemaOnlyTool`/`ReflectiveFunctionTool`/`McpTool`)均未覆写 `matchRule`。而 `io.agentscope.core.tool.coding.ShellCommandTool` 实现的是 `AgentTool`(非 `ToolBase`),**根本不参与 `PermissionEngine` 裁决**(只有 `ToolBase` 子类经 `Toolkit` 注册后才会被引擎 gate)。全 jar `grep matchRule` 命中:仅 `PermissionEngine`、`PermissionRule`、`ToolBase` 三处。
|
||||
|
||||
**结论:R6 里举例的 `"command =~ '^ls .*'"` / `"command IN [...]"` / `"command == 'rm'"` 等 DSL 字符串,RC3 里没有任何代码能解析**——它们只是 R6 文档的示意,不是可用语法。
|
||||
|
||||
3. **默认裁决顺序(`PermissionEngine.checkPermission`,实测源码):**
|
||||
denyRules(最高)→ askRules → 工具自检(`ToolBase.checkPermissions`,默认 passthrough;EXPLORE/ACCEPT_EDITS read-only 在此)→ allowRules → BYPASS 模式放行 → **default ASK(DONT_ASK 模式下转 DENY)**。
|
||||
- **关键:有 allowRules 并不隐含"其余 deny"**——未命中任何规则的调用落到 default ASK,**不是** deny。要实现"白名单才允许、其余拒绝"语义,**必须显式加一条 catch-all DENY 规则**(`ruleContent = null`)兜底,否则非白名单命令只会触发 ASK(或 DONT_ASK 下的 DENY),不是稳定的 DENY。
|
||||
|
||||
4. **catch-all 规则** = `new PermissionRule(toolName, null, behavior, source)`(`ruleContent` 传 `null`)。`PermissionRule` 紧凑构造器只对 `toolName`/`behavior`/`source` 做 `Objects.requireNonNull`,**`ruleContent` 允许 null**,引擎见 null 即匹配一切。
|
||||
|
||||
5. **输入字段名**:matcher 第二参数 `Map<String,Object> input` 即工具收到的 `toolInput`。字段名由工具的 `@Tool`/`@ToolParam` 参数名决定——对自建 `ManagedShellCommandTool` 而言就是其参数名(约定 `command`)。
|
||||
|
||||
**对 Task 3.1(PermissionConfigMapper)的最终指导(已采用):**
|
||||
|
||||
由于 matcher DSL 完全由自建工具定义,Task 3.1 与 Task 3.2(恢复 `ManagedShellCommandTool`)**共同约定**一套简单、易解析的 `ruleContent` 格式:
|
||||
- **每条命令一行精确匹配**:`ruleContent = "<firstToken>"`(即裸 token 字符串,如 `"ls"`、`"rm"`)。Task 3.2 的 `ManagedShellCommandTool.matchRule(content, input)` 实现为:取 `input.get("command")` 的首 token,与 `content` 字符串 `equals` 比对。这比 R6 示例的 `command == 'ls'` DSL 更简单、无需引号/转义解析器,且天然表达"首 token 等于 X"语义。
|
||||
- **catch-all**:`ruleContent = null`(引擎原生支持)。
|
||||
|
||||
各 `ShellMode` 的规则构造(mapper 行为):
|
||||
- **DISABLED**:单条 `addDenyRule("execute_shell_command", new PermissionRule(TOOL, null, DENY, "shell-config"))` —— catch-all deny,禁用整个工具。
|
||||
- **WHITELIST**(默认):每条白名单命令一条 `addAllowRule(TOOL, new PermissionRule(TOOL, "<token>", ALLOW, "shell-config"))`;**外加一条** `addDenyRule(TOOL, new PermissionRule(TOOL, null, DENY, "shell-config"))` 兜底,使非白名单命令稳定 DENY(而非落到 default ASK)。deny 优先级高于 allow,但白名单 allow 与 catch-all deny 不会冲突:catch-all 只匹配"未被前面 allow 规则认领"的调用(实际引擎是逐条 evaluate,denyRules 先扫——见下注意)。
|
||||
- **⚠️ 顺序注意**:引擎先评估 denyRules 再评估 allowRules。若 catch-all deny 放在 WHITELIST,它会在 allowRules 之前命中**所有**命令(包括白名单内的),导致白名单也被 deny。**因此 WHITELIST 模式不能用 catch-all deny 实现兜底**——正确做法:**只加白名单 allow 规则,mode 设为 `DONT_ASK`**,使未命中白名单的调用经 default path 转 DENY(DONT_ASK 模式 defaultDecisionAsk 返回 DENY)。这是 RC3 下表达"白名单"语义的唯一正确方式。
|
||||
- **BLACKLIST**:每条黑名单命令一条 `addDenyRule(TOOL, new PermissionRule(TOOL, "<token>", DENY, "shell-config"))`,**外加一条** `addAllowRule(TOOL, new PermissionRule(TOOL, null, ALLOW, "shell-config"))` 兜底放行。因引擎 denyRules 先于 allowRules 评估,黑名单命令会被前面的 deny 命中拒绝,其余命令落到后面 catch-all allow 被放行——干净实现"黑名单拒绝、其余允许",不依赖 mode 副作用(mode 保持 DEFAULT 即可)。**不要**用 BYPASS 模式实现(BYPASS 对所有工具全放行、且会绕过规则评估,粒度太粗)。
|
||||
|
||||
**最终采用(见 PermissionConfigMapper 实现):**
|
||||
- DISABLED → mode DEFAULT + 单条 catch-all deny 规则(toolName 级),禁用整个 `execute_shell_command` 工具。
|
||||
- WHITELIST → mode **DONT_ASK** + 每条白名单 allow 规则(无 catch-all deny,否则会先于 allow 吞掉白名单)。非白名单命令经 default path + DONT_ASK 转 DENY,白名单命令命中 allow 放行。
|
||||
- BLACKLIST → mode DEFAULT + 每条黑名单 deny 规则 + 一条 catch-all allow 兜底(deny 先于 allow 评估,黑名单硬拒、其余放行)。
|
||||
|
||||
---
|
||||
|
||||
## R7:JDK 21 baseline
|
||||
|
||||
**结论(实测):** agentscope core 2.0.0-RC3 + 所声明的 4 个扩展(redis/mysql/oss/skill-git-repository)在 **JDK 21.0.2** 下全部解析成功;`liteflow-react-agent-core` 干净编译(`mvn -pl .../liteflow-react-agent-core -am clean compile` → **BUILD SUCCESS**);`V2ApiProbe` 6/6 测试绿。baseline 成立。
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
package com.yomahub.liteflow.agent.component;
|
||||
|
||||
import com.yomahub.liteflow.agent.exception.AgentConfigException;
|
||||
import com.yomahub.liteflow.agent.permission.PermissionConfigMapper;
|
||||
import com.yomahub.liteflow.agent.state.AgentStateStoreResolver;
|
||||
import com.yomahub.liteflow.property.agent.AgentConfig;
|
||||
import io.agentscope.core.ReActAgent;
|
||||
import io.agentscope.core.model.Model;
|
||||
import io.agentscope.core.permission.PermissionContextState;
|
||||
import io.agentscope.core.state.AgentStateStore;
|
||||
import io.agentscope.core.tool.Toolkit;
|
||||
|
||||
|
|
@ -44,11 +46,12 @@ import java.util.concurrent.ConcurrentHashMap;
|
|||
* {@code registerTool(Object)}(v2 反射注册 {@code @Tool} 方法)。</li>
|
||||
* <li>{@code maxIters} — {@code cmp.maxIterations() > 0 ? it : cfg.defaults.maxIterations}。</li>
|
||||
* <li>{@code stateStore} — {@link AgentStateStoreResolver#resolve(AgentConfig)}(可能返回 null=NONE,合法)。</li>
|
||||
* <li>{@code permissionContext} — {@link PermissionConfigMapper#map(AgentConfig)}(ShellConfig→v2 命令级规则,Task 3.1)。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p><b>不设(留给后续 Task / RC3 无):</b>
|
||||
* {@code .filesystem()/.workspace()/.compaction()/.memory(MemoryConfig)}(RC3 无 HarnessAgent 这些方法)、
|
||||
* {@code .middleware(...)(Task 5.1)、.permissionContext(...)(Task 3.1)、
|
||||
* {@code .middleware(...)(Task 5.1)、
|
||||
* {@code .skillRepository(...)(Task 4.1)。本类不引用这些协作者。
|
||||
*
|
||||
* <h2>线程安全</h2>
|
||||
|
|
@ -119,6 +122,10 @@ public final class ReactAgentFactory {
|
|||
}
|
||||
|
||||
AgentStateStore stateStore = AgentStateStoreResolver.resolve(cfg);
|
||||
// ShellConfig → v2 PermissionContextState 命令级规则(findings R6-ext);永非 null,
|
||||
// 在工具调用前由 v2 PermissionEngine 裁决 execute_shell_command。Task 3.2 恢复
|
||||
// ManagedShellCommandTool 后,该工具的 matchRule 即按本 mapper 产出的 ruleContent 约定解析。
|
||||
PermissionContextState permissionContext = PermissionConfigMapper.map(cfg);
|
||||
|
||||
try {
|
||||
return ReActAgent.builder()
|
||||
|
|
@ -128,6 +135,7 @@ public final class ReactAgentFactory {
|
|||
.toolkit(toolkit)
|
||||
.maxIters(maxIters)
|
||||
.stateStore(stateStore) // null 合法 = NONE 语义(findings R1)
|
||||
.permissionContext(permissionContext)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
throw new AgentConfigException(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,161 @@
|
|||
package com.yomahub.liteflow.agent.permission;
|
||||
|
||||
import com.yomahub.liteflow.property.agent.AgentConfig;
|
||||
import com.yomahub.liteflow.property.agent.ShellConfig;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
import io.agentscope.core.permission.PermissionBehavior;
|
||||
import io.agentscope.core.permission.PermissionContextState;
|
||||
import io.agentscope.core.permission.PermissionMode;
|
||||
import io.agentscope.core.permission.PermissionRule;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 把 liteflow {@link ShellConfig}(mode/whitelist/blacklist)翻译成 v2
|
||||
* {@link PermissionContextState} 的命令级规则,替代 1.0 在 {@code ManagedShellCommandTool}
|
||||
* 内手写的命令过滤。执行期由 v2 {@code PermissionEngine} 在工具调用前裁决。
|
||||
*
|
||||
* <h2>工具名</h2>
|
||||
* 所有规则 target {@link #SHELL_TOOL_NAME},即 Task 3.2 将恢复的
|
||||
* {@code ManagedShellCommandTool} 的 {@code @Tool(name=...)}(与 1.0 一致)。
|
||||
*
|
||||
* <h2>matcher DSL(findings R6-ext,实测 RC3 源码)</h2>
|
||||
* {@code PermissionEngine} 不解析 {@code ruleContent}——匹配完全委托给
|
||||
* {@code ToolBase.matchRule(content, input)}。RC3 core 里<b>没有</b>任何
|
||||
* {@code ToolBase} 子类覆写 {@code matchRule}(基类实现 {@code return ruleContent == null;}
|
||||
* 只认 catch-all),故本 mapper 与 Task 3.2 的 {@code ManagedShellCommandTool.matchRule}
|
||||
* 共同约定一套简单格式:
|
||||
* <ul>
|
||||
* <li><b>精确匹配某命令首 token</b>:{@code ruleContent = "<token>"}(裸字符串,如 {@code "ls"})。
|
||||
* Task 3.2 的工具取 {@code input.get("command")} 首 token 与之 {@code equals} 比对。</li>
|
||||
* <li><b>catch-all(匹配一切)</b>:{@code ruleContent = null}(引擎原生支持)。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>各 mode 的规则构造</h2>
|
||||
* 引擎评估顺序(实测):denyRules → askRules → 工具自检 → allowRules → BYPASS → default
|
||||
* (DEFAULT 模式 default=ASK,DONT_ASK 模式 default=DENY)。<b>有 allowRules 并不隐含"其余 deny"。</b>
|
||||
* <ul>
|
||||
* <li><b>{@link ShellMode#DISABLED}</b>:单条 catch-all deny({@code ruleContent=null}),
|
||||
* mode {@link PermissionMode#DEFAULT}。整条工具禁用。</li>
|
||||
* <li><b>{@link ShellMode#WHITELIST}</b>(默认):每条白名单命令一条精确 allow 规则,
|
||||
* mode {@link PermissionMode#DONT_ASK}。白名单命令命中 allow 放行;非白名单不命中任何规则,
|
||||
* 经 default path + DONT_ASK 转 DENY。注意:<b>不能</b>用 catch-all deny 兜底——denyRules
|
||||
* 先于 allowRules 评估,catch-all deny 会把白名单命令也一并吞掉。DONT_ASK 是唯一干净表达
|
||||
* "白名单才放行"的 RC3 语义。</li>
|
||||
* <li><b>{@link ShellMode#BLACKLIST}</b>:每条黑名单命令一条精确 deny 规则 + 一条 catch-all
|
||||
* allow 兜底,mode {@link PermissionMode#DEFAULT}。deny 先于 allow 评估:黑名单命令被前面 deny
|
||||
* 拒绝,其余落到后面 catch-all allow 放行。不依赖 mode 副作用(不用 BYPASS——它对所有工具全放行、
|
||||
* 且绕过规则评估,粒度太粗)。</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>永非 null 契约</h2>
|
||||
* {@link #map(AgentConfig)} 始终返回非 null {@link PermissionContextState}——即便入参为 null 或
|
||||
* shell 配置缺失,也回退到一个安全默认(DEFAULT mode 无规则)。调用方({@code ReactAgentFactory.build})
|
||||
* 直接 {@code .permissionContext(map(cfg))} 即可。
|
||||
*/
|
||||
public final class PermissionConfigMapper {
|
||||
|
||||
/**
|
||||
* 受管 shell 工具名常量:与 Task 3.2 将恢复的 {@code ManagedShellCommandTool} 的
|
||||
* {@code @Tool(name=...)} 一致(沿用 1.0 命名)。
|
||||
*/
|
||||
public static final String SHELL_TOOL_NAME = "execute_shell_command";
|
||||
|
||||
/** 规则来源标签({@link PermissionRule#source()})。 */
|
||||
private static final String SOURCE = "shell-config";
|
||||
|
||||
private PermissionConfigMapper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 {@link AgentConfig#getShell()} 翻译成 {@link PermissionContextState}。
|
||||
*
|
||||
* @param cfg agent 配置;可为 null(回退安全默认,不抛)
|
||||
* @return 永远非 null 的 {@link PermissionContextState}
|
||||
*/
|
||||
public static PermissionContextState map(AgentConfig cfg) {
|
||||
if (cfg == null) {
|
||||
return safeDefault();
|
||||
}
|
||||
ShellConfig shell = cfg.getShell();
|
||||
if (shell == null) {
|
||||
return safeDefault();
|
||||
}
|
||||
ShellMode mode = shell.getMode();
|
||||
if (mode == null) {
|
||||
mode = ShellMode.WHITELIST;
|
||||
}
|
||||
switch (mode) {
|
||||
case DISABLED:
|
||||
return disabledContext();
|
||||
case WHITELIST:
|
||||
return whitelistContext(shell.getWhitelist());
|
||||
case BLACKLIST:
|
||||
return blacklistContext(shell.getBlacklist());
|
||||
default:
|
||||
// 未来新增枚举值:保守回退安全默认(不抛,不臆造映射)
|
||||
return safeDefault();
|
||||
}
|
||||
}
|
||||
|
||||
/* ----- 各 mode 的规则构造 ----- */
|
||||
|
||||
/** DISABLED:单条 catch-all deny,整条工具禁用。 */
|
||||
private static PermissionContextState disabledContext() {
|
||||
return PermissionContextState.builder()
|
||||
.mode(PermissionMode.DEFAULT)
|
||||
.addDenyRule(SHELL_TOOL_NAME, catchAll(PermissionBehavior.DENY))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* WHITELIST:每条白名单命令一条精确 allow + mode DONT_ASK(非白名单经 default 转 DENY)。
|
||||
*/
|
||||
private static PermissionContextState whitelistContext(List<String> whitelist) {
|
||||
PermissionContextState.Builder b = PermissionContextState.builder()
|
||||
.mode(PermissionMode.DONT_ASK);
|
||||
if (whitelist != null) {
|
||||
for (String cmd : whitelist) {
|
||||
if (cmd != null && !cmd.isBlank()) {
|
||||
b.addAllowRule(SHELL_TOOL_NAME, exactToken(cmd.trim(), PermissionBehavior.ALLOW));
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* BLACKLIST:每条黑名单命令一条精确 deny + 一条 catch-all allow 兜底放行。
|
||||
*/
|
||||
private static PermissionContextState blacklistContext(List<String> blacklist) {
|
||||
PermissionContextState.Builder b = PermissionContextState.builder()
|
||||
.mode(PermissionMode.DEFAULT);
|
||||
if (blacklist != null) {
|
||||
for (String cmd : blacklist) {
|
||||
if (cmd != null && !cmd.isBlank()) {
|
||||
b.addDenyRule(SHELL_TOOL_NAME, exactToken(cmd.trim(), PermissionBehavior.DENY));
|
||||
}
|
||||
}
|
||||
}
|
||||
// catch-all allow 兜底:deny 先评估,非黑名单命令落到此被放行。
|
||||
b.addAllowRule(SHELL_TOOL_NAME, catchAll(PermissionBehavior.ALLOW));
|
||||
return b.build();
|
||||
}
|
||||
|
||||
/* ----- rule factories ----- */
|
||||
|
||||
/** 精确匹配某命令首 token:{@code ruleContent = token}(裸字符串,工具侧 equals 比对)。 */
|
||||
private static PermissionRule exactToken(String token, PermissionBehavior behavior) {
|
||||
return new PermissionRule(SHELL_TOOL_NAME, token, behavior, SOURCE);
|
||||
}
|
||||
|
||||
/** catch-all:{@code ruleContent = null}(引擎见 null 匹配一切)。 */
|
||||
private static PermissionRule catchAll(PermissionBehavior behavior) {
|
||||
return new PermissionRule(SHELL_TOOL_NAME, null, behavior, SOURCE);
|
||||
}
|
||||
|
||||
/** 安全默认:DEFAULT mode、无规则(最保守回退;不会误放行也不会误拒绝已配置的语义)。 */
|
||||
private static PermissionContextState safeDefault() {
|
||||
return PermissionContextState.builder().mode(PermissionMode.DEFAULT).build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
package com.yomahub.liteflow.test.agent.v2;
|
||||
|
||||
import com.yomahub.liteflow.agent.permission.PermissionConfigMapper;
|
||||
import com.yomahub.liteflow.property.agent.AgentConfig;
|
||||
import com.yomahub.liteflow.property.agent.ShellConfig;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
import io.agentscope.core.permission.PermissionBehavior;
|
||||
import io.agentscope.core.permission.PermissionContextState;
|
||||
import io.agentscope.core.permission.PermissionMode;
|
||||
import io.agentscope.core.permission.PermissionRule;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Task 3.1 单元测试:验证 {@link PermissionConfigMapper} 把 liteflow {@link ShellConfig}
|
||||
* (mode/whitelist/blacklist)正确翻译成 v2 {@link PermissionContextState} 的命令级规则。
|
||||
*
|
||||
* <p>纯单元:不构建 ReActAgent、不调 LLM、不起 Spring。只断言规则结构(mode + 三张规则表)。
|
||||
*
|
||||
* <p>matcher DSL 与默认裁决语义来自探针(findings R6-ext):
|
||||
* <ul>
|
||||
* <li>catch-all = {@code ruleContent == null}({@link PermissionEngine} 见 null 即匹配一切)。</li>
|
||||
* <li>引擎评估顺序:denyRules → askRules → tool 自检 → allowRules → BYPASS → default(DEFAULT 模式 ASK,DONT_ASK 转 DENY)。</li>
|
||||
* <li>有 allowRules 并不隐含"其余 deny"——需显式兜底规则。</li>
|
||||
* </ul>
|
||||
*/
|
||||
class PermissionConfigMapperTest {
|
||||
|
||||
private static final String TOOL = "execute_shell_command";
|
||||
|
||||
/** DISABLED:整体禁用 shell 工具——catch-all deny。 */
|
||||
@Test
|
||||
void disabled_mapsToCatchAllDeny() {
|
||||
AgentConfig cfg = shellCfg(ShellMode.DISABLED, Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
PermissionContextState ctx = PermissionConfigMapper.map(cfg);
|
||||
|
||||
assertNotNull(ctx, "map() 永远返回非 null");
|
||||
assertEquals(PermissionMode.DEFAULT, ctx.getMode(), "DISABLED 用 DEFAULT mode 即可(catch-all deny 已足够)");
|
||||
|
||||
// allow / ask 表为空
|
||||
assertTrue(ctx.getAllowRules().isEmpty(), "DISABLED 不应有 allow 规则");
|
||||
assertTrue(ctx.getAskRules().isEmpty(), "DISABLED 不应有 ask 规则");
|
||||
|
||||
// deny 表含一条 execute_shell_command 的 catch-all(ruleContent == null)
|
||||
List<PermissionRule> denies = ctx.getDenyRules().get(TOOL);
|
||||
assertNotNull(denies, "DISABLED 必须为 " + TOOL + " 注册 deny 规则");
|
||||
assertEquals(1, denies.size(), "DISABLED 仅需一条 catch-all deny");
|
||||
PermissionRule deny = denies.get(0);
|
||||
assertEquals(TOOL, deny.toolName());
|
||||
assertEquals(PermissionBehavior.DENY, deny.behavior());
|
||||
assertNull(deny.ruleContent(), "catch-all deny 的 ruleContent 必须为 null");
|
||||
}
|
||||
|
||||
/** WHITELIST:白名单命令逐条 allow,mode=DONT_ASK 使非白名单命令经 default 转 DENY。 */
|
||||
@Test
|
||||
void whitelist_mapsToPerCommandAllowInDontAskMode() {
|
||||
AgentConfig cfg = shellCfg(ShellMode.WHITELIST, Arrays.asList("ls", "cat"), Collections.emptyList());
|
||||
|
||||
PermissionContextState ctx = PermissionConfigMapper.map(cfg);
|
||||
|
||||
assertEquals(PermissionMode.DONT_ASK, ctx.getMode(),
|
||||
"WHITELIST 用 DONT_ASK:白名单命中 allow 放行,非白名单经 default 转 DENY(不能用 catch-all deny,"
|
||||
+ "否则 denyRules 优先级会先吞掉白名单)");
|
||||
|
||||
// deny / ask 表为空(非白名单的拒绝靠 DONT_ASK default path,不靠显式 deny 规则)
|
||||
assertTrue(ctx.getDenyRules().isEmpty(), "WHITELIST 不应有 deny 规则");
|
||||
assertTrue(ctx.getAskRules().isEmpty(), "WHITELIST 不应有 ask 规则");
|
||||
|
||||
// allow 表含 ls / cat 两条精确匹配规则
|
||||
List<PermissionRule> allows = ctx.getAllowRules().get(TOOL);
|
||||
assertNotNull(allows);
|
||||
assertEquals(2, allows.size(), "whitelist=[ls,cat] → 2 条 allow 规则");
|
||||
for (PermissionRule r : allows) {
|
||||
assertEquals(TOOL, r.toolName());
|
||||
assertEquals(PermissionBehavior.ALLOW, r.behavior());
|
||||
assertNotNull(r.ruleContent(), "每条 allow 的 ruleContent = 命令首 token(非 null)");
|
||||
}
|
||||
// 内容必须是 ls 与 cat(顺序无强约束,但 mapper 按配置顺序产出,断言之)
|
||||
assertEquals("ls", allows.get(0).ruleContent());
|
||||
assertEquals("cat", allows.get(1).ruleContent());
|
||||
}
|
||||
|
||||
/** BLACKLIST:黑名单命令逐条 deny + 一条 catch-all allow 兜底放行其余。deny 先于 allow 评估。 */
|
||||
@Test
|
||||
void blacklist_mapsToPerCommandDenyPlusCatchAllAllow() {
|
||||
AgentConfig cfg = shellCfg(ShellMode.BLACKLIST, Collections.emptyList(), Collections.singletonList("rm"));
|
||||
|
||||
PermissionContextState ctx = PermissionConfigMapper.map(cfg);
|
||||
|
||||
assertEquals(PermissionMode.DEFAULT, ctx.getMode(),
|
||||
"BLACKLIST 用 DEFAULT mode:靠 catch-all allow 放行非黑名单,不依赖 mode 副作用");
|
||||
|
||||
assertTrue(ctx.getAskRules().isEmpty(), "BLACKLIST 不应有 ask 规则");
|
||||
|
||||
// deny 表含 rm 精确匹配
|
||||
List<PermissionRule> denies = ctx.getDenyRules().get(TOOL);
|
||||
assertNotNull(denies);
|
||||
assertEquals(1, denies.size(), "blacklist=[rm] → 1 条 deny 规则");
|
||||
PermissionRule deny = denies.get(0);
|
||||
assertEquals(TOOL, deny.toolName());
|
||||
assertEquals(PermissionBehavior.DENY, deny.behavior());
|
||||
assertEquals("rm", deny.ruleContent(), "deny 规则 ruleContent = 黑名单命令首 token");
|
||||
|
||||
// allow 表含一条 catch-all(ruleContent == null)兜底
|
||||
List<PermissionRule> allows = ctx.getAllowRules().get(TOOL);
|
||||
assertNotNull(allows, "BLACKLIST 必须有 catch-all allow 兜底,否则非黑名单命令落到 default ASK 而非 ALLOW");
|
||||
assertEquals(1, allows.size());
|
||||
PermissionRule allow = allows.get(0);
|
||||
assertEquals(TOOL, allow.toolName());
|
||||
assertEquals(PermissionBehavior.ALLOW, allow.behavior());
|
||||
assertNull(allow.ruleContent(), "catch-all allow 的 ruleContent 必须为 null");
|
||||
}
|
||||
|
||||
/** 默认构造 AgentConfig(mode=WHITELIST 默认值 + 默认白名单)→ 非 null,合理结构。 */
|
||||
@Test
|
||||
void defaultConfig_neverNull_andHasDefaultWhitelistAllows() {
|
||||
AgentConfig cfg = new AgentConfig(); // shell.mode 默认 WHITELIST,whitelist 默认一批只读命令
|
||||
|
||||
PermissionContextState ctx = PermissionConfigMapper.map(cfg);
|
||||
|
||||
assertNotNull(ctx, "默认配置也必须返回非 null PermissionContextState");
|
||||
assertEquals(PermissionMode.DONT_ASK, ctx.getMode(), "默认 mode=WHITELIST → DONT_ASK");
|
||||
// 默认 whitelist 非空(ShellConfig 默认 ~40 条),故 allow 规则非空
|
||||
List<PermissionRule> allows = ctx.getAllowRules().get(TOOL);
|
||||
assertNotNull(allows);
|
||||
assertFalse(allows.isEmpty(), "默认白名单应产出 allow 规则");
|
||||
}
|
||||
|
||||
/** map(null) 不 NPE——返回一个安全的默认 context(永非 null 契约)。 */
|
||||
@Test
|
||||
void nullAgentConfig_returnsSafeDefault() {
|
||||
PermissionContextState ctx = PermissionConfigMapper.map(null);
|
||||
|
||||
assertNotNull(ctx, "map(null) 也必须返回非 null");
|
||||
// 不抛、结构合理即可(具体 mode/规则不强约束,只要不 NPE 且非 null)
|
||||
}
|
||||
|
||||
/* ----- helpers ----- */
|
||||
|
||||
private static AgentConfig shellCfg(ShellMode mode, List<String> whitelist, List<String> blacklist) {
|
||||
AgentConfig cfg = new AgentConfig();
|
||||
ShellConfig shell = cfg.getShell();
|
||||
shell.setMode(mode);
|
||||
shell.setWhitelist(whitelist);
|
||||
shell.setBlacklist(blacklist);
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue