# @cyia/pi-block-find-root 中文文档

规范地址：https://npmcn.com/package/%40cyia/pi-block-find-root/

版本：0\.10\.15
npm 元数据更新时间：2026-11-03T09:54:57.949Z
中文译文更新时间：2026-11-11T07:10:25.316Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40cyia/pi-block-find-root
原文对照：https://npmcn.com/package/%40cyia/pi-block-find-root/?lang=original

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：50.1 kB，13 个文件
- 模块格式：ES Module
- TypeScript 类型：未声明
- 运行依赖：3 个
- peer 依赖：typebox \*，@earendil-works/pi-coding-agent \*
- 首次发布：2026-09-21T09:38:06.407Z
- 最新版本发布：2026-11-03T09:54:57.773Z
- 近 30 天下载量：3383（2026-10-11 至 2026-11-09，npm 官方下载统计）
- 已发布版本数：20（近 12 个月 20 个）

## 中文简介

pi 扩展：拦截 find 查询禁用路径、给 bash 补默认超时、守卫循环

## 安装

```sh
npm install @cyia/pi-block-find-root
```

也可以使用：`pnpm add @cyia/pi-block-find-root`、`yarn add @cyia/pi-block-find-root`、`bun add @cyia/pi-block-find-root`

## README

# pi-block-find-root

pi 扩展集合：拦截 `find` 查询禁用路径、给 `bash` 补默认超时、守卫循环。运行环境固定 Linux / msys2，只处理 POSIX 路径。

```
find(path="/etc", pattern="*")
→ 禁止使用 /etc 查询内容

bash(command="find / -name aaa")     # 需 blockBash: true
→ 禁止使用 bash 调用 find 查询 /（命中：/）
```

> ⚠️ 防误操作的护栏，不是安全边界。`read`、`grep` 等工具不受影响； bash 里的 `find` 会被解析拦截，但不可能覆盖所有变形（见下）。

## 配置

写在 pi 的 `settings.json`，项目覆盖全局：

| 文件 | 作用域 |
| --- | --- |
| `~/.pi/agent/settings.json` | 全局 |
| `.pi/settings.json` | 当前项目（需项目被信任） |

```json
{
    "blockFindRoot": {
        "forbiddenPaths": ["/", "/etc"],
        "blockBash": true
    },
    "defaultTimeout": 120,
    "guardLoop": {
        "mode": "model",
        "maxContinuations": 5
    }
}
```

| 键 | 默认 | 作用 |
| --- | --- | --- |
| `forbiddenPaths` | `["/"]` | 禁用路径列表 |
| `blockBash` | `false` | 是否检测 `bash` 命令里的 `find`。**不写就是关** |

`find` 工具本身始终受管，`blockBash` 只管 `bash` 那一条。逐键回退： 项目写了哪个键就用项目的，没写的键去看全局，都没有则用默认值。

## 默认超时

bash 命令没写 `timeout` 就补上，默认 **120s**。配置一个数：

```json
{ "defaultTimeout": 300 }
```

模型自己给了 `timeout` 就不动它。长驻命令（dev server、watch）自己写个大的， 或者 `"defaultTimeout": 2147483`。实现在 `src/default-timeout.ts`。

## 命中规则

输入路径 **等于** 禁用路径，或 **在该路径之下**。

以 `forbiddenPaths: ["/etc"]` 为例：

| `find` 的 `path` | 结果 |
| --- | --- |
| `/etc` | 🚫 |
| `/etc/passwd` | 🚫 |
| `/etcfoo` | ✅ 放行（不是子路径） |
| 省略 | 按 `cwd` 判定 |

比较前只做轻量归一：去掉尾部通配符（`/etc/*` → `/etc`）。 不展开 `~`、不解析符号链接，按 POSIX 路径原样比。

## bash 里的 find（默认关，`blockBash: true` 才开）

解析逻辑全部在 `src/bash-find-parser.ts`（纯函数，不依赖 pi，可单独测）：

