---
title: "harness - BashToolStrategy 安全审计策略"
---


当前 Bash 审计策略类是 `BashToolStrategy`，它实现 Agent 模块的 `HITLStrategy`：

```java
public class BashToolStrategy implements HITLStrategy {
    @Override
    public String evaluate(ReActTrace trace, Map<String, Object> args) {
        // 返回 null：放行
        // 返回非空字符串：进入 HITL，字符串作为审核原因
    }
}
```

Harness 默认把该策略绑定到 `bash`。但 HITL 默认关闭；要使它对 Harness Agent 生效，还需启用 `hitlEnabled`，并在 Agent 工具配置中包含 `hitl`：

```java
HarnessEngine engine = HarnessEngine.of("work", ".soloncode/")
        .sessionProvider(sessionProvider)
        .toolsAdd(ToolName.TOOL_BASH, ToolName.TOOL_HITL)
        .hitlEnabled(true)
        .build();
```

> `HarnessEngine.Builder.toolsAdd(...)` 只有三个重载：`toolsAdd(ToolName...)`、`toolsAdd(Collection<String>)`，以及已弃用的 `toolsAdd(ToolPermission...)`（4.0.4 起）。**没有 `String...` 重载**，所以 `.toolsAdd("bash", "hitl")` 无法编译（`javac` 会报“对于 toolsAdd(String,String)，找不到合适的方法”）。需要字符串写法时用 `toolsAdd(Arrays.asList("bash", "hitl"))`；`ToolName` 位于 `org.noear.solon.ai.harness.agent.ToolName`。

### 1、真实评估流程

`BashToolStrategy.evaluate()` 当前分两步：

1. 内置规则启用时（`internalRulesEnabled` 默认 `true`），按**注册顺序**直接检查内置规则 matcher；命中后立即返回放行或拦截结果。
2. 阶段 1 未命中时（`internalRulesEnabled(false)` 时则直接进入这一步），把权限上下文中的规则与内置规则合并，交给 `PermissionEngine` 评估，并以策略自身的 `defaultDecision` 作为未命中兜底。

`PermissionEngine` 的排序规则是：

- `priority` 越大越优先；
- 同一优先级下，`DENY > ALLOW > ASK`；
- 工具名 `*` 匹配所有工具，其他工具名采用忽略大小写的精确匹配；
- 参数规则可使用 glob pattern 或自定义 matcher。

需要特别注意默认值：

- `PermissionEngine.evaluate(toolName, args, context)` 三参数重载未命中时默认返回 `ASK`；
- `BashToolStrategy` 调用的是带 `defaultDecision` 的四参数重载，而它自身的 `defaultDecision` 默认为 `PermissionDecision.ALLOW`。

因此，**当前 Bash 策略未命中任何规则时默认放行**，不能把它描述为“所有非只读命令均要求人工确认”。如果需要默认询问，可显式配置：

```java
BashToolStrategy bashStrategy = new BashToolStrategy()
        .defaultDecision(PermissionDecision.ASK);
```

> `internalRulesEnabled(false)` 并不会移除内置规则。阶段 1 只是被跳过，阶段 2 仍然无条件把同一批内置规则并入（优先级 100）再评估，所以拦截结论基本不变，只是提示文案从各自的具体文案退化为通用文案（DENY 为“操作被权限策略拒绝。”，ASK 为“高危操作，需要人工介入确认。”）。个别规则还有顺序差异：阶段 1 按注册顺序（只读放行 6a 在 6b 之前），阶段 2 按 `DENY > ALLOW` 排序，因此 `internalRulesEnabled(false)` 会让 `cat file.txt |` 这类命令从放行变为拒绝。

### 2、内置 Bash 规则

`BashToolStrategy` 构造器注册的内置规则共 **7 条**（源码注释编号 1、2、3、4、5、6a、6b），优先级统一为 `INTERNAL_PRIORITY = 100`（高于用户规则的默认优先级 0）。并非所谓“七层安全体系”。

#### 2.1 空命令

`command` 为空（或 `null`）时直接放行，即 `evaluate()` 返回 `null`。规则 1 的 matcher 是 `Assert.isEmpty((String) args.get("command"))`，行为 `ALLOW`。

这与单独调用 `BashCommandClassifier.classify("")` 得到 `DENY` 的行为不同；`BashToolStrategy` 没有直接使用 `classify()` 的空命令分支。

#### 2.2 字符串风险模式

命令字符串（trim 后）包含以下任一片段时拒绝：

