@cyia/pi-block-find-root

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

Packages

Package details

extension

Install @cyia/pi-block-find-root from npm and Pi will load the resources declared by the package manifest.

$ pi install npm:@cyia/pi-block-find-root
Package
@cyia/pi-block-find-root
Version
0.10.15
Published
Nov 3, 2026
Downloads
2,671/mo · 1,476/wk
Author
wszgrcy
License
MIT
Types
extension
Size
48.9 KB
Dependencies
3 dependencies · 2 peers
Pi manifest JSON
{
  "extensions": [
    "./src/block-find-root.ts",
    "./src/default-timeout.ts",
    "./src/custom-comp.ts",
    "./src/check2.ts"
  ]
}

Security note

Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.

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 当前项目(需项目被信任)
{
	"blockFindRoot": {
		"forbiddenPaths": ["/", "/etc"],
		"blockBash": true
	},
	"defaultTimeout": 120,
	"guardLoop": {
		"mode": "model",
		"maxContinuations": 5
	}
}
键 默认 作用
forbiddenPaths ["/"] 禁用路径列表
blockBash false 是否检测 bash 命令里的 find。不写就是关

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

默认超时

bash 命令没写 timeout 就补上,默认 120s。配置一个数:

{ "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,可单独测):

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"。

配置

{
  "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。零依赖。

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() 里按路径把值取出来,就一个函数。

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 即可。

开发

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