```ts
import { extractFindPaths } from "./bash-find-parser.ts";
extractFindPaths("sudo timeout 1 find / -name aaa"); // → [["/"]]
```

职责划分：

| 文件 | 干什么 |
| --- | --- |
| `src/bash-find-parser.ts` | 只解析：从 command 里抽出每次 `find` 的路径参数 |
| `src/path-match.ts` | 只比较：路径归一 + 「等于或在之下」 |
| `src/block-find-root.ts` | 只拦截：读配置、把上面两个接起来、返回 `{ block: true }` |
| `test/` | 三个文件各一套用例，共 170+ 条 |

解析步骤：

1.  `lexSegments` — 一遍扫描同时切段 + 分词：引号外的 `; & | ( ) 换行` 是段边界， 引号里的不算；`$( )` 与反引号（单引号外）会递归解析里面的内容；`\x` 取字面量；
2.  `findCallArgs` — 跳过命令词前面的 `VAR=` 赋值、数字参数、包装器 （`sudo` `env` `time` `timeout` `nohup` `nice` `xargs` `watch` `stdbuf` `busybox` `command`） 和选项（含 `sudo -u root` 这种带值的）；
3.  命令词是 `find`（含 `/usr/bin/find`，大小写不敏感）就收下它后面的参数； `findstr`、`echo find /`、`git find` 之类不算；命令词是 shell 则递归解析一层；
4.  `pathCandidates` — 丢掉选项（`-name` / `-path` / `-maxdepth` 等连值一起丢）、 丢掉重定向（`2>/dev/null`）、`-exec` 系列直接截断（后面是子命令）；
5.  `find` 没写路径参数时由 `block-find-root` 按 `cwd` 判定。

已覆盖的变形：`sudo -u root find /etc`、`timeout 1 find /`、`find "/"`、`fi\nd /`、 `find /etc\ dir`（带空格的真实路径）、`x=$(find /)`、`` y=`find /etc` ``、 `echo "a $(find /)"`（双引号里会真执行 → 拦）、`bash -c "find /"`、`a; find /etc`。

已知拦不住：路径来自 stdin/变量展开后再交给别的程序、自写脚本里调 `find`、 `docker` / `ssh` 里跑 find、`read`/`grep` 直接读禁用路径。护栏而已，别当安全边界。

## 守卫循环（`guardLoop`）

干活前多给一条「守卫条件」。Agent 每次想收工时，必须先调 `guard_check` 工具， **由你本人判定条件满足没满足**：

-   你说满足 → 工具返回 `{ end: true }` → Agent 停止，守卫清除
-   你说不满足 → 工具把「还差什么」返回给 Agent → 接着干
-   Agent 没调 `guard_check` 就想停 → 打回继续干

`guard_check` 只在守卫生效期间挂给模型，平时模型看不到这个工具。判定就是本地 扫一眼会话记录，**不多发一次模型请求**，也不解析模型输出的 JSON。

```
/guard npm test 全绿，并且新增代码都有测试
把 validate() 的测试补上
```

第一行只是挂上守卫（不开跑），第二行就是普通提示词。

```
[Agent] 改完了，validate() 的测试补上了
[守卫] 提问：你是否做完了？（第 1/5 次续跑）
[Agent] （继续跑测试、修失败用例……）
[Agent] toolCall guard_check { note: "npm test 全绿，12 passing" }
[UI]    守卫条件是否已满足：npm test 全绿…   ❯ ✅ 满足了，可以收工
                                    🔁 还没，继续干
[守卫] ✅ 用户确认条件已满足
```

### 命令

| 写法 | 作用 |
| --- | --- |
| `/guard` | 看当前守卫 |
| `/guard <条件>` | 设守卫（接下来正常描述任务） |
| `/guard off` | 解除 |
| `/guard max [n]` | 看 / 改续跑上限（本会话） |

条件正好以 `off` / `max` 这些关键字开头时，拿引号包起来：`/guard "max 文件改成 json"`。

### 配置

