@cyia/pi-block-find-root
pi 扩展:拦截 find 查询禁用路径、给 bash 补默认超时、守卫循环
Package details
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+ 条 |
解析步骤:
lexSegments— 一遍扫描同时切段 + 分词:引号外的; & | ( ) 换行是段边界, 引号里的不算;$( )与反引号(单引号外)会递归解析里面的内容;\x取字面量;findCallArgs— 跳过命令词前面的VAR=赋值、数字参数、包装器 (sudoenvtimetimeoutnohupnicexargswatchstdbufbusyboxcommand) 和选项(含sudo -u root这种带值的);- 命令词是
find(含/usr/bin/find,大小写不敏感)就收下它后面的参数;findstr、echo find /、git find之类不算;命令词是 shell 则递归解析一层; pathCandidates— 丢掉选项(-name/-path/-maxdepth等连值一起丢)、 丢掉重定向(2>/dev/null)、-exec系列直接截断(后面是子命令);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 优先。
不会死循环
- 续跑上限 ——
used >= maxContinuations就清除守卫并放行,不静默停。 - fail-open —— 问不到人(无 UI / Esc / 超时)一律当满足放行。 守卫是刹车,不是门槛,问不到人不能把会话堵死。
- 只在收工时问 ——
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