@trim21/personal-pi-extensions 0.0.352 → 0.0.353

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.352",
3
+ "version": "0.0.353",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -22,7 +22,12 @@ import {
22
22
  selectCheckboxActions,
23
23
  selectWithOptionalInput,
24
24
  } from "../lib/ui.js";
25
- import { type ApprovalRule, commandPatternsFor, evaluateBashApproval } from "./approval-rules.js";
25
+ import {
26
+ type ApprovalRule,
27
+ commandPatternsFor,
28
+ evaluateBashApproval,
29
+ matchRule,
30
+ } from "./approval-rules.js";
26
31
  import {
27
32
  type BwrapMode,
28
33
  createBwrapBashOperations,
@@ -63,6 +68,19 @@ export const FULL_ACCESS_CHOICES: readonly SelectAction[] = [
63
68
  { label: DENY_WITH_REASON, inputPrompt: "Why was this denied?" },
64
69
  ];
65
70
 
71
+ /**
72
+ * 全权限审批 UI 的决策结果:业务层(execute/approveFullAccess)据此
73
+ * 决定放行、拒绝并持久化勾选的规则,UI 层不直接产生副作用。
74
+ */
75
+ export interface FullAccessUIDecision {
76
+ /** 用户选择的动作 label(ALLOW_ONCE / ALLOW_FOREVER / DENY / DENY_WITH_REASON)。 */
77
+ result: string;
78
+ /** 用户勾选、需持久化为 allow 规则的 pattern;未勾选时为空数组。 */
79
+ foreverApprovedPattern: string[];
80
+ /** DENY_WITH_REASON 时用户输入的理由。 */
81
+ reason?: string;
82
+ }
83
+
66
84
  export interface BwrapExecutionRequest {
67
85
  toolCallId: string;
68
86
  command: string;
@@ -425,10 +443,66 @@ export class BwrapRuntime {
425
443
  ): Promise<void> {
426
444
  const policy = resolveEscalation({ hasUI: ctx.hasUI });
427
445
  if (policy.kind === "deny") throw new Error(policy.reason);
446
+ const decision = await this.approveFullAccessUI(ctx, command, reason, workdir);
447
+ // 关闭对话框 = 中断并拒绝,不循环重问
448
+ if (decision === undefined) {
449
+ ctx.abort();
450
+ throw new Error("User denied the command execution.");
451
+ }
452
+ const { result, foreverApprovedPattern } = decision;
453
+ switch (result) {
454
+ case ALLOW_ONCE: {
455
+ if (foreverApprovedPattern.length > 0) {
456
+ await this.persistAllowRule(ctx, command, foreverApprovedPattern);
457
+ }
458
+ return;
459
+ }
460
+ case ALLOW_FOREVER: {
461
+ await this.persistAllowRule(ctx, command, foreverApprovedPattern);
462
+ return;
463
+ }
464
+ case DENY: {
465
+ if (foreverApprovedPattern.length > 0) {
466
+ await this.persistAllowRule(ctx, command, foreverApprovedPattern);
467
+ }
468
+ throw new Error("User denied unsandboxed execution.");
469
+ }
470
+ case DENY_WITH_REASON: {
471
+ if (foreverApprovedPattern.length > 0) {
472
+ await this.persistAllowRule(ctx, command, foreverApprovedPattern);
473
+ }
474
+ const feedback = decision.reason?.trim() ?? "";
475
+ throw new Error(
476
+ feedback
477
+ ? `User denied unsandboxed execution: ${feedback}`
478
+ : "User denied unsandboxed execution.",
479
+ );
480
+ }
481
+ }
482
+ }
483
+
484
+ /**
485
+ * 全权限审批的 UI 层:弹对话框收集用户决策并返回结构化结果,副作用
486
+ * (abort / throw / 持久化规则)由调用方根据结果处理。
487
+ * 返回 undefined 表示对话框被关闭(用户取消)。
488
+ */
489
+ private async approveFullAccessUI(
490
+ ctx: ExtensionContext,
491
+ command: string,
492
+ reason: string | undefined,
493
+ workdir?: string,
494
+ ): Promise<FullAccessUIDecision | undefined> {
428
495
  // 弹框前解析命令的持久化规则(勾选的 pattern 会写入),在弹框里以
429
496
  // checkbox 列出:`echo 1 | head` → `echo *`、`head *`,逐项决定是否
430
497
  // allow forever,避免用户对"永久允许"持久化什么一无所知。
431
498
  const patterns = await commandPatternsFor(command);
499
+ // checkbox 只列出未命中 allow 规则的 pattern:已提前允许的部分自动放行,
500
+ // 无需再展示或重复勾选持久化(deny 命中的命令在 evaluate 阶段已被拒绝)。
501
+ const rules = this.resolve(ctx).approvalRules;
502
+ const unallowedPatterns = patterns.filter((pattern) => {
503
+ const rule = rules.findLast((r) => matchRule(pattern, r.pattern));
504
+ return rule?.action !== "allow";
505
+ });
432
506
  // dcg 扫描建议是可选的参考文本:未安装时静默跳过;已安装但扫描失败
433
507
  // 时 notify 提示,弹窗本身与无 dcg 时一致
434
508
  const outcome = await dcgSuggestion(command);
@@ -447,7 +521,7 @@ export class BwrapRuntime {
447
521
  if (outcome.kind === "suggestion") {
448
522
  lines.push("", outcome.suggestion.text, "---");
449
523
  }
450
- if (patterns.length > 0) {
524
+ if (unallowedPatterns.length > 0) {
451
525
  lines.push(
452
526
  "",
453
527
  "勾选规则将持久化为允许规则(后续同模式命令自动放行),未勾选规则仅本次处理:",
@@ -468,30 +542,12 @@ export class BwrapRuntime {
468
542
  signal: ctx.signal,
469
543
  });
470
544
  // 关闭对话框 = 中断并拒绝,不循环重问
471
- if (verdict === undefined) {
472
- ctx.abort();
473
- throw new Error("User denied the command execution.");
474
- }
475
- switch (verdict.label) {
476
- case ALLOW_ONCE: {
477
- return;
478
- }
479
- case ALLOW_FOREVER: {
480
- await this.persistAllowRule(ctx, command, patterns);
481
- return;
482
- }
483
- case DENY: {
484
- throw new Error("User denied unsandboxed execution.");
485
- }
486
- case DENY_WITH_REASON: {
487
- const feedback = verdict.input?.trim() ?? "";
488
- throw new Error(
489
- feedback
490
- ? `User denied unsandboxed execution: ${feedback}`
491
- : "User denied unsandboxed execution.",
492
- );
493
- }
494
- }
545
+ if (verdict === undefined) return undefined;
546
+ return {
547
+ result: verdict.label,
548
+ foreverApprovedPattern: [],
549
+ reason: verdict.input,
550
+ };
495
551
  }
496
552
 
497
553
  // 每个识别到的 pattern 一个 checkbox:勾选 = 持久化为 allow 规则。
@@ -508,41 +564,22 @@ export class BwrapRuntime {
508
564
  ] as const satisfies readonly CheckboxAction<"allow-once" | "deny" | "deny-with-reason">[];
509
565
  const verdict = await selectCheckboxActions(
510
566
  description,
511
- [...new Set(patterns)].map((pattern) => ({ label: pattern })),
567
+ [...new Set(unallowedPatterns)].map((pattern) => ({ label: pattern })),
512
568
  actions,
513
569
  ctx.ui,
514
570
  { signal: ctx.signal },
515
571
  );
516
- // 关闭对话框 = 中断并拒绝,不循环重问
517
- if (verdict === undefined) {
518
- ctx.abort();
519
- throw new Error("User denied the command execution.");
520
- }
521
- switch (verdict.action) {
522
- case "allow-once": {
523
- if (verdict.selected.length > 0) {
524
- await this.persistAllowRule(ctx, command, verdict.selected);
525
- }
526
- return;
527
- }
528
- case "deny": {
529
- if (verdict.selected.length > 0) {
530
- await this.persistAllowRule(ctx, command, verdict.selected);
531
- }
532
- throw new Error("User denied unsandboxed execution.");
533
- }
534
- case "deny-with-reason": {
535
- if (verdict.selected.length > 0) {
536
- await this.persistAllowRule(ctx, command, verdict.selected);
537
- }
538
- const feedback = verdict.input?.trim() ?? "";
539
- throw new Error(
540
- feedback
541
- ? `User denied unsandboxed execution: ${feedback}`
542
- : "User denied unsandboxed execution.",
543
- );
544
- }
545
- }
572
+ if (verdict === undefined) return undefined;
573
+ const resultByAction = {
574
+ "allow-once": ALLOW_ONCE,
575
+ deny: DENY,
576
+ "deny-with-reason": DENY_WITH_REASON,
577
+ } as const;
578
+ return {
579
+ result: resultByAction[verdict.action],
580
+ foreverApprovedPattern: verdict.selected,
581
+ reason: verdict.input,
582
+ };
546
583
  }
547
584
 
548
585
  /** 把命令的权限模式写入项目 bwrap.json 的 approvalRules(allow forever)。 */