@trim21/personal-pi-extensions 0.0.422 → 0.0.423

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.422",
3
+ "version": "0.0.423",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -1,13 +1,13 @@
1
1
  /**
2
- * approval-rules —— bash 命令审核组件(对齐 opencode 的权限方法):
3
- * 用 tree-sitter 解析命令(含嵌套 `$(...)`),按 BashArity 生成命令模式
4
- * (`git checkout main` → `git checkout *`),再用通配匹配对 allow/deny
5
- * 规则求值。接入 bwrap 的 `dangerouslyDisableSandbox` 审批:命中规则自动
2
+ * approval-rules —— bash 命令匹配引擎(对齐 opencode 的权限方法):
3
+ * 用 tree-sitter 解析命令(含嵌套 `$(...)`),对每条命令的原文做通配
4
+ * 匹配,对 allow/deny 规则求值。规则匹配不经过任何命令归一——BashArity
5
+ * 归一模式只用于 "allow forever" 的建议规则(见 approval-suggest.ts)。
6
+ * 接入 bwrap 的 `dangerouslyDisableSandbox` 审批:命中规则自动
6
7
  * 放行/拒绝,未命中才弹审批对话框。文件输出重定向(`>` / `>>` / `&>`
7
8
  * 等)不会因命令规则自动放行,避免 `echo *` 把 `echo '' > file` 带过。
8
9
  *
9
10
  * 参考实现:
10
- * - opencode packages/opencode/src/permission/arity.ts(BashArity 表)
11
11
  * - opencode packages/core/src/util/wildcard.ts(通配匹配)
12
12
  * - opencode packages/opencode/src/tool/shell.ts(tree-sitter 命令提取)
13
13
  */
@@ -173,34 +173,6 @@ export async function parseBashCommands(command: string): Promise<ParsedBash> {
173
173
  }
174
174
  }
175
175
 
176
- // ── BashArity:命令前缀 → token 数(参考 opencode arity.ts)─────────────────
177
-
178
- /**
179
- * 命令前缀 → 定义该命令的 token 数。`git checkout main` → `git` 的 arity 2,
180
- * 权限模式取前 2 个 token + `*`(`git checkout *`),避免具体参数进规则。
181
- * 表来自 opencode packages/opencode/src/permission/arity.ts(Apache-2.0),
182
- * 数据存放在 arity.json(所有 key 带引号)。
183
- */
184
- const ARITY = JSON.parse(readFileSync(new URL("arity.json", import.meta.url), "utf8")) as Record<
185
- string,
186
- number
187
- >;
188
-
189
- /**
190
- * 生成命令的权限模式:BashArity 前缀 + `*`。
191
- * `git checkout main` → `git checkout *`;未收录的命令 → 命令名 + `*`。
192
- */
193
- export function commandPattern(command: BashCommand): string {
194
- const tokens = [command.name, ...command.args];
195
- for (let len = tokens.length; len > 0; len--) {
196
- const prefix = tokens.slice(0, len).join(" ");
197
- const arity = ARITY[prefix];
198
- if (arity !== undefined) return [...tokens.slice(0, arity), "*"].join(" ");
199
- }
200
- if (tokens.length === 0) return "*";
201
- return [tokens[0], "*"].join(" ");
202
- }
203
-
204
176
  // ── 通配匹配(参考 opencode wildcard.ts)────────────────────────────────────
205
177
 
206
178
  /**
@@ -218,31 +190,15 @@ export function matchRule(input: string, pattern: string): boolean {
218
190
 
219
191
  // ── 规则求值 ────────────────────────────────────────────────────────────────
220
192
 
221
- /**
222
- * 命令(含所有嵌套命令)的权限模式列表(命令替换里的命令也展开)。
223
- * 供规则求值与"allow forever"写规则复用。
224
- */
225
- function patternsFromCommands(commands: BashCommand[]): string[] {
226
- const flat: string[] = [];
227
- const visit = (cmd: BashCommand) => {
228
- flat.push(commandPattern(cmd));
229
- for (const nested of cmd.nested) visit(nested);
230
- };
231
- for (const cmd of commands) visit(cmd);
232
- return flat;
233
- }
234
-
235
- export async function commandPatternsFor(command: string): Promise<string[]> {
236
- const parsed = await parseBashCommands(command);
237
- return patternsFromCommands(parsed.commands);
238
- }
239
-
240
193
  /**
241
194
  * 对命令(含所有嵌套命令)求值:
242
195
  * - deny 优先:任一命令命中 deny 规则即整体拒绝
243
196
  * - 文件输出重定向不自动放行:即使命令规则全匹配,也返回 undefined 交人审
244
197
  * - allow 需全量:所有命令都命中 allow 规则才整体放行,否则返回
245
198
  * undefined(有命令未命中规则,交给人审),避免未允许的命令被同链放行带过。
199
+ * - 匹配输入是命令原文(tree-sitter command 节点 text,含嵌套逐条展开),
200
+ * 对齐 opencode shell.ts 的 patterns;通配规则按字面写,`--` 与普通
201
+ * token 无区别。
246
202
  * 规则内后写优先(findLast,对齐 opencode PermissionV2)。
247
203
  */