```json
{
  "guardLoop": {
    "maxContinuations": 5,
    "askTimeoutMs": 0,
    "auto": ""
  }
}
```

| 键 | 默认 | 作用 |
| --- | --- | --- |
| `maxContinuations` | `5` | 最多续跑几次，到顶强制放行 |
| `askTimeoutMs` | `0` | 问人超时，0 = 不限时 |
| `auto` | `""` | 非空则会话一开始就自动挂上这条守卫（无头跑 / 固定项目护栏） |

同样项目覆盖全局、逐键回退。`/guard max` 改的是**本会话**默认值，存在会话条目里， 比 settings 优先。

### 不会死循环

1.  **续跑上限** —— `used >= maxContinuations` 就清除守卫并放行，不静默停。
2.  **fail-open** —— 问不到人（无 UI / Esc / 超时）一律当满足放行。 守卫是刹车，不是门槛，问不到人不能把会话堵死。
3.  **只在收工时问** —— `outcome !== "completed"`（被中断 / 出错）不检查。

### 实现

| 文件 | 干什么 |
| --- | --- |
| `src/guard-eval.ts` | 纯函数：参数解析、会话状态投影、完成确认识别、注入文案 |
| `src/guard-loop.ts` | 胶水：`guard_check` 工具（动态挂载）、`/guard` 命令、`agent_before_settle` 检查 |
| `test/guard-eval.test.ts` | 纯函数用例 |

边界选的是 `agent_before_settle`：pi 文档里写的「最终可操作边界」，能追加条目并请求一次 续跑。`turn_end` 每个 turn 都触发（工具往返之间），`agent_end` 之后 pi 还可能自己重试或处理 排队消息，`agent_settled` 只能通知改不了。

状态全部存会话条目，不用内存变量（重载会话、切分支都会重建）：

| `customType` | 进上下文 | 内容 |
| --- | --- | --- |
| `guard` | ✗ | `{kind:"set",condition}` / `{kind:"clear"}` |
| `guard-continue` | ✗ | 每次续跑一条，条数就是已用次数 |
| `guard-defaults` | ✗ | `/guard max` 改的本会话默认值 |
| `guard-condition` | ✓ | 告诉模型有这条守卫、收工前必须调 `guard_check` |
| `guard-feedback` | ✓ | 没确认就停下时的“提问：你是否做完了？” |

判定就是 `findGuardAck()`：在设守卫那条条目之后，找 `toolName === "guard_check"` 的 `toolResult`，看它的 `details.end`。`true` 就放行，其他一律算没完成。

工具本身在 `execute` 里弹框问你（`ctx.ui.select`），选“还没”会再问一句“还差什么”， 原话作为工具结果返回给模型；你说满足时结果里带 `terminate: true`，直接收工。

## 会话条目读取（写扩展用）

`src/session-query.ts`：查 `pi.appendEntry()` 手动存进会话的东西。 输入 branch 条目列表 + key 列表，返回每个 key **最后写入的 data**。零依赖。

```ts
import { readEntries, readEntry } from "./session-query.ts";

const branch = ctx.sessionManager.getBranch();

const { guard, guardDefaults } = readEntries(branch, ["guard", "guard-defaults"]); // 多个 key
const last = readEntry(branch, "guard"); // 就一个 key
```

key 就是 `pi.appendEntry(customType, data)` 里的 customType；没写过 = `undefined`。 倒着扫，每个 key 命中第一条就是最后一次写的，全部凑齐就收工（这些条目都是刚写的， 就在会话尾部，2 万条会话下不用全扫）。只认 `type === "custom"`，同名的 `custom_message`（`pi.sendMessage` 写的，会进模型上下文）不会被混进来。 用例见 `test/session-query.test.ts`。

## 配置读取封装（写扩展用）

`src/settings.ts`：从 `pi.getSettings()` 里按路径把值取出来，就一个函数。

