@zhushanwen/pi-permission 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +455 -0
  2. package/index.ts +2 -0
  3. package/package.json +52 -0
  4. package/src/__tests__/approval.test.ts +456 -0
  5. package/src/__tests__/commands.test.ts +487 -0
  6. package/src/__tests__/config.test.ts +229 -0
  7. package/src/__tests__/e2e-modes.test.ts +253 -0
  8. package/src/__tests__/extension.test.ts +170 -0
  9. package/src/__tests__/index-integration.test.ts +282 -0
  10. package/src/__tests__/model-picker.test.ts +371 -0
  11. package/src/__tests__/pipeline.test.ts +976 -0
  12. package/src/__tests__/production.test.ts +102 -0
  13. package/src/__tests__/rule-editor-component.test.ts +475 -0
  14. package/src/__tests__/rule-editor.test.ts +252 -0
  15. package/src/__tests__/rule-templates.test.ts +264 -0
  16. package/src/__tests__/statusline.test.ts +221 -0
  17. package/src/__tests__/types.test.ts +59 -0
  18. package/src/approval.ts +352 -0
  19. package/src/ast/__tests__/analyzer.test.ts +262 -0
  20. package/src/ast/__tests__/loader.test.ts +136 -0
  21. package/src/ast/analyzer.ts +326 -0
  22. package/src/ast/index.ts +7 -0
  23. package/src/ast/loader.ts +113 -0
  24. package/src/classifier/__tests__/classifier.test.ts +285 -0
  25. package/src/classifier/__tests__/json-parser.test.ts +118 -0
  26. package/src/classifier/__tests__/model-resolver.test.ts +173 -0
  27. package/src/classifier/__tests__/prompt.test.ts +59 -0
  28. package/src/classifier/classifier.ts +242 -0
  29. package/src/classifier/index.ts +28 -0
  30. package/src/classifier/json-parser.ts +127 -0
  31. package/src/classifier/model-resolver.ts +295 -0
  32. package/src/classifier/prompt.ts +52 -0
  33. package/src/commands.ts +234 -0
  34. package/src/config.ts +216 -0
  35. package/src/index.ts +291 -0
  36. package/src/model-picker.ts +398 -0
  37. package/src/pipeline.ts +541 -0
  38. package/src/production.ts +102 -0
  39. package/src/rule-editor-component.ts +883 -0
  40. package/src/rule-editor.ts +262 -0
  41. package/src/rule-templates.ts +332 -0
  42. package/src/rules/__tests__/builtins.test.ts +614 -0
  43. package/src/rules/__tests__/index.test.ts +26 -0
  44. package/src/rules/__tests__/matcher.test.ts +378 -0
  45. package/src/rules/__tests__/wildcard.test.ts +84 -0
  46. package/src/rules/builtins.ts +680 -0
  47. package/src/rules/index.ts +27 -0
  48. package/src/rules/matcher.ts +160 -0
  49. package/src/rules/wildcard.ts +43 -0
  50. package/src/statusline.ts +226 -0
  51. package/src/types.ts +249 -0