- 反引号 `` ` ``
- `$(`
- `/dev/`

提示为“检测到潜在的命令注入或设备重定向风险。”。这是字符串包含判断，不是完整的 Shell 语法分析。

#### 2.3 系统特权命令

当前正则只检查：

```text
.*\b(sudo|su|chown|chmod|chgrp|passwd|visudo)\b.*
```

即 `sudo`、`su`、`chown`、`chmod`、`chgrp`、`passwd`、`visudo` 七个词。该规则的 `prompt` 为 `null`，命中后由 `toResult()` 生成包含原命令的动态提示：

```text
检测到系统特权指令 [原命令]。
```

当前源码没有把 `kill`、`pkill`、`systemctl`、`service` 等进程或服务命令加入此规则。

> 因为是词边界匹配，`passwd` 也会命中 `cat ../../etc/passwd` 这类命令（它同时符合规则 4）；而规则 3 注册在规则 4 之前，实际提示会显示为“检测到系统特权指令”。

#### 2.4 路径回溯

命令字符串包含 `../` 或 `..\` 时拒绝，提示为“检测到路径回溯操作，禁止访问工作区外目录。”。该规则是字面字符串检查，不是对规范化文件路径执行真实边界解析。

#### 2.5 敏感路径

命令字符串包含以下任一片段时拒绝，提示为“禁止访问系统敏感配置文件。”：

```text
/etc/
/var/
/root/
~/.ssh/
~/.bashrc
~/.zshrc
```

同样，这些是字符串包含规则。

#### 2.6 只读命令分类

`BashCommandClassifier` 维护只读/搜索命令集合，包括：

- 搜索：`grep`、`egrep`、`fgrep`、`rg`、`ag`、`ack`、`find`、`fd`、`locate`
- 目录：`ls`、`dir`、`tree`、`exa`
- 文件查看：`cat`、`bat`、`less`、`more`、`head`、`tail`
- 信息与统计：`wc`、`file`、`which`、`whereis`、`whence`、`type`、`stat`、`du`、`df`
- 输出与文本处理：`echo`、`printf`、`diff`、`comm`、`sort`、`uniq`、`cut`、`tr`、`awk`、`sed`
- 格式化：`jq`、`yq`、`xmllint`
- 系统信息：`pwd`、`env`、`printenv`、`id`、`whoami`、`hostname`、`uname`、`date`、`cal`
- Git 只读双词命令：`git log`、`git show`、`git diff`、`git status`、`git branch`、`git tag`、`git remote`、`git rev-parse`、`git ls-files`、`git blame`、`git shortlog`

这些命令仍先经过前述风险规则。例如 `cat /etc/passwd` 会先命中系统特权命令规则（`passwd` 关键词，规则 3 早于规则 5），不能因为 `cat` 在只读集合中就放行。

只读分类还有两个例外：

- `sed -i`、`sed --in-place` 等原地修改形式不算只读；
- `git branch -d`、`git branch -D`、`git branch --delete` 不算只读。

普通输出重定向 `>`、`>>` 会使命令不再被归类为只读；当前实现对 `2>` 做了特殊排除，但这仍不是完整的 Shell 重定向解析。

> 这两个例外只影响“能否被规则 6a 直接放行”。在默认 `defaultDecision=ALLOW` 下，它们既没命中 6a 也没命中其他内置规则，最终仍是放行（把默认决策改成 `ASK` 才会体现差别）。

#### 2.7 管道与不完整命令

包含 `|` 时，分类器按管道分段；只有每一段都能被识别为只读命令，整个命令才按只读放行。这不是一份独立的“管道安全工具白名单”。

不完整命令规则（规则 6b，注册在只读放行 6a 之后）使用正则：

```text
(\|\s*|&&\s*|\|\|\s*|;\s*)$
```

即命令尾部是以下符号时按“不完整命令”拒绝，提示为“检测到不完整的命令或空命令。”（提示中的“空命令”实际不会出现：空命令已由规则 1 放行）：

```text
|  &&  ||  ;
```

> 由于规则 6a 排在 6b 之前，且管道分段使用 `split("\\|")` 会丢弃尾部空段，所以当命令头部本身是只读命令时，尾部符号不会触发 6b：`cat file.txt |`、`ls &&`、`ls ;` 都会被放行。只有非只读命令（如 `npm run build |`）才会命中 6b。该检查只针对尾部，不代表完整拦截 `;`、`&` 或其他组合命令。

### 3、当前没有声明的内置硬拦截

不要把以下能力归因于当前 `BashToolStrategy`：

- 不存在包管理器或环境变更命令的统一硬拦截；
- 不存在 `curl`、`wget`、`ssh` 等网络命令的统一硬拦截；
- 不存在 `rm -rf`、`mv` 的专门内置硬拦截；
- 不存在完整的多命令组合解析与统一拒绝；
- `BashCommandClassifier` 虽定义了 Git 写命令集合和 `isGitWriteCommand()`，但当前 `BashToolStrategy` 没有调用它。

这些命令如果没有命中其他内置规则或额外权限规则，会落到 Bash 策略的默认决策；当前默认是 `ALLOW`。

### 4、通过 PermissionRule 添加约束

额外规则应通过 `PermissionRule` / `PermissionContext` 配置，而不是假定内置策略已经覆盖。

#### 4.1 glob 规则

```java
HarnessEngine engine = HarnessEngine.of("work", ".soloncode/")
        .sessionProvider(sessionProvider)
        .toolsAdd(ToolName.TOOL_BASH, ToolName.TOOL_HITL)
        .hitlEnabled(true)
        .build();

engine.addPermissionRule(
        PermissionRule.deny("bash", "rm -rf *").priority(10));
engine.addPermissionRule(
        PermissionRule.ask("bash", "git push *").priority(10));
engine.addPermissionRule(
        PermissionRule.allow("bash", "git log *").priority(10));
```

pattern 会针对参数中第一个可用字符串字段匹配，字段顺序为：

```text
command, file_path, path, content, url, link
```

若这些字段都不是字符串，则退化为对 `args.toString()` 做匹配。

`*` 和 `?` 按 glob 转换（`globToRegex`），并且使用 `Pattern.matches()` 做整串匹配、忽略大小写。例如要匹配任意位置的关键词，需要在 pattern 中显式加入 `*`：`git push *` 无法匹配 `cd /tmp && git push origin main`。

> `PermissionRule.priority(int)` 返回新规则，调用结果必须使用。内置规则优先级为 100，用户规则要排在它们之前，priority 需要大于 100；否则只会在内置规则未命中时才有机会生效（阶段 1 的内置直接命中更早于引擎评估）。对于自定义 matcher，应直接使用带 priority 参数的 `PermissionRule.of(...)` 创建；不要再调用 `priority(...)`，因为当前实现只复制 `pattern`，会丢失 matcher 与 prompt。

#### 4.2 自定义 matcher

```java
PermissionRule denyNetwork = PermissionRule.of(
        "bash",
        PermissionBehavior.DENY,
        (toolName, args) -> {
            Object value = args == null ? null : args.get("command");
            if (!(value instanceof String)) {
                return false;
            }

            String command = ((String) value).trim();
            return command.startsWith("curl ")
                    || command.startsWith("wget ")
                    || command.startsWith("ssh ");
        },
        "网络命令被当前业务策略拒绝。",
        200);

engine.addPermissionRule(denyNetwork);
```

内置 matcher 在第一阶段直接命中时会立即返回。若业务要求完全不同的评估顺序或覆盖语义，更适合注册自定义 `HITLStrategy`，而不是依赖普通规则覆盖内置直接匹配。

### 5、自定义 HITLStrategy

```java
HITLStrategy auditStrategy = new HITLStrategy() {
    @Override
    public String evaluate(ReActTrace trace, Map<String, Object> args) {
        Object value = args == null ? null : args.get("command");
        String command = value instanceof String ? (String) value : "";

        if (command.contains("rm -rf")) {
            return "检测到递归删除操作：" + command;
        }
        if (command.startsWith("curl ") || command.startsWith("wget ")) {
            return "网络访问需要人工确认：" + command;
        }
        return null;
    }
};

HITLInterceptor hitl = new HITLInterceptor()
        .onTool("bash", auditStrategy);

HarnessEngine engine = HarnessEngine.of("work", ".soloncode/")
        .sessionProvider(sessionProvider)
        .toolsAdd(ToolName.TOOL_BASH, ToolName.TOOL_HITL)
        .hitlInterceptor(hitl)
        .hitlEnabled(true)
        .build();
```

替换 `HITLInterceptor` 会同时替换 Harness 默认的 Bash 策略和默认 `onApproved` 记忆回调；需要的行为应自行重新注册。

> Harness 的默认接线（`initHitlDefault()`，仅在未设置 `hitlInterceptor` 时执行）是：`new BashToolStrategy().permissionContextSupplier(options::getPermissionContext)` 注册到 `bash`；同时挂上默认 `onApproved` 回调——人工选择“总是允许”时，把本次的 `command`（write/edit 工具则取 `file_path`）作为 glob pattern 写入全局权限上下文，后续相同操作自动放行；没有可取字段时退化为工具级放行规则。

### 6、运行时开关

```java
engine.setHitlEnabled(true);
engine.setHitlEnabled(false);
```

该方法会同步更新 Harness 配置和拦截器的 enabled 状态。它只控制 HITL 是否启用，不会改变 `BashToolStrategy` 的规则集合或“未命中默认 ALLOW”的行为。