```ts
import { readSetting } from "./settings.ts";

// 默认值是对象时深合并（配置只写了一部分键，没写的键用默认；数组整份顶掉默认）
const cfg = readSetting(pi, "blockFindRoot", { forbiddenPaths: ["/"], blockBash: false });
const sec = readSetting(pi, "defaultTimeout", 120);

// 路径三种写法（直接交给 es-toolkit 的 get）：
readSetting(pi, "guardLoop.maxContinuations", 5);              // 点号
readSetting(pi, ["guardLoop", "maxContinuations"], 5);         // 数组
readSetting(pi, ["users", 0, "name"], "");                     // 数组里带下标
```

**全局 + 项目的合并是 pi 的事**：`pi.getSettings()` 返回的就是生效值（全局 + 项目 + CLI override 合好，项目不受信任时它自己也不读项目那份），扩展不该再读一遍 settings.json 再合一次。所以这里没有 `SettingsManager`、没有缓存、没有两层合并。

**只读**：配置是人手改文件改出来的，没有运行时写入，所以没有任何 setter。 **不校验**：文件里写的是什么类型就是什么类型（`"120"` 就是字符串），值不对是配置写错了， 用它的扩展自己发现。路径用 `es-toolkit/compat` 的 `get`，合并用 `es-toolkit/object` 的 `mergeWith`（默认值先 `cloneDeep` —— `merge` 会改第一个参数；数组走自定义 customizer 整份顶掉，因为 es-toolkit 默认是按下标合，`forbiddenPaths: []` 就关不掉了）。 唯一新增依赖 `es-toolkit`。用例见 `test/settings.test.ts`。

## 本地试

本仓库 `.pi/settings.json` 已配好规则，`.pi/extensions/` 里有转发文件， 直接在这个目录跑 `pi` 即可。

## 开发

```bash
npm install
npm run typecheck
npm test          # node --test，320+ 条
```

测试分四层：

| 文件 | 盖住什么 |
| --- | --- |
| `test/bash-find-parser.test.ts` | 解析：该认的必须认，不该认的绝不认 |
| `test/path-match.test.ts` | 比较：等于 / 子路径 / 兄弟目录不误伤 / 通配符归一 |
| `test/block-find-root.test.ts` | 端到端：喂真实 `tool_call` 事件，配置直接喂假 `pi.getSettings()` |
| `test/guard-eval.test.ts` | 守卫：参数解析 / 状态投影 / 完成确认识别 / 上限 / 注入文案 |
| `test/guard-eval-fuzz.test.ts` | 差分：随机条目序列下，倒扫投影与正向全扫参考实现结果必须一致 |
| `test/session-query.test.ts` | 会话条目读取（通用）：最后写入的 data、custom\_message 不混、脏数据 |
| `test/settings.test.ts` | 配置读取：顶层键 / 点号路径 / 数组路径、默认值逐键合并、值原样返回 |

## License

MIT

## 常见问题

### @cyia/pi-block-find-root 是什么？

pi 扩展：拦截 find 查询禁用路径、给 bash 补默认超时、守卫循环

### 如何安装 @cyia/pi-block-find-root？

在项目目录运行 npm install @cyia/pi-block-find-root，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：typebox（\*）、@earendil-works/pi-coding-agent（\*）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @cyia/pi-block-find-root 支持 TypeScript 吗？

该包的 package\.json 没有声明类型入口。需要类型提示时，可以在 npm 上查找社区维护的 @types/cyia\_\_pi-block-find-root 是否存在。

### @cyia/pi-block-find-root 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @cyia/pi-block-find-root 可以商用吗？

@cyia/pi-block-find-root 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @cyia/pi-block-find-root 的下载量有多少？

根据 npm 官方下载统计，@cyia/pi-block-find-root 在 2026/10/11 – 2026/11/09 这 30 天内被下载 3,383 次，日均约 113 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @cyia/pi-block-find-root 还在维护吗？

最新版本 0\.10\.15 发布于 2026/11/03（7 天前）；至今共发布 20 个版本，其中过去 12 个月发布了 20 个；首次发布于 2026/09/21。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