package/README.md ADDED
@@ -0,0 +1,455 @@
1
+ # @zhushanwen/pi-permission
2
+
3
+ Pi permission 扩展 — 四档权限模式(yolo / auto / approve / strict)+ 三层安全管道(AST 结构分析 + 规则匹配 + AI Classifier),为 bash 工具调用提供可配置的安全门。
4
+
5
+ ## 功能
6
+
7
+ - **四档权限模式**:从「完全放行」到「全部审批」的渐进式安全策略
8
+ - **三层管道**(auto 模式):AST 结构分析 → 规则匹配 → AI 风险分类 + 用户审批竞速
9
+ - **内置危险规则**:12 条 builtin-danger 规则(rm -rf、curl|sh、chmod 777 等高危模式)
10
+ - **白名单快速放行**:50+9 条安全命令白名单(24 Codex 原始 + 26 扩充 + 9 条件安全)
11
+ - **用户自定义规则**:OpenCode wildcard 语法,last-match-wins 语义
12
+ - **AI Classifier**:auto 模式下用 LLM 评估未知命令风险(low/medium/high)
13
+ - **用户审批 UI**:TUI(自定义 Component)/ RPC(select 对话框)/ headless(fail-closed deny)
14
+ - **Reject-with-Reason**:用户拒绝时可输入真实理由(回传 agent 辅助理解)
15
+ - **statusline 集成**:TUI 底部显示当前权限模式标签
16
+ - **fail-closed**:任何异常路径 → block(绝不静默放行)
17
+
18
+ ## 安装
19
+
20
+ ```bash
21
+ # npm 方式(唯一正式方式)
22
+ pi install npm:@zhushanwen/pi-permission
23
+
24
+ # 本地开发(symlink)
25
+ ln -s /path/to/xyz-pi-extensions-workspace/feat-permission-and-auto-mode/extensions/permission \
26
+ ~/.pi/agent/extensions/permission
27
+ ```
28
+
29
+ ## 配置
30
+
31
+ 配置文件位置:`~/.pi/agent/permission-config.json`(首次运行自动创建默认配置)。
32
+
33
+ 可通过 `PI_CODING_AGENT_DIR` 环境变量覆盖基础路径。
34
+
35
+ ### 配置结构
36
+
37
+ ```json
38
+ {
39
+ "mode": "yolo",
40
+ "enabled": true,
41
+ "classifier": {
42
+ "enabled": true,
43
+ "model": "auto",
44
+ "timeout": 90,
45
+ "autoApproveLowRisk": true,
46
+ "autoDenyHighRisk": true
47
+ },
48
+ "userRules": []
49
+ }
50
+ ```
51
+
52
+ | 字段 | 默认值 | 说明 |
53
+ |------|--------|------|
54
+ | `mode` | `"yolo"` | 当前权限模式(yolo/auto/approve/strict) |
55
+ | `enabled` | `true` | 扩展是否启用(false=完全放行,等同 yolo 但保留配置) |
56
+ | `classifier.enabled` | `true` | 是否启用 AI 层(auto 模式自动 true) |
57
+ | `classifier.model` | `"auto"` | AI 模型(`auto` 选最便宜,或 `provider/model-id`) |
58
+ | `classifier.timeout` | `90` | AI 分类超时秒数 |
59
+ | `classifier.autoApproveLowRisk` | `true` | 低风险是否自动放行(false=转人工) |
60
+ | `classifier.autoDenyHighRisk` | `true` | 高风险是否自动拦截(true=强制 deny) |
61
+ | `userRules` | `[]` | 用户自定义规则数组 |
62
+
63
+ ## 四档模式
64
+
65
+ 按严格等级从低到高:
66
+
67
+ ### YOLO(默认)
68
+
69
+ 完全无防护,所有工具调用放行。不跑任何层。适合受信任的隔离环境(如 dev container)。
70
+
71
+ ### Auto
72
+
73
+ 安全命令规则直接放行 + 非安全命令过 AI 审查 + AI 认为安全放行 / AI 认为非安全人工审批。
74
+
75
+ 三层管道:
76
+ 1. **AST 结构分析**(层 1):tree-sitter-bash 解析命令,检测危险结构(subshell、command_substitution、file_redirect 等)
77
+ 2. **规则匹配**(层 2):白名单 + builtin-danger + user rules,last-match-wins
78
+ 3. **AI Classifier + 用户审批竞速**(层 3):AI 评估风险等级,同时弹出用户审批;AI 先返回时按 outcome 分支(allow/deny/ask)
79
+
80
+ ### Approve
81
+
82
+ 自动模式去除 AI,规则匹配后安全放行、非安全直接人工审批。无 AI 调用,适合无网络/无 API key 环境。
83
+
84
+ ### Strict
85
+
86
+ 全部审批。不跑 AST/规则/AI,所有工具调用都弹出用户审批。
87
+
88
+ ## 切换模式
89
+
90
+ ```
91
+ /permission 显示当前模式和可用模式列表
92
+ /permission yolo 切换到 yolo 模式
93
+ /permission auto 切换到 auto 模式
94
+ /permission approve 切换到 approve 模式
95
+ /permission strict 切换到 strict 模式
96
+ /permission status 显示详细配置
97
+ /permission model overlay 选择 AI classifier 模型(W7)
98
+ ```
99
+
100
+ ## 内置规则(摘要)
101
+
102
+ 内置规则分两类,均代码硬编码,用户不可改:
103
+
104
+ - **白名单(builtin-safe)**:50 条无条件安全命令(24 Codex 移植 + 26 本扩展扩充:`cat`/`cd`/`echo`/`ls`/`grep`/`pwd`/`diff`/`jq`/`du`/`file`/`ps` 等)+ 9 条带 flag 子检查的条件安全命令(`base64`/`find`/`rg`/`git`/`sed`/`sort`/`iconv`/`shuf`/`date`)。规则遍历无 deny/allow 命中时(ask),白名单兜底 allow(不跑 AI)。
105
+ - **危险规则(builtin-danger)**:12 条正则规则(`rm -rf`、`sudo`、`chmod 777`、`curl ... | sh`、`git push --force`、`git reset --hard` 等)。pattern 是 RegExp 源字符串(含 `\b`/`\s`),用 `new RegExp(pattern, 'i')` 编译,`action` 固定 `deny`。
106
+
107
+ 完整清单与每条规则的 pattern/示例见下方「规则系统」第 2、3 节。
108
+
109
+ ## 规则系统
110
+
111
+ 本节是用户编辑规则的完整指南,覆盖三层规则来源、内置白名单/危险规则全量清单、自定义规则语法、匹配优先级与常见场景示例。
112
+
113
+ ### 1. 规则系统总览
114
+
115
+ 规则(Rule)是层 2「规则匹配」的核心数据单元。每条规则描述「某条命令匹配某模式时执行某动作」。规则来源分三层:
116
+
117
+ | 来源 | source 字段 | 数量 | 形态 | 可改 |
118
+ |------|-------------|------|------|------|
119
+ | 内置安全白名单 | `builtin-safe` | 50 无条件 + 9 条件 | 函数实现(`isKnownSafeCommand`),不进 `Rule[]` 数组 | 不可改 |
120
+ | 内置危险规则 | `builtin-danger` | 12 条正则 | `BUILTIN_DANGER_RULES` 常量,代码硬编码 | 不可改 |
121
+ | 用户自定义规则 | `user` | 任意 | `permission-config.json` 的 `userRules` 数组 | 可改 |
122
+
123
+ 规则层在整个权限管道中的位置(auto / approve 模式):
124
+
125
+ ```
126
+ W2 AST 结构分析 → W3 规则匹配 → W4 AI Classifier(仅 auto)
127
+ ```
128
+
129
+ W3 内部评估顺序:
130
+
131
+ 1. **`[...BUILTIN_DANGER_RULES, ...userRules]`**:按数组顺序遍历,last-match-wins。deny → 直接 deny;allow → 直接 allow
132
+ 2. **无规则匹配(ask)→ 白名单兜底**:`isKnownSafeCommand(argv)` 命中 → `allow`(虚拟 `builtin-safe` rule)
133
+ 3. **仍无命中**:返回 `ask`(交下游 W4 AI 或人工审批,不静默 deny)
134
+
135
+ 注意:只有 bash 工具的命令字符串会走规则匹配;非 bash 工具(Read/Write/Edit 等)在 `pipeline.ts` 的 `matchNonBashTool` 中单独评估用户规则(详见第 5 节「tool 字段」)。
136
+
137
+ ### 2. 内置安全白名单(builtin-safe,不可改)
138
+
139
+ 由 `BUILTIN_UNCONDITIONAL_SAFE` + `CONDITIONAL_SAFE_COMMANDS` 实现(函数判定,不是 `Rule[]`),白名单仅在规则遍历无 deny/allow 命中时(ask)作为 allow 兜底,不短路规则遍历。用户 deny 规则可覆盖白名单。
140
+
141
+ **50 条无条件安全命令**(`BUILTIN_UNCONDITIONAL_SAFE`,仅看 argv[0] basename):
142
+
143
+ ```
144
+ arch / basename / cat / cd / cksum / cmp / column / comm / cut /
145
+ diff / dirname / du / df / echo / expand / expr / false / file /
146
+ fold / grep / groups / head / id / jq / ls / md5sum / nl / paste /
147
+ printenv / ps / pwd / readlink / realpath / rev / seq / sha256sum /
148
+ shasum / stat / tail / tr / true / tsort / uniq / uname / uptime /
149
+ wc / whereis / who / whoami / which
150
+ ```
151
+
152
+ 注:前 24 条(cat/cd/cut/echo/expr/false/grep/head/id/ls/nl/paste/pwd/rev/seq/stat/tail/tr/true/uname/uniq/wc/which/whoami)移植自 Codex safelist。后 26 条(arch/basename/cksum/cmp/column/comm/diff/dirname/du/df/expand/file/fold/groups/jq/md5sum/printenv/ps/readlink/realpath/sha256sum/shasum/tsort/uptime/whereis/who)是本扩展在验证无写入 flag 后扩充。Codex 源码里的 `numfmt`/`tac` 仅在 linux 安全,本扩展面向跨平台 agent,统一不加入。
153
+
154
+ **9 条带 flag 子检查的条件安全命令**(`CONDITIONAL_SAFE_COMMANDS`,argv 级判定):
155
+
156
+ | 命令 | 安全条件 | 命中危险即不放行 |
157
+ |------|----------|------------------|
158
+ | `base64` | 不含写文件 flag | 禁 `-o` / `--output` / `--output=*` / `-o*`(合并 flag 如 `-ob64.txt`) |
159
+ | `find` | 不含执行/删除/写文件 flag | 禁 `-exec` / `-execdir` / `-ok` / `-okdir` / `-delete` / `-fls` / `-fprint` / `-fprint0` / `-fprintf` |
160
+ | `rg` | 不含执行外部工具 flag | 禁 `--pre` / `--pre=*` / `--hostname-bin` / `--hostname-bin=*` / `--search-zip` / `-z` |
161
+ | `git` | 子命令属于 `status`/`log`/`diff`/`show`/`branch` 且只读 | 见下文 git 子表 |
162
+ | `sed` | 仅 `sed -n {N\|M,N}p [file]`(argv 长度 ≤ 4) | 其余形式不放行 |
163
+ | `sort` | 不含写文件 flag | 禁 `-o` / `--output` / `--output=*` / `-o*`(合并 flag) |
164
+ | `iconv` | 不含写文件 flag | 禁 `-o` / `--output` / `--output=*` |
165
+ | `shuf` | 不含写文件 flag | 禁 `-o` / `--output` / `--output=*` |
166
+ | `date` | 不含设置时间 flag | 禁 `-s` / `--set` / `--set=*`(`-s` 设置系统时间需 root) |
167
+
168
+ **git 子命令安全判定细则**:
169
+
170
+ - 子命令白名单:`status` / `log` / `diff` / `show` / `branch`
171
+ - 全局选项禁用:`-C` / `-c` / `-p` / `--config-env` / `--config-env=*` / `--exec-path` / `--exec-path=*` / `--git-dir` / `--git-dir=*` / `--namespace` / `--namespace=*` / `--paginate` / `--super-prefix` / `--super-prefix=*` / `--work-tree` / `--work-tree=*`(短选项内联值如 `-C.` 也禁)
172
+ - 子命令选项禁用:`--output` / `--output=*` / `--ext-diff` / `--textconv` / `--exec` / `--exec=*`
173
+ - `git branch` 仅当全部参数是只读 flag(`--list`/`-l`/`--show-current`/`-a`/`--all`/`-r`/`--remotes`/`-v`/`-vv`/`--verbose`/`--format=*`)时安全,否则视为创建/重命名/删除分支
174
+
175
+ **sed 安全判定细则**:
176
+
177
+ 仅形如 `sed -n {N\|M,N}p [file]` 的命令安全,argv 长度上限 4。其中第三参数必须匹配 `/^(\d+,)?\d+p$/`(如 `5p`、`2,8p`)。其余 `sed` 用法(如 `s/.../.../`、`-i`)一律不放行。
178
+
179
+ ### 3. 内置危险规则(builtin-danger,不可改)
180
+
181
+ 12 条正则规则(`BUILTIN_DANGER_RULES`),`action` 固定 `deny`,`source` 固定 `builtin-danger`。pattern 是 RegExp 源字符串(含 `\b`/`\s`),由 `resolvePattern` 用 `new RegExp(pattern, 'i')` 编译(大小写不敏感)。
182
+
183
+ | id | pattern(正则字面量)| description | 匹配示例 |
184
+ |----|----------------------|-------------|----------|
185
+ | bd-001 | `\brm\s+(-[^\s]*r\|--recursive)` | recursive delete | `rm -rf /`、`rm -fr x`、`rm --recursive y` |
186
+ | bd-002 | `\bsudo\b` | sudo | `sudo`、`sudo -E apt update` |
187
+ | bd-003 | `\bchmod\b.*777` | world-writable permissions | `chmod 777 /tmp`、`chmod 0777 file` |
188
+ | bd-004 | `>\s*/dev/[sh]d[a-z]` | raw device redirect | `dd ... > /dev/sda` |
189
+ | bd-005 | `\bgit\s+push\s+.*(-f\b\|--force\b)` | force push | `git push --force`、`git push -f` |
190
+ | bd-006 | `\bgit\s+reset\s+--hard\b` | hard reset | `git reset --hard HEAD~1` |
191
+ | bd-007 | `\bgit\s+clean\s+-[^\s]*f` | git clean | `git clean -fd`、`git clean -dfx` |
192
+ | bd-008 | `\bgit\s+checkout\s+\.\s*($\|[;&\|])` | git checkout (discard all) | `git checkout .` |
193
+ | bd-009 | `\bgit\s+restore\b` | git restore | `git restore file.txt` |
194
+ | bd-010 | `\b(curl\|wget)\b.*\|\s*(ba)?sh\b` | pipe to shell | `curl http://x \| sh`、`wget x \| bash` |
195
+ | bd-011 | `\bgh\s+repo\s+(create\|delete\|rename\|archive)\b` | modify GitHub repo | `gh repo delete foo` |
196
+ | bd-012 | `\bgh\s+release\s+(create\|delete\|edit)\b` | modify GitHub release | `gh release create v1` |
197
+
198
+ ### 4. 自定义规则(用户编辑指南)
199
+
200
+ 在 `~/.pi/agent/permission-config.json` 的 `userRules` 数组中添加。完整示例:
201
+
202
+ ```json
203
+ {
204
+ "userRules": [
205
+ { "id": "user-001", "tool": "bash", "pattern": "npm *", "action": "allow", "source": "user", "description": "允许所有 npm 命令" },
206
+ { "id": "user-002", "tool": "bash", "pattern": "git push *", "action": "deny", "source": "user", "description": "禁止 git push" },
207
+ { "id": "user-003", "tool": "bash", "pattern": "docker *", "action": "ask", "source": "user", "description": "docker 转人工" },
208
+ { "id": "user-004", "tool": "*", "pattern": "*", "action": "ask", "source": "user", "description": "兜底:所有未匹配工具转人工" }
209
+ ]
210
+ }
211
+ ```
212
+
213
+ 加载时配置层(`normalizeRule`)会对每条规则归一化,缺失字段有兜底:
214
+
215
+ - `id` 缺失 → 自动分配 `user-<n>`(按数组下标 +1)
216
+ - `tool` 缺失 → `*`(匹配所有工具)
217
+ - `pattern` 缺失 → `*`(匹配所有命令)
218
+ - `action` 非 `allow`/`deny`/`ask` → 该规则被丢弃(不影响其他规则)
219
+ - `source` 非 `user` → 归一化为 `user`
220
+
221
+ ### 5. 规则字段详解
222
+
223
+ | 字段 | 类型 | 必填 | 说明 |
224
+ |------|------|------|------|
225
+ | `id` | string | 是 | 唯一 id,用户规则建议 `user-<n>`(缺失会自动分配) |
226
+ | `tool` | string | 是 | 工具名匹配,wildcard 语法。`bash` 精确匹配 bash 工具;`*` 匹配所有工具(含 Read/Write/Edit);也可写具体工具名如 `read`、`write` |
227
+ | `pattern` | string | 是 | 命令/目标匹配,wildcard 语法。bash 工具对 `argv.join(' ')` 匹配;非 bash 工具对 `toolName` 匹配 |
228
+ | `action` | `"allow"` \| `"deny"` \| `"ask"` | 是 | 决策动作,详见第 8 节 |
229
+ | `source` | `"user"` | 是 | 规则来源,用户规则固定 `user`(写成其他值也会被归一化回 `user`) |
230
+ | `description` | string | 否 | 人类可读描述,在 matchedRule 与拒绝理由中展示 |
231
+
232
+ **tool 字段语义补充**:
233
+
234
+ - bash 工具的命令匹配由 `matcher.ts` 的 `matchRulesForArgv` 负责,它**只遍历规则数组、不对 rule.tool 做过滤**(因为 bash 工具上下文已确定)。所以 bash 规则的 `tool` 字段写什么不影响匹配结果,惯例写 `"bash"`。
235
+ - 非 bash 工具(Read/Write/Edit 等)由 `pipeline.ts` 的 `matchNonBashTool` 负责,它会先用 `wildcardToRegExp(rule.tool).test(toolName)` 过滤,再判定 `pattern`。要让规则对非 bash 工具生效,`tool` 必须能匹配工具名(如 `read`、`write`,或 `*`)。
236
+
237
+ ### 6. wildcard 语法
238
+
239
+ `pattern` 与非 bash 的 `tool` 字段都用 OpenCode 风格 wildcard(`wildcardToRegExp` 编译):
240
+
241
+ - `*` 匹配任意字符(含空格),等价于 `.*`
242
+ - `?` 匹配单个字符,等价于 `.`
243
+ - 末尾 ` *` 改写为可选的 `( .*)?`,使 `ls *` 也能匹配无参的裸 `ls`
244
+ - 特殊字符 `. + ^ $ { } ( ) | [ ] \` 按字面量匹配(自动转义)
245
+ - 路径分隔符 `\` 统一归一化为 `/`
246
+ - 全锚定 `^...$`
247
+ - 非 Windows:大小写敏感,用 `s`(dotAll)flag;Windows:大小写不敏感,用 `si` flag
248
+
249
+ 常见 pattern 示例:
250
+
251
+ | pattern | 匹配 | 不匹配 |
252
+ |---------|------|--------|
253
+ | `npm *` | `npm install`、`npm`、`npm run build` | `pnpm install` |
254
+ | `git commit -m *` | `git commit -m "msg"` | `git commit` |
255
+ | `rm -rf *` | `rm -rf /tmp/x` | `rm -r /tmp` |
256
+ | `docker *` | `docker ps`、`docker` | `docker-compose up`(中间有 `-` 仍命中 `docker *`) |
257
+
258
+ ### 7. 匹配优先级(last-match-wins)
259
+
260
+ 规则遍历遵循 **last-match-wins**(最后一条匹配的规则胜出)。拼接顺序由 pipeline 固定:
261
+
262
+ ```
263
+ [...BUILTIN_DANGER_RULES, ...userRules]
264
+ ```
265
+
266
+ 即内置危险规则在前,用户规则在后。配合 last-match-wins:
267
+
268
+ - 多条规则都匹配时,**数组末尾的那条**胜出
269
+ - 用户规则在 builtin-danger 之后,因此**用户规则可以覆盖内置危险规则**
270
+ - 在 `userRules` 数组内部,**靠后的规则覆盖靠前的**
271
+ - 无任何匹配 → `ask`(交下游,不静默 deny)
272
+
273
+ **覆盖示例**:命令 `git push --force origin main`
274
+
275
+ 1. 遍历到 bd-005(`git push ... --force`)→ 命中 deny,记为 winner
276
+ 2. 遍历到 `userRules` 末尾的 `{ pattern: "git push *", action: "allow" }` → 命中 allow,覆盖 winner
277
+ 3. 最终 action = `allow`(用户已显式放行,风险自负)
278
+
279
+ **叠加示例**:想让 `npm install` 放行但 `npm publish` 拦截,按顺序写两条(deny 在后才能胜出):
280
+
281
+ ```json
282
+ { "id": "user-allow-npm", "tool": "bash", "pattern": "npm *", "action": "allow", "source": "user" },
283
+ { "id": "user-deny-publish", "tool": "bash", "pattern": "npm publish *", "action": "deny", "source": "user" }
284
+ ```
285
+
286
+ ### 8. action 三态语义
287
+
288
+ | action | 行为 | 适用场景 |
289
+ |--------|------|----------|
290
+ | `allow` | 直接放行,跳过 AI 与人工审批 | 信任的命令(白名单/常驻开发命令) |
291
+ | `deny` | 直接拦截,返回 `{ block: true, reason }` | 禁止的命令(覆盖 builtin allow 或封禁危险操作) |
292
+ | `ask` | 转下游:auto 模式 → W4 AI Classifier;approve/strict 模式 → 人工审批 | 不确定是否安全的命令,交给 AI 或人判断 |
293
+
294
+ 注意 `deny` 与「无匹配」不同:无匹配返回 `ask`(让命令进入下游评估),`deny` 是显式拦截。要拦截必须显式写 `action: "deny"`。
295
+
296
+ ### 9. 常见场景示例
297
+
298
+ **场景 1:允许所有 npm 命令**
299
+
300
+ ```json
301
+ { "id": "user-npm", "tool": "bash", "pattern": "npm *", "action": "allow", "source": "user" }
302
+ ```
303
+
304
+ **场景 2:禁止 git push(含 --force)**
305
+
306
+ ```json
307
+ { "id": "user-no-push", "tool": "bash", "pattern": "git push *", "action": "deny", "source": "user" }
308
+ ```
309
+
310
+ **场景 3:docker 转人工审批**
311
+
312
+ ```json
313
+ { "id": "user-docker", "tool": "bash", "pattern": "docker *", "action": "ask", "source": "user" }
314
+ ```
315
+
316
+ **场景 4:允许 npm install 但禁止 npm publish**
317
+
318
+ ```json
319
+ { "id": "user-npm-allow", "tool": "bash", "pattern": "npm *", "action": "allow", "source": "user" },
320
+ { "id": "user-publish-deny", "tool": "bash", "pattern": "npm publish *", "action": "deny", "source": "user" }
321
+ ```
322
+
323
+ publish 规则在后,last-match-wins 时 deny 胜出。
324
+
325
+ **场景 5:全工具兜底审批(如所有 Write/Edit 都问一下)**
326
+
327
+ ```json
328
+ { "id": "user-all-ask", "tool": "*", "pattern": "*", "action": "ask", "source": "user" }
329
+ ```
330
+
331
+ 对非 bash 工具生效(`matchNonBashTool` 会用 `tool='*'` 匹配任意 toolName)。建议放在 `userRules` 末尾作为兜底。
332
+
333
+ **场景 6:覆盖内置危险规则(谨慎)**
334
+
335
+ 例如内置 bd-001 禁止 `rm -rf`,但容器内想放行清理 `/tmp`:
336
+
337
+ ```json
338
+ { "id": "user-rm-tmp", "tool": "bash", "pattern": "rm -rf /tmp/*", "action": "allow", "source": "user" }
339
+ ```
340
+
341
+ 放在 `userRules` 末尾即覆盖 bd-001。务必缩窄 pattern 范围,避免误放行。
342
+
343
+ ### 10. 配置文件管理
344
+
345
+ - **路径**:`~/.pi/agent/permission-config.json`,可用 `PI_CODING_AGENT_DIR` 环境变量覆盖基础目录
346
+ - **首次创建**:扩展启动时若文件不存在,自动写入默认配置(`mode: "yolo"`、空 `userRules`)
347
+ - **权限**:`0o600`(原子写:先写 `.tmp` 再 rename,避免半写状态)
348
+ - **编辑方式**:目前需手动编辑 JSON 文件(未来计划提供 `/permission add-rule` 命令辅助编辑)
349
+ - **热重载**:每次 tool_call 都重读配置,用 `mtimeMs + size` 双 key 缓存检测变化(防 APFS 等 mtime 精度截断)。编辑保存后下一次命令即生效,无需重启
350
+
351
+ ### 11. 调试技巧
352
+
353
+ - `/permission status`:查看当前配置摘要(含 `userRules` 数量)
354
+ - 查看决策来源:`PermissionDecision.source`(`mode` / `ast` / `rule` / `ai` / `user`),区分是模式直接放行、AST 拦截、规则命中、AI 分类还是人工审批
355
+ - 查看命中规则:`PermissionDecision.matchedRule`(命中时携带 Rule 对象),从 `id` 可判断是 `builtin-safe`(白名单虚拟规则)、`bd-<n>`(内置危险)还是 `user-<n>`(用户规则)
356
+ - 故意写一条 `deny` + 带 `description` 的用户规则触发拦截,从 tool_result 的 block reason 文案反查命中的是哪条规则
357
+ - 规则 pattern 编译失败会被静默跳过(regex 构造异常不阻塞管道),怀疑某条规则没生效时先用 `/^(\d+,)?\d+p$/` 这类标准语法自测
358
+
359
+ ## AI Classifier
360
+
361
+ auto 模式下层 3 用 LLM 评估未知命令风险:
362
+
363
+ - **模型**:`classifier.model`(`auto` 自动选最便宜,或指定 `provider/model-id`)
364
+ - **输出**:`risk_level`(low/medium/high)+ `outcome`(allow/deny/ask)+ `reasoning` + `confidence`
365
+ - **override**(WT7 偏差补丁):
366
+ - `low + allow + autoApproveLowRisk=false` → 强制 `ask`(转人工)
367
+ - `high + allow + autoDenyHighRisk=true` → 强制 `deny`(即使 AI 说放行)
368
+ - **Racing**:AI 分类与用户审批并行;AI 先返回时按 outcome 分支(allow/deny 关闭对话框,ask 等用户)
369
+
370
+ ### 切换 classifier 模型(/permission model)
371
+
372
+ `/permission model` 弹出 overlay 选择 AI classifier 使用的模型,写回 `classifier.model`:
373
+
374
+ - **第一级 provider 选择**:列出 `Auto`(自动选最便宜可用模型)+ 所有可用 provider(来自 `~/.pi/agent/models.json`,按字母序)。当前 `classifier.model` 预选高亮。
375
+ - **第二级 model 选择**:选中具体 provider 后,列出该 provider 下所有可用 model(按 `cost.input` 升序,并列按 id 字母序)。`Esc` 回退到 provider 列表。
376
+ - **键位**:`↑/↓` 导航、`Enter` 确认、`Esc` 取消(provider stage)或回退(model stage)。
377
+ - **三模式分发**:
378
+ - **TUI**:`ctx.ui.custom` 渲染 overlay(`ProviderModelSelectorComponent`,两级 `SelectList` 状态机)。
379
+ - **RPC**:两次 `ctx.ui.select`(先 provider 含 `Auto`,再 model)。
380
+ - **headless**(json/print):无交互 UI,返回降级提示。
381
+ - **无可用模型**:`models.json` 不存在 / 无 provider 配 `apiKey` / 解析失败时,`listAvailableModels` 返回空 Map,命令降级为提示 `No available models. Configure ~/.pi/agent/models.json first.`(不阻塞,不修改配置)。
382
+ - **结果写回**:选中后 `classifier.model` 更新为 `auto` 或 `provider/model-id`,其余字段(mode/enabled/timeout/userRules)保留。
383
+
384
+ ## statusline 集成
385
+
386
+ session_start 时注册权限 footer,在 TUI 底部显示当前模式标签:
387
+
388
+ ```
389
+ [pi-permission] Auto · enabled AST + rules + AI classifier
390
+ ```
391
+
392
+ 模式标签:YOLO / Auto / Approve / Strict(对应 yolo/auto/approve/strict)。
393
+
394
+ ## 已知限制
395
+
396
+ - **Footer 单例覆盖**:Pi 只有一个 footer 槽位。本扩展的 footer 会覆盖(或被覆盖)其他扩展的 footer,最典型的冲突是 **`@zhushanwen/pi-statusline`**(两者都注册 footer)。若同时安装,后注册者覆盖前者。要同时显示两者,需自行 fork 合并 footer 渲染逻辑。可用 `grep -r "setFooter" ~/.pi/agent/extensions/` 排查冲突。
397
+ - **TUI Reject-with-Reason**:当前 RPC 分支已完整接入 `ctx.ui.input` 采集拒绝理由;TUI 分支因 pi-tui Input 组件集成成本较高,暂保留简化 deny(固定文案),后续迭代补齐内联文本输入。
398
+ - **headless 模式**:json/print 模式无交互 UI,所有审批请求 fail-closed deny(不阻塞自动化流程,但 strict/approve 模式下无法放行)。
399
+ - **wasm 加载**:AST 分析依赖 tree-sitter-bash wasm,加载失败时 fail-closed(clean=false, parseError=true)。
400
+ - **并发**:tool_call handler 用 approvalChain 串行化(Pi 不保证 handler 串行,但权限检查涉及共享 UI 对话框)。
401
+
402
+ ## 架构
403
+
404
+ ```
405
+ tool_call event
406
+ │
407
+ ▼
408
+ yolo / disabled? ──yes──→ allow(快速路径)
409
+ │ no
410
+ ▼
411
+ strict? ──yes──→ 用户审批
412
+ │ no
413
+ ▼
414
+ bash? ──yes──→ AST 结构分析
415
+ │ │
416
+ │ ├─ clean=false →(auto: 层3 / approve: 审批)
417
+ │ └─ clean=true → argv 提取
418
+ │
419
+ ▼
420
+ 规则匹配(层 2)
421
+ │
422
+ ├─ allow → 放行
423
+ ├─ deny → 拒绝
424
+ └─ ask →(auto: 层3 Racing / approve: 审批)
425
+
426
+ 层 3 Racing(auto 模式):
427
+ AI Classifier ──┐
428
+ ├─ race → AI 赢按 outcome / 用户赢按 decision
429
+ 用户审批 UI ──┘
430
+ ```
431
+
432
+ ## 设计原则
433
+
434
+ - **fail-closed**:任何异常 → block(绝不静默放行)
435
+ - **checkPermission 永不 throw**:caller(tool_call handler)依赖此契约
436
+ - **deps 注入**:所有外部依赖(AST/规则/AI/UI)通过 CheckPermissionDeps 注入,便于测试 mock
437
+ - **session 隔离**:config 在 session_start 重建的闭包,每 session 独立
438
+ - **纯函数核心**:checkPermission / runLayer2 / applyAutoApproveOverrides 都是纯函数
439
+
440
+ ## 开发
441
+
442
+ ```bash
443
+ # 类型检查
444
+ pnpm --filter @zhushanwen/pi-permission typecheck
445
+
446
+ # 测试
447
+ pnpm --filter @zhushanwen/pi-permission test
448
+
449
+ # 测试监听模式
450
+ pnpm --filter @zhushanwen/pi-permission test:watch
451
+ ```
452
+
453
+ ## License
454
+
455
+ MIT
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Re-export from src — pi loads this file as the extension entry point
2
+ export { default } from "./src/index.ts";
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@zhushanwen/pi-permission",
3
+ "version": "0.0.1",
4
+ "description": "Pi permission extension — four permission modes (yolo/auto/approve/strict) with three-layer pipeline (AST + rules + AI classifier).",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "pi": {
8
+ "extensions": [
9
+ "./index.ts"
10
+ ]
11
+ },
12
+ "keywords": [
13
+ "pi-package",
14
+ "permission",
15
+ "security",
16
+ "approval",
17
+ "bash",
18
+ "sandbox"
19
+ ],
20
+ "license": "MIT",
21
+ "files": [
22
+ "src/",
23
+ "index.ts"
24
+ ],
25
+ "dependencies": {
26
+ "tree-sitter-bash": "^0.25.1",
27
+ "web-tree-sitter": "^0.26.11"
28
+ },
29
+ "peerDependencies": {
30
+ "@earendil-works/pi-ai": "*",
31
+ "@earendil-works/pi-coding-agent": "*",
32
+ "@earendil-works/pi-tui": "*"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "@earendil-works/pi-ai": {
36
+ "optional": true
37
+ },
38
+ "@earendil-works/pi-tui": {
39
+ "optional": true
40
+ }
41
+ },
42
+ "devDependencies": {
43
+ "@earendil-works/pi-ai": "*",
44
+ "@earendil-works/pi-tui": "*",
45
+ "vitest": "^4.1.8"
46
+ },
47
+ "scripts": {
48
+ "typecheck": "npx tsc --noEmit",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest"
51
+ }
52
+ }