248
204
  export async function evaluateBashApproval(
@@ -250,14 +206,19 @@ export async function evaluateBashApproval(
250
206
  rules: readonly ApprovalRule[],
251
207
  ): Promise<ApprovalAction | undefined> {
252
208
  const parsed = await parseBashCommands(command);
253
- const patterns = patternsFromCommands(parsed.commands);
254
- if (patterns.length === 0) return;
209
+ const raws: string[] = [];
210
+ const visit = (cmd: BashCommand) => {
211
+ raws.push(cmd.raw);
212
+ for (const nested of cmd.nested) visit(nested);
213
+ };
214
+ for (const cmd of parsed.commands) visit(cmd);
215
+ if (raws.length === 0) return;
255
216
  let allowed = 0;
256
- for (const pattern of patterns) {
257
- const rule = rules.findLast((r) => matchRule(pattern, r.pattern));
217
+ for (const raw of raws) {
218
+ const rule = rules.findLast((r) => matchRule(raw, r.pattern));
258
219
  if (rule?.action === "deny") return "deny";
259
220
  if (rule?.action === "allow") allowed++;
260
221
  }
261
222
  if (parsed.hasFileOutputRedirect) return;
262
- return allowed === patterns.length ? "allow" : undefined;
223
+ return allowed === raws.length ? "allow" : undefined;
263
224
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * approval-suggest —— "allow forever" 的建议规则生成:
3
+ * 用 BashArity 表把命令归一成模式(`git checkout main` → `git checkout *`),
4
+ * 供用户选择"永久允许"时写入配置。归一模式不参与规则判定——匹配引擎
5
+ * (approval-rules)按命令原文做通配匹配。
6
+ *
7
+ * 参考实现:opencode packages/opencode/src/permission/arity.ts(Apache-2.0)
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+
11
+ import { type BashCommand, parseBashCommands } from "./approval-rules.js";
12
+
13
+ /**
14
+ * 命令前缀 → 定义该命令的 token 数。`git checkout main` → `git` 的 arity 2,
15
+ * 权限模式取前 2 个 token + `*`(`git checkout *`),避免具体参数进规则。
16
+ * 表来自 opencode packages/opencode/src/permission/arity.ts(Apache-2.0),
17
+ * 数据存放在 arity.json(所有 key 带引号)。
18
+ */
19
+ const ARITY = JSON.parse(readFileSync(new URL("arity.json", import.meta.url), "utf8")) as Record<
20
+ string,
21
+ number
22
+ >;
23
+
24
+ /**
25
+ * 生成命令的 BashArity 建议模式:BashArity 前缀 + `*`。
26
+ * `git checkout main` → `git checkout *`;未收录的命令 → 命令名 + `*`。
27
+ */
28
+ export function commandPattern(command: BashCommand): string {
29
+ const tokens = [command.name, ...command.args];
30
+ for (let len = tokens.length; len > 0; len--) {
31
+ const prefix = tokens.slice(0, len).join(" ");
32
+ const arity = ARITY[prefix];
33
+ if (arity !== undefined) return [...tokens.slice(0, arity), "*"].join(" ");
34
+ }
35
+ if (tokens.length === 0) return "*";
36
+ return [tokens[0], "*"].join(" ");
37
+ }
38
+
39
+ /** 命令(含所有嵌套命令)的 BashArity 建议模式列表(命令替换里的命令也展开)。 */
40
+ function patternsFromCommands(commands: BashCommand[]): string[] {
41
+ const flat: string[] = [];
42
+ const visit = (cmd: BashCommand) => {
43
+ flat.push(commandPattern(cmd));
44
+ for (const nested of cmd.nested) visit(nested);
45
+ };
46
+ for (const cmd of commands) visit(cmd);
47
+ return flat;
48
+ }
49
+
50
+ export async function commandPatternsFor(command: string): Promise<string[]> {
51
+ const parsed = await parseBashCommands(command);
52
+ return patternsFromCommands(parsed.commands);
53
+ }
@@ -23,12 +23,8 @@ import {
23
23
  selectCheckboxActions,
24
24
  selectWithOptionalInput,
25
25
  } from "../lib/ui.js";
26
- import {
27
- type ApprovalRule,
28
- commandPatternsFor,
29
- evaluateBashApproval,
30
- matchRule,
31
- } from "./approval-rules.js";
26
+ import { type ApprovalRule, evaluateBashApproval, matchRule } from "./approval-rules.js";
27
+ import { commandPatternsFor } from "./approval-suggest.js";
32
28
  import {
33
29
  type BwrapMode,
34
30
  createBwrapBashOperations,