@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
@@ -0,0 +1,680 @@
1
+ /**
2
+ * 内置规则:安全白名单(50+9)+ 危险规则(permission-gate 12)。
3
+ *
4
+ * ── G3 修正 ──
5
+ * Codex 的 base64/find/rg/git/sed 有 argv 级 flag 子检查,单条 wildcard 无法表达
6
+ * 「base64 但不含 -o」。所以白名单用 `isKnownSafeCommand(argv): boolean` 函数实现
7
+ * (不进 Rule[] 数组),而 permission-gate 12 条危险规则作为 `Rule[]`
8
+ * (source='builtin-danger',pattern=正则字符串)。
9
+ *
10
+ * 移植忠实源码:
11
+ * - 安全白名单:codex-cli/codex-rs/shell-command/src/command_safety/is_safe_command.rs
12
+ * (is_safe_to_call_with_exec / is_safe_git_command / git_branch_is_read_only /
13
+ * is_valid_sed_n_arg / find_git_subcommand / executable_name_lookup_key)
14
+ * - git 全局选项:is_dangerous_command.rs(is_git_global_option_with_value /
15
+ * is_git_global_option_with_inline_value)
16
+ * - 危险规则:pi-agent-extensions/permission-gate/index.ts DEFAULT_RULES
17
+ *
18
+ * 与源码的差异(合理近似,已注释):
19
+ * - Rust `cfg(target_os = "linux")` 的 numfmt/tac:本扩展面向 Pi agent(跨平台),
20
+ * TS 无编译期 cfg,统一不加入白名单(与源码在非 linux 行为一致)。
21
+ * - Rust `bash -lc "<script>"` 复合命令解析(parse_shell_lc_plain_commands):
22
+ * 该职责属于 W2 AST 层(analyzeBashStructure 会把复合命令拆成 commands[][]),
23
+ * 本函数只做单条 argv 判定,不重复实现 bash 解析。
24
+ * - Windows powershell safelist:非本扩展目标平台,不移植。
25
+ * - argv[0]=='zsh' → 'bash' 的归一化:保留(与源码一致)。
26
+ */
27
+
28
+ import type { Rule } from "../types.js";
29
+
30
+ // ──────────────────────── Codex 安全白名单 ────────────────────────
31
+
32
+ /**
33
+ * Codex 无条件安全命令(50 个,is_safe_to_call_with_exec 的 match 分支)。
34
+ * 前 24 条移植自 Codex safelist,后 26 条是本扩展在验证无写入 flag 后扩充。
35
+ * numfmt/tac 仅 linux 安全,跨平台不加入。
36
+ */
37
+ export const BUILTIN_UNCONDITIONAL_SAFE: ReadonlySet<string> = new Set([
38
+ "arch",
39
+ "basename",
40
+ "cat",
41
+ "cd",
42
+ "cksum",
43
+ "cmp",
44
+ "column",
45
+ "comm",
46
+ "cut",
47
+ "diff",
48
+ "dirname",
49
+ "du",
50
+ "df",
51
+ "echo",
52
+ "expand",
53
+ "expr",
54
+ "false",
55
+ "file",
56
+ "fold",
57
+ "grep",
58
+ "groups",
59
+ "head",
60
+ "id",
61
+ "jq",
62
+ "ls",
63
+ "md5sum",
64
+ "nl",
65
+ "paste",
66
+ "printenv",
67
+ "ps",
68
+ "pwd",
69
+ "readlink",
70
+ "realpath",
71
+ "rev",
72
+ "seq",
73
+ "sha256sum",
74
+ "shasum",
75
+ "stat",
76
+ "tail",
77
+ "tr",
78
+ "true",
79
+ "tsort",
80
+ "uniq",
81
+ "uname",
82
+ "uptime",
83
+ "wc",
84
+ "whereis",
85
+ "who",
86
+ "whoami",
87
+ "which",
88
+ ]);
89
+
90
+ /** 9 个带 flag 子检查的条件安全命令。 */
91
+ const CONDITIONAL_SAFE_COMMANDS: ReadonlySet<string> = new Set([
92
+ "base64",
93
+ "find",
94
+ "rg",
95
+ "git",
96
+ "sed",
97
+ "sort",
98
+ "iconv",
99
+ "shuf",
100
+ "date",
101
+ ]);
102
+
103
+ // ── executable_name_lookup_key(取 basename,非 win32 不转小写/不去后缀) ──
104
+
105
+ /**
106
+ * 取 argv[0] 的 basename 作为 lookup key。
107
+ *
108
+ * 移植自 Rust executable_name_lookup_key(非 windows 分支):
109
+ * `Path::new(raw).file_name()`。TS 用 lastIndexOf('/') 近似(命令名不含反斜杠,
110
+ * 且 Windows 路径分隔符在本扩展不做特殊处理)。
111
+ */
112
+ function executableNameLookupKey(raw: string): string | undefined {
113
+ if (raw.length === 0) return undefined;
114
+ const slash = raw.lastIndexOf("/");
115
+ return slash >= 0 ? raw.slice(slash + 1) : raw;
116
+ }
117
+
118
+ // ── 合并短 flag 检查工具(G4: 合并 flag 如 -fo 需检测任意位置的 -o)──
119
+
120
+ /**
121
+ * 检查短 flag 簇中是否含某个字符(如 `-fo` 含 `o`)。
122
+ *
123
+ * 短 flag 簇:以单个 `-` 开头、非 `--`、长度 ≥ 2 的 token。
124
+ * `-o` / `-fo` / `-of` / `-fox` 都算短 flag 簇;`--output` / `--output=x` 不算。
125
+ *
126
+ * @param arg 待检查的 token
127
+ * @param flagChar 要检测的字符(如 "o" 表示 -o)
128
+ * @returns 短 flag 簇中任意位置出现该字符 → true
129
+ */
130
+ function shortFlagClusterHasChar(arg: string, flagChar: string): boolean {
131
+ // 只处理短 flag 簇:单个 - 开头,非 --,长度 ≥ 2
132
+ if (!arg.startsWith("-") || arg.startsWith("--") || arg.length < 2) return false;
133
+ // arg 去掉开头的 -,检查剩余字符
134
+ return arg.slice(1).includes(flagChar);
135
+ }
136
+
137
+ // ── base64 ──
138
+
139
+ const UNSAFE_BASE64_OPTIONS: ReadonlySet<string> = new Set(["-o", "--output"]);
140
+
141
+ function isSafeBase64(argv: string[]): boolean {
142
+ // 跳过 argv[0],任一参数命中危险 → 不安全
143
+ return !argv.slice(1).some((arg) => {
144
+ return (
145
+ UNSAFE_BASE64_OPTIONS.has(arg) ||
146
+ arg.startsWith("--output=") ||
147
+ // 合并短 flag 簇含 o(-o / -fo / -of / -fob64)
148
+ shortFlagClusterHasChar(arg, "o")
149
+ );
150
+ });
151
+ }
152
+
153
+ // ── find ──
154
+
155
+ const UNSAFE_FIND_OPTIONS: ReadonlySet<string> = new Set([
156
+ // 可执行任意命令
157
+ "-exec",
158
+ "-execdir",
159
+ "-ok",
160
+ "-okdir",
161
+ // 删除匹配文件
162
+ "-delete",
163
+ // 把路径写入文件
164
+ "-fls",
165
+ "-fprint",
166
+ "-fprint0",
167
+ "-fprintf",
168
+ ]);
169
+
170
+ function isSafeFind(argv: string[]): boolean {
171
+ return !argv.some((arg) => UNSAFE_FIND_OPTIONS.has(arg));
172
+ }
173
+
174
+ // ── rg (ripgrep) ──
175
+
176
+ const UNSAFE_RIPGREP_OPTIONS_WITH_ARGS: readonly string[] = [
177
+ // 对每个匹配执行任意命令
178
+ "--pre",
179
+ // 取本地 hostname 的命令
180
+ "--hostname-bin",
181
+ ];
182
+
183
+ const UNSAFE_RIPGREP_OPTIONS_WITHOUT_ARGS: ReadonlySet<string> = new Set([
184
+ // 调用外部解压工具,谨慎起见不放行
185
+ "--search-zip",
186
+ "-z",
187
+ ]);
188
+
189
+ function isSafeRipgrep(argv: string[]): boolean {
190
+ return !argv.some((arg) => {
191
+ if (UNSAFE_RIPGREP_OPTIONS_WITHOUT_ARGS.has(arg)) return true;
192
+ return UNSAFE_RIPGREP_OPTIONS_WITH_ARGS.some(
193
+ (opt) => arg === opt || arg.startsWith(opt + "="),
194
+ );
195
+ });
196
+ }
197
+
198
+ // ── git 全局选项(is_dangerous_command.rs)──
199
+
200
+ /** 取值的 global option(下一个参数是它的值,需 skip)。 */
201
+ const GIT_GLOBAL_OPTION_WITH_VALUE: ReadonlySet<string> = new Set([
202
+ "-C",
203
+ "-c",
204
+ "--config-env",
205
+ "--exec-path",
206
+ "--git-dir",
207
+ "--namespace",
208
+ "--super-prefix",
209
+ "--work-tree",
210
+ ]);
211
+
212
+ /** 短选项(如 `-C`/`-c`)的字符长度;更长者带内联值(`-C.`/`-ck=v`)。 */
213
+ const SHORT_FLAG_LENGTH = 2;
214
+
215
+ /** 内联取值的 global option(`--key=value` 形式,自身即完整,不 skip 下一个)。 */
216
+ function isGitGlobalOptionWithInlineValue(arg: string): boolean {
217
+ return (
218
+ arg.startsWith("--config-env=") ||
219
+ arg.startsWith("--exec-path=") ||
220
+ arg.startsWith("--git-dir=") ||
221
+ arg.startsWith("--namespace=") ||
222
+ arg.startsWith("--super-prefix=") ||
223
+ arg.startsWith("--work-tree=") ||
224
+ // `-C.` / `-ck=v`(短选项内联值,长度 > SHORT_FLAG_LENGTH)
225
+ ((arg.startsWith("-C") || arg.startsWith("-c")) &&
226
+ arg.length > SHORT_FLAG_LENGTH)
227
+ );
228
+ }
229
+
230
+ /**
231
+ * 在 argv 中查找第一个匹配的 git 子命令,跳过它前面的已知 global option。
232
+ *
233
+ * 移植自 Rust find_git_subcommand。返回 [index, subcommand] 或 undefined。
234
+ */
235
+ export function findGitSubcommand(
236
+ command: string[],
237
+ subcommands: readonly string[],
238
+ ): [number, string] | undefined {
239
+ const cmd0 = command[0];
240
+ if (cmd0 === undefined) return undefined;
241
+ if (executableNameLookupKey(cmd0) !== "git") return undefined;
242
+
243
+ let skipNext = false;
244
+ for (let idx = 1; idx < command.length; idx++) {
245
+ const arg = command[idx];
246
+ if (arg === undefined) continue;
247
+
248
+ if (skipNext) {
249
+ skipNext = false;
250
+ continue;
251
+ }
252
+
253
+ if (isGitGlobalOptionWithInlineValue(arg)) continue;
254
+
255
+ if (GIT_GLOBAL_OPTION_WITH_VALUE.has(arg)) {
256
+ skipNext = true;
257
+ continue;
258
+ }
259
+
260
+ if (arg === "--" || arg.startsWith("-")) continue;
261
+
262
+ if (subcommands.includes(arg)) return [idx, arg];
263
+
264
+ // 第一个非选项 token 就是子命令;若不在目标列表,停止扫描
265
+ // (避免把后续位置参数如分支名误判为子命令)
266
+ return undefined;
267
+ }
268
+
269
+ return undefined;
270
+ }
271
+
272
+ // ── git 子命令选项模式(is_safe_command.rs GitOptionPattern)──
273
+
274
+ type GitOptionPattern =
275
+ | { kind: "exact"; option: string }
276
+ | { kind: "shortWithInlineValue"; option: string }
277
+ | { kind: "prefix"; prefix: string };
278
+
279
+ function gitPatternMatches(pattern: GitOptionPattern, arg: string): boolean {
280
+ switch (pattern.kind) {
281
+ case "exact":
282
+ return arg === pattern.option;
283
+ case "shortWithInlineValue":
284
+ return arg.startsWith(pattern.option) && arg.length > pattern.option.length;
285
+ case "prefix":
286
+ return arg.startsWith(pattern.prefix);
287
+ }
288
+ }
289
+
290
+ const UNSAFE_GIT_GLOBAL_OPTIONS: readonly GitOptionPattern[] = [
291
+ { kind: "exact", option: "-C" },
292
+ { kind: "shortWithInlineValue", option: "-C" },
293
+ { kind: "exact", option: "-c" },
294
+ { kind: "shortWithInlineValue", option: "-c" },
295
+ { kind: "exact", option: "-p" },
296
+ { kind: "exact", option: "--config-env" },
297
+ { kind: "prefix", prefix: "--config-env=" },
298
+ { kind: "exact", option: "--exec-path" },
299
+ { kind: "prefix", prefix: "--exec-path=" },
300
+ { kind: "exact", option: "--git-dir" },
301
+ { kind: "prefix", prefix: "--git-dir=" },
302
+ { kind: "exact", option: "--namespace" },
303
+ { kind: "prefix", prefix: "--namespace=" },
304
+ { kind: "exact", option: "--paginate" },
305
+ { kind: "exact", option: "--super-prefix" },
306
+ { kind: "prefix", prefix: "--super-prefix=" },
307
+ { kind: "exact", option: "--work-tree" },
308
+ { kind: "prefix", prefix: "--work-tree=" },
309
+ ];
310
+
311
+ const UNSAFE_GIT_SUBCOMMAND_OPTIONS: readonly GitOptionPattern[] = [
312
+ { kind: "exact", option: "--output" },
313
+ { kind: "prefix", prefix: "--output=" },
314
+ { kind: "exact", option: "--ext-diff" },
315
+ { kind: "exact", option: "--textconv" },
316
+ { kind: "exact", option: "--exec" },
317
+ { kind: "prefix", prefix: "--exec=" },
318
+ ];
319
+
320
+ function gitMatchesAnyPattern(arg: string, patterns: readonly GitOptionPattern[]): boolean {
321
+ return patterns.some((p) => gitPatternMatches(p, arg));
322
+ }
323
+
324
+ function gitHasUnsafeGlobalOption(globalArgs: string[]): boolean {
325
+ return globalArgs.some((arg) => gitMatchesAnyPattern(arg, UNSAFE_GIT_GLOBAL_OPTIONS));
326
+ }
327
+
328
+ function gitSubcommandArgsAreReadOnly(args: string[]): boolean {
329
+ return !args.some((arg) => gitMatchesAnyPattern(arg, UNSAFE_GIT_SUBCOMMAND_OPTIONS));
330
+ }
331
+
332
+ /** `git branch` 只读 flag 集合。 */
333
+ const GIT_BRANCH_READ_ONLY_FLAGS: ReadonlySet<string> = new Set([
334
+ "--list",
335
+ "-l",
336
+ "--show-current",
337
+ "-a",
338
+ "--all",
339
+ "-r",
340
+ "--remotes",
341
+ "-v",
342
+ "-vv",
343
+ "--verbose",
344
+ ]);
345
+
346
+ /** `git branch` 仅当参数明确表示只读查询时才安全。 */
347
+ function gitBranchIsReadOnly(branchArgs: string[]): boolean {
348
+ if (branchArgs.length === 0) return true; // `git branch` 列分支
349
+ let sawReadOnlyFlag = false;
350
+ for (const arg of branchArgs) {
351
+ if (GIT_BRANCH_READ_ONLY_FLAGS.has(arg) || arg.startsWith("--format=")) {
352
+ sawReadOnlyFlag = true;
353
+ } else {
354
+ // 任何其他 flag 或位置参数可能创建/重命名/删除分支
355
+ return false;
356
+ }
357
+ }
358
+ return sawReadOnlyFlag;
359
+ }
360
+
361
+ const SAFE_GIT_SUBCOMMANDS: readonly string[] = ["status", "log", "diff", "show", "branch"];
362
+
363
+ function isSafeGitCommand(command: string[]): boolean {
364
+ const found = findGitSubcommand(command, SAFE_GIT_SUBCOMMANDS);
365
+ if (found === undefined) return false;
366
+ const [subcommandIdx, subcommand] = found;
367
+
368
+ const globalArgs = command.slice(1, subcommandIdx);
369
+ if (gitHasUnsafeGlobalOption(globalArgs)) return false;
370
+
371
+ const subcommandArgs = command.slice(subcommandIdx + 1);
372
+
373
+ switch (subcommand) {
374
+ case "status":
375
+ case "log":
376
+ case "diff":
377
+ case "show":
378
+ return gitSubcommandArgsAreReadOnly(subcommandArgs);
379
+ case "branch":
380
+ return (
381
+ gitSubcommandArgsAreReadOnly(subcommandArgs) &&
382
+ gitBranchIsReadOnly(subcommandArgs)
383
+ );
384
+ default:
385
+ return false;
386
+ }
387
+ }
388
+
389
+ // ── sed(仅 `sed -n {N|M,N}p [file]`)──
390
+
391
+ /** sed `-n {N|M,N}p [file]` 的最大 argv 长度:`sed -n {arg} [file]` = 4。 */
392
+ const SED_MAX_ARGV_LENGTH = 4;
393
+ /** `M,N` 范围形式按逗号拆分后的段数。 */
394
+ const SED_RANGE_PARTS = 2;
395
+
396
+ /** 匹配 /^(\d+,)?\d+p$/ —— 移植自 is_valid_sed_n_arg。 */
397
+ function isValidSedNArg(arg: string | undefined): boolean {
398
+ if (arg === undefined) return false;
399
+ // 必须以 'p' 结尾,剥离之
400
+ if (!arg.endsWith("p")) return false;
401
+ const core = arg.slice(0, -1);
402
+ if (core.length === 0) return false;
403
+ const parts = core.split(",");
404
+ if (parts.length === 1) {
405
+ const [num] = parts;
406
+ return num !== undefined && num.length > 0 && /^\d+$/.test(num);
407
+ }
408
+ if (parts.length === SED_RANGE_PARTS) {
409
+ const [a, b] = parts;
410
+ return (
411
+ a !== undefined &&
412
+ b !== undefined &&
413
+ a.length > 0 &&
414
+ b.length > 0 &&
415
+ /^\d+$/.test(a) &&
416
+ /^\d+$/.test(b)
417
+ );
418
+ }
419
+ return false;
420
+ }
421
+
422
+ function isSafeSed(command: string[]): boolean {
423
+ // command.len() <= SED_MAX_ARGV_LENGTH && command[1]=='-n' && is_valid_sed_n_arg(command[2])
424
+ return (
425
+ command.length <= SED_MAX_ARGV_LENGTH &&
426
+ command[1] === "-n" &&
427
+ isValidSedNArg(command[2])
428
+ );
429
+ }
430
+
431
+ // ── sort ──
432
+
433
+ const UNSAFE_SORT_OPTIONS: ReadonlySet<string> = new Set(["-o", "--output"]);
434
+
435
+ function isSafeSort(argv: string[]): boolean {
436
+ return !argv.slice(1).some((arg) => {
437
+ return (
438
+ UNSAFE_SORT_OPTIONS.has(arg) ||
439
+ arg.startsWith("--output=") ||
440
+ // 合并短 flag 簇含 o(-o / -fo / -of / -nfo)
441
+ shortFlagClusterHasChar(arg, "o")
442
+ );
443
+ });
444
+ }
445
+
446
+ // ── iconv ──
447
+
448
+ const UNSAFE_ICONV_OPTIONS: ReadonlySet<string> = new Set(["-o", "--output"]);
449
+
450
+ function isSafeIconv(argv: string[]): boolean {
451
+ return !argv.slice(1).some((arg) => {
452
+ return (
453
+ UNSAFE_ICONV_OPTIONS.has(arg) ||
454
+ arg.startsWith("--output=") ||
455
+ // 合并短 flag 簇含 o(-o / -fo / -of)
456
+ shortFlagClusterHasChar(arg, "o")
457
+ );
458
+ });
459
+ }
460
+
461
+ // ── shuf ──
462
+
463
+ const UNSAFE_SHUF_OPTIONS: ReadonlySet<string> = new Set(["-o", "--output"]);
464
+
465
+ function isSafeShuf(argv: string[]): boolean {
466
+ return !argv.slice(1).some((arg) => {
467
+ return (
468
+ UNSAFE_SHUF_OPTIONS.has(arg) ||
469
+ arg.startsWith("--output=") ||
470
+ // 合并短 flag 簇含 o(-o / -fo / -of)
471
+ shortFlagClusterHasChar(arg, "o")
472
+ );
473
+ });
474
+ }
475
+
476
+ // ── date ──
477
+
478
+ const UNSAFE_DATE_OPTIONS: ReadonlySet<string> = new Set(["-s", "--set"]);
479
+
480
+ function isSafeDate(argv: string[]): boolean {
481
+ return !argv.slice(1).some((arg) => {
482
+ return (
483
+ UNSAFE_DATE_OPTIONS.has(arg) ||
484
+ arg.startsWith("--set=") ||
485
+ // 合并短 flag 簇含 s(-s / -ds / -sd)
486
+ shortFlagClusterHasChar(arg, "s")
487
+ );
488
+ });
489
+ }
490
+
491
+ // ── is_safe_to_call_with_exec(单条 argv 判定)──
492
+
493
+ function isSafeToCallWithExec(command: string[]): boolean {
494
+ const cmd0 = command[0];
495
+ if (cmd0 === undefined) return false;
496
+ const key = executableNameLookupKey(cmd0);
497
+ if (key === undefined) return false;
498
+
499
+ // 无条件 50 安全命令
500
+ if (BUILTIN_UNCONDITIONAL_SAFE.has(key)) return true;
501
+
502
+ switch (key) {
503
+ case "base64":
504
+ return isSafeBase64(command);
505
+ case "find":
506
+ return isSafeFind(command);
507
+ case "rg":
508
+ return isSafeRipgrep(command);
509
+ case "git":
510
+ return isSafeGitCommand(command);
511
+ case "sed":
512
+ return isSafeSed(command);
513
+ case "sort":
514
+ return isSafeSort(command);
515
+ case "iconv":
516
+ return isSafeIconv(command);
517
+ case "shuf":
518
+ return isSafeShuf(command);
519
+ case "date":
520
+ return isSafeDate(command);
521
+ default:
522
+ return false;
523
+ }
524
+ }
525
+
526
+ /**
527
+ * 判断 argv 是否命中 Codex 已知安全白名单(50 无条件 + 9 条件)。
528
+ *
529
+ * 对 argv[0]=='zsh' 归一化为 'bash'(与源码一致;但本扩展不实现 bash -lc 复合解析,
530
+ * 故 'bash' 作为 argv[0] 时只走单条 argv 判定,不会被误判为安全——除非 argv[0]
531
+ * 本身就是无条件安全命令,这种情况不存在)。
532
+ *
533
+ * 纯函数:无 side effect,不 spawn 子进程。
534
+ */
535
+ export function isKnownSafeCommand(argv: string[]): boolean {
536
+ if (argv.length === 0) return false;
537
+
538
+ // argv[0]=='zsh' → 'bash' 归一化(源码行为)
539
+ const normalized: string[] = argv.map((s) => (s === "zsh" ? "bash" : s));
540
+
541
+ return isSafeToCallWithExec(normalized);
542
+ }
543
+
544
+ // ──────────────────────── permission-gate 危险规则(12 条)────────────────────────
545
+
546
+ /**
547
+ * 内置危险规则(permission-gate DEFAULT_RULES,正则字符串 + 'i' flag)。
548
+ *
549
+ * pattern 是 RegExp 源字符串(含 `\b`/`\s` 等),由 matcher.resolvePattern 用
550
+ * `new RegExp(pattern, 'i')` 编译。action='deny',source='builtin-danger'。
551
+ */
552
+ export const BUILTIN_DANGER_RULES: readonly Rule[] = [
553
+ {
554
+ id: "bd-001",
555
+ tool: "bash",
556
+ // 覆盖分离 flag 写法(rm -f -r /)与合并 flag(rm -rf):
557
+ // `rm` 后任意 token 序列中出现含 `r` 的短 flag 簇(-r/-fr/-rf/-xr...)或 --recursive。
558
+ // 注意:单 `-` 前必须锚定空白(\s-),否则 `.*` 会回溯到长选项 `--verbose` 的
559
+ // 第二个 `-`,把 `-ver` 当成短 flag 簇而误命中。`\s-` 确保只匹配 token 起始的
560
+ // 单连字符短 flag 簇,不会误吃 `--verbose` / `--dir` 等长选项中的 r。
561
+ pattern: "\\brm\\b.*(\\s-(?:[a-zA-Z]*r)|--recursive)",
562
+ action: "deny",
563
+ source: "builtin-danger",
564
+ description: "recursive delete",
565
+ },
566
+ {
567
+ id: "bd-002",
568
+ tool: "bash",
569
+ pattern: "\\bsudo\\b",
570
+ action: "deny",
571
+ source: "builtin-danger",
572
+ description: "sudo",
573
+ },
574
+ {
575
+ id: "bd-003",
576
+ tool: "bash",
577
+ // 覆盖等价写法:777 / a+rwx / ugo+rwx / ugo=rwx(均赋予所有用户写权限)。
578
+ pattern: "\\bchmod\\b.*(777|a\\+rwx|ugo\\+rwx|ugo=rwx)",
579
+ action: "deny",
580
+ source: "builtin-danger",
581
+ description: "world-writable permissions",
582
+ },
583
+ {
584
+ id: "bd-004",
585
+ tool: "bash",
586
+ // 覆盖两类危险设备写入:
587
+ // 1) 重定向 `> /dev/...`:`> /dev/sda`、`> /dev/nvme0n1`、`> /dev/mmcblk0`
588
+ // 2) dd of= 写入:`dd of=/dev/sda`、`dd of=/dev/nvme0n1`
589
+ // 设备名扩展至 [a-z0-9](支持 nvme0n1 / mmcblk0 等现代命名)。
590
+ pattern: "(>\\s*/dev/(sd|hd|nvme|mmcblk|vd|xvd)[a-z0-9]+|of=/dev/(sd|hd|nvme|mmcblk|vd|xvd)[a-z0-9]+)",
591
+ action: "deny",
592
+ source: "builtin-danger",
593
+ description: "raw device write",
594
+ },
595
+ {
596
+ id: "bd-005",
597
+ tool: "bash",
598
+ pattern: "\\bgit\\s+push\\s+.*(-f\\b|--force\\b)",
599
+ action: "deny",
600
+ source: "builtin-danger",
601
+ description: "force push",
602
+ },
603
+ {
604
+ id: "bd-006",
605
+ tool: "bash",
606
+ pattern: "\\bgit\\s+reset\\s+--hard\\b",
607
+ action: "deny",
608
+ source: "builtin-danger",
609
+ description: "hard reset",
610
+ },
611
+ {
612
+ id: "bd-007",
613
+ tool: "bash",
614
+ // 覆盖分离 flag(git clean -d -f)与合并 flag(git clean -fd):
615
+ // `git clean` 后任意位置出现含 `f` 的短 flag 簇(-f/-fd/-df)或 --force。
616
+ // 注意:单 `-` 前必须锚定空白(\s-),否则 `.*` 会回溯到长选项 `--files` 的
617
+ // 第二个 `-`,把 `-fil` 当成短 flag 簇而误命中。`\s-` 确保只匹配 token 起始的
618
+ // 单连字符短 flag 簇,不会误吃 `--files` 等长选项中的 f。
619
+ pattern: "\\bgit\\s+clean\\b.*(\\s-(?:[a-zA-Z]*f)|--force)",
620
+ action: "deny",
621
+ source: "builtin-danger",
622
+ description: "git clean --force",
623
+ },
624
+ {
625
+ id: "bd-008",
626
+ tool: "bash",
627
+ // 覆盖 `git checkout .` 与 `git checkout -- .`(均可丢弃工作区所有改动)。
628
+ // 行尾锚定匹配字符串结束或命令分隔符(; & |)。
629
+ pattern: "\\bgit\\s+checkout\\s+(--\\s+)?\\.\\s*($|[;&|])",
630
+ action: "deny",
631
+ source: "builtin-danger",
632
+ description: "git checkout . (discard all)",
633
+ },
634
+ {
635
+ id: "bd-009",
636
+ tool: "bash",
637
+ pattern: "\\bgit\\s+restore\\b",
638
+ action: "deny",
639
+ source: "builtin-danger",
640
+ description: "git restore",
641
+ },
642
+ {
643
+ id: "bd-010",
644
+ tool: "bash",
645
+ pattern: "\\b(curl|wget)\\b.*\\|\\s*(ba)?sh\\b",
646
+ action: "deny",
647
+ source: "builtin-danger",
648
+ description: "pipe to shell",
649
+ },
650
+ {
651
+ id: "bd-011",
652
+ tool: "bash",
653
+ pattern: "\\bgh\\s+repo\\s+(create|delete|rename|archive)\\b",
654
+ action: "deny",
655
+ source: "builtin-danger",
656
+ description: "modify GitHub repo",
657
+ },
658
+ {
659
+ id: "bd-012",
660
+ tool: "bash",
661
+ pattern: "\\bgh\\s+release\\s+(create|delete|edit)\\b",
662
+ action: "deny",
663
+ source: "builtin-danger",
664
+ description: "modify GitHub release",
665
+ },
666
+ ];
667
+
668
+ /**
669
+ * 返回默认规则(builtin-danger 12 条)。
670
+ *
671
+ * 深拷贝:每条 Rule 对象都复制(`{ ...r }`),防外部修改返回值后污染共享常量
672
+ * BUILTIN_DANGER_RULES(m6:浅拷贝只复制数组,元素仍共享,caller 改 rule.action
673
+ * /rule.pattern 会污染内置常量,影响后续所有调用)。
674
+ */
675
+ export function getDefaultRules(): Rule[] {
676
+ return BUILTIN_DANGER_RULES.map((r) => ({ ...r }));
677
+ }
678
+
679
+ /** 重新导出 CONDITIONAL_SAFE_COMMANDS 仅供测试断言白名单覆盖完整。 */
680
+ export const _CONDITIONAL_SAFE_COMMANDS_FOR_TEST = CONDITIONAL_SAFE_COMMANDS;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * rules 模块 barrel —— 只 re-export 公开 API。
3
+ *
4
+ * 公开 surface(供 W5/调用方使用):
5
+ * - matchRules / matchRulesForArgv:规则匹配入口
6
+ * - getDefaultRules:内置危险规则(12 条)
7
+ * - isKnownSafeCommand:Codex 白名单判断(W5 可能直接用)
8
+ * - wildcardToRegExp:wildcard 编译(测试用)
9
+ * - findGitSubcommand:git 子命令提取(测试用)
10
+ *
11
+ * 类型从 ../types.js re-export(Rule / RuleMatchResult / PermissionAction 等)。
12
+ */
13
+ export type {
14
+ PermissionAction,
15
+ Rule,
16
+ RuleMatchResult,
17
+ RuleSource,
18
+ } from "../types.js";
19
+ export {
20
+ BUILTIN_DANGER_RULES,
21
+ BUILTIN_UNCONDITIONAL_SAFE,
22
+ findGitSubcommand,
23
+ getDefaultRules,
24
+ isKnownSafeCommand,
25
+ } from "./builtins.js";
26
+ export { matchRules, matchRulesForArgv, resolvePattern } from "./matcher.js";
27
+ export { wildcardToRegExp } from "./wildcard.js";