@trim21/personal-pi-extensions 0.1.530 → 0.1.531
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 +1 -1
- package/src/bwrap/runtime.ts +61 -78
- package/src/lib/ui.ts +0 -51
package/package.json
CHANGED
package/src/bwrap/runtime.ts
CHANGED
|
@@ -18,12 +18,7 @@ import { type TObject, Type } from "typebox";
|
|
|
18
18
|
import { type CommandSpec, parseCommand } from "../lib/cli.js";
|
|
19
19
|
import { fenceCodeBlock } from "../lib/markdown.js";
|
|
20
20
|
import { formatDisplayPath } from "../lib/path.js";
|
|
21
|
-
import {
|
|
22
|
-
type CheckboxAction,
|
|
23
|
-
type SelectAction,
|
|
24
|
-
selectCheckboxActions,
|
|
25
|
-
selectWithOptionalInput,
|
|
26
|
-
} from "../lib/ui.js";
|
|
21
|
+
import { type SelectAction, selectMultiple, selectWithOptionalInput } from "../lib/ui.js";
|
|
27
22
|
import { type ApprovalRule, evaluateBashApproval, matchRule } from "./approval-rules.js";
|
|
28
23
|
import { commandPatternsFor } from "./approval-suggest.js";
|
|
29
24
|
import {
|
|
@@ -55,24 +50,19 @@ export function resolveEscalation(opts: { hasUI: boolean }): EscalationDecision
|
|
|
55
50
|
|
|
56
51
|
/** 全权限审批对话框的选项 label(也作为 switch 匹配键与测试引用)。 */
|
|
57
52
|
export const ALLOW_ONCE = "Allow once";
|
|
58
|
-
export const ALLOW_FOREVER = "Allow forever";
|
|
59
53
|
export const DENY = "Deny";
|
|
60
54
|
export const DENY_WITH_REASON = "Deny with reason";
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{ label: ALLOW_FOREVER },
|
|
66
|
-
{ label: DENY },
|
|
67
|
-
{ label: DENY_WITH_REASON, inputPrompt: "Why was this denied?" },
|
|
68
|
-
];
|
|
55
|
+
/** 第一层的折叠入口:进入按 pattern 勾选持久化规则的子菜单。 */
|
|
56
|
+
export const EDIT_RULES = "Edit approval rules";
|
|
57
|
+
/** 规则子菜单的返回项:结束勾选,回到第一层做放行/拒绝决策。 */
|
|
58
|
+
export const BACK = "Back";
|
|
69
59
|
|
|
70
60
|
/**
|
|
71
61
|
* 全权限审批 UI 的决策结果:业务层(execute/approveFullAccess)据此
|
|
72
62
|
* 决定放行、拒绝并持久化勾选的规则,UI 层不直接产生副作用。
|
|
73
63
|
*/
|
|
74
64
|
export interface FullAccessUIDecision {
|
|
75
|
-
/** 用户选择的动作 label(ALLOW_ONCE /
|
|
65
|
+
/** 用户选择的动作 label(ALLOW_ONCE / DENY / DENY_WITH_REASON)。 */
|
|
76
66
|
result: string;
|
|
77
67
|
/** 用户勾选、需持久化为 allow 规则的 pattern;未勾选时为空数组。 */
|
|
78
68
|
foreverApprovedPattern: string[];
|
|
@@ -511,10 +501,6 @@ export class BwrapRuntime {
|
|
|
511
501
|
}
|
|
512
502
|
return;
|
|
513
503
|
}
|
|
514
|
-
case ALLOW_FOREVER: {
|
|
515
|
-
await this.persistAllowRule(ctx, command, foreverApprovedPattern);
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
504
|
case DENY: {
|
|
519
505
|
if (foreverApprovedPattern.length > 0) {
|
|
520
506
|
await this.persistAllowRule(ctx, command, foreverApprovedPattern);
|
|
@@ -546,25 +532,29 @@ export class BwrapRuntime {
|
|
|
546
532
|
reason: string | undefined,
|
|
547
533
|
execCwd: string,
|
|
548
534
|
): Promise<FullAccessUIDecision | undefined> {
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
//
|
|
535
|
+
// 弹框前解析命令的持久化规则:`echo 1 | head` → `echo *`、`head *`。
|
|
536
|
+
// 持久化规则的勾选折叠进 EDIT_RULES 子菜单,主决策列表只保留放行/拒绝,
|
|
537
|
+
// 避免一屏 checkbox 淹没决策项。
|
|
552
538
|
const patterns = await commandPatternsFor(command);
|
|
553
|
-
//
|
|
539
|
+
// 子菜单只列出未命中 allow 规则的 pattern:已提前允许的部分自动放行,
|
|
554
540
|
// 无需再展示或重复勾选持久化(deny 命中的命令在 evaluate 阶段已被拒绝)。
|
|
555
541
|
const rules = this.resolve(ctx).approvalRules;
|
|
556
|
-
const unallowedPatterns =
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
542
|
+
const unallowedPatterns = [
|
|
543
|
+
...new Set(
|
|
544
|
+
patterns.filter((pattern) => {
|
|
545
|
+
const rule = rules.findLast((r) => matchRule(pattern, r.pattern));
|
|
546
|
+
return rule?.action !== "allow";
|
|
547
|
+
}),
|
|
548
|
+
),
|
|
549
|
+
];
|
|
560
550
|
// dcg 扫描建议是可选的参考文本:未安装时静默跳过;已安装但扫描失败
|
|
561
551
|
// 时 notify 提示,弹窗本身与无 dcg 时一致
|
|
562
552
|
const outcome = await dcgSuggestion(command);
|
|
563
553
|
if (outcome.kind === "failed") {
|
|
564
554
|
ctx.ui.notify(`dcg 扫描失败,本次无破坏性命令建议: ${outcome.detail}`, "warning");
|
|
565
555
|
}
|
|
566
|
-
// 弹框主体按行组织('\n' join),便于 review;suggestion
|
|
567
|
-
//
|
|
556
|
+
// 弹框主体按行组织('\n' join),便于 review;suggestion 块带前导空行 +
|
|
557
|
+
// 尾部 "---" 分隔。
|
|
568
558
|
const lines: string[] = [
|
|
569
559
|
"Allow this command to run without sandbox?",
|
|
570
560
|
"---",
|
|
@@ -575,13 +565,6 @@ export class BwrapRuntime {
|
|
|
575
565
|
if (outcome.kind === "suggestion") {
|
|
576
566
|
lines.push("", outcome.suggestion.text, "---");
|
|
577
567
|
}
|
|
578
|
-
if (unallowedPatterns.length > 0) {
|
|
579
|
-
lines.push(
|
|
580
|
-
"",
|
|
581
|
-
"勾选规则将持久化为允许规则(后续同模式命令自动放行),未勾选规则仅本次处理:",
|
|
582
|
-
"---",
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
568
|
lines.push(fenceCodeBlock(command));
|
|
586
569
|
// 执行目录与工作区不同时,提示实际执行目录(execCwd 是解析后的绝对路径,
|
|
587
570
|
// 显示用 pretty path 风格:home 内 `~/…`,否则绝对路径)
|
|
@@ -590,51 +573,51 @@ export class BwrapRuntime {
|
|
|
590
573
|
}
|
|
591
574
|
const description = lines.join("\n");
|
|
592
575
|
|
|
593
|
-
//
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
576
|
+
// 主决策列表:允许一次 / 拒绝 / 拒绝并附理由。有可持久化的 pattern 时
|
|
577
|
+
// 追加折叠入口,进入子菜单逐项勾选。
|
|
578
|
+
const actions: SelectAction[] = [
|
|
579
|
+
{ label: ALLOW_ONCE },
|
|
580
|
+
{ label: DENY },
|
|
581
|
+
{ label: DENY_WITH_REASON, inputPrompt: "Why was this denied?" },
|
|
582
|
+
];
|
|
583
|
+
if (unallowedPatterns.length > 0) {
|
|
584
|
+
actions.push({ label: EDIT_RULES });
|
|
585
|
+
}
|
|
586
|
+
// 子菜单沿用同一份说明,并补上勾选规则的语义提示。
|
|
587
|
+
const editDescription = [
|
|
588
|
+
"勾选要持久化为允许规则的命令模式(后续同模式命令自动放行,未勾选仅本次处理):",
|
|
589
|
+
"---",
|
|
590
|
+
"",
|
|
591
|
+
description,
|
|
592
|
+
].join("\n");
|
|
593
|
+
|
|
594
|
+
// 两层循环:子菜单勾选后回到主决策,直到用户在 Allow once / Deny 系列中做出
|
|
595
|
+
// 选择。勾选的规则在放行时持久化;Deny 系列同样持久化(用户确认该模式可信,
|
|
596
|
+
// 只是本次命令不执行)。子菜单关闭 = 返回主决策;主决策关闭 = 取消(上层按拒绝处理)。
|
|
597
|
+
let selected: string[] = [];
|
|
598
|
+
for (;;) {
|
|
599
|
+
const pending =
|
|
600
|
+
selected.length > 0
|
|
601
|
+
? `\n\n将持久化为允许规则: ${selected.map((pattern) => escapeHtml(pattern)).join(", ")}`
|
|
602
|
+
: "";
|
|
603
|
+
const verdict = await selectWithOptionalInput(description + pending, actions, ctx.ui, {
|
|
597
604
|
signal: ctx.signal,
|
|
598
605
|
});
|
|
599
|
-
// 关闭对话框 = 中断并拒绝,不循环重问
|
|
600
606
|
if (verdict === undefined) return undefined;
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
607
|
+
if (verdict.label !== EDIT_RULES) {
|
|
608
|
+
return {
|
|
609
|
+
result: verdict.label,
|
|
610
|
+
foreverApprovedPattern: selected,
|
|
611
|
+
reason: verdict.input,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
selected = await selectMultiple(
|
|
615
|
+
editDescription,
|
|
616
|
+
unallowedPatterns.map((pattern) => ({ label: pattern })),
|
|
617
|
+
ctx.ui,
|
|
618
|
+
{ signal: ctx.signal, doneLabel: BACK },
|
|
619
|
+
);
|
|
606
620
|
}
|
|
607
|
-
|
|
608
|
-
// 每个识别到的 pattern 一个 checkbox:勾选 = 持久化为 allow 规则。
|
|
609
|
-
// Allow once = 执行本次并持久化勾选的规则;Deny 系列 = 拒绝本次,
|
|
610
|
-
// 勾选的规则仍持久化(用户确认该模式可信,只是本次命令不执行)。
|
|
611
|
-
const actions = [
|
|
612
|
-
{ action: "allow-once", label: ALLOW_ONCE },
|
|
613
|
-
{ action: "deny", label: DENY },
|
|
614
|
-
{
|
|
615
|
-
action: "deny-with-reason",
|
|
616
|
-
label: DENY_WITH_REASON,
|
|
617
|
-
inputPrompt: "Why was this denied?",
|
|
618
|
-
},
|
|
619
|
-
] as const satisfies readonly CheckboxAction<"allow-once" | "deny" | "deny-with-reason">[];
|
|
620
|
-
const verdict = await selectCheckboxActions(
|
|
621
|
-
description,
|
|
622
|
-
[...new Set(unallowedPatterns)].map((pattern) => ({ label: pattern })),
|
|
623
|
-
actions,
|
|
624
|
-
ctx.ui,
|
|
625
|
-
{ signal: ctx.signal },
|
|
626
|
-
);
|
|
627
|
-
if (verdict === undefined) return undefined;
|
|
628
|
-
const resultByAction = {
|
|
629
|
-
"allow-once": ALLOW_ONCE,
|
|
630
|
-
deny: DENY,
|
|
631
|
-
"deny-with-reason": DENY_WITH_REASON,
|
|
632
|
-
} as const;
|
|
633
|
-
return {
|
|
634
|
-
result: resultByAction[verdict.action],
|
|
635
|
-
foreverApprovedPattern: verdict.selected,
|
|
636
|
-
reason: verdict.input,
|
|
637
|
-
};
|
|
638
621
|
}
|
|
639
622
|
|
|
640
623
|
/** 把命令的权限模式写入项目 bwrap.json 的 approvalRules(allow forever)。 */
|
package/src/lib/ui.ts
CHANGED
|
@@ -111,54 +111,3 @@ export async function selectMultiple(
|
|
|
111
111
|
}
|
|
112
112
|
return selected;
|
|
113
113
|
}
|
|
114
|
-
|
|
115
|
-
/** 结束动作:选中即结束多选循环;带 inputPrompt 时先弹输入框,内容经 input 返回。 */
|
|
116
|
-
export interface CheckboxAction<T extends string> {
|
|
117
|
-
action: T;
|
|
118
|
-
label: string;
|
|
119
|
-
inputPrompt?: string;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export interface CheckboxActionResult<T extends string> {
|
|
123
|
-
/** 勾选的条目 label(勾选顺序,经 inputPrompt 输入的条目已并入)。 */
|
|
124
|
-
selected: string[];
|
|
125
|
-
/** 用户选中的结束动作。 */
|
|
126
|
-
action: T;
|
|
127
|
-
/** 结束动作带 inputPrompt 时的输入内容:undefined=取消输入,""=空提交。 */
|
|
128
|
-
input?: string;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Checkbox 多选 + 结束动作的组合对话框(selectMultiple 的带操作变体)。
|
|
133
|
-
*
|
|
134
|
-
* 循环列出全部 `actions`(固定操作,选中即结束循环)与全部 `entries`
|
|
135
|
-
* (☑/☐ 前缀切换勾选);对话框关闭(dismiss)返回 undefined。操作在前、
|
|
136
|
-
* checkbox 条目在后,用户先决定本次动作,需要时再勾选要持久化的规则。
|
|
137
|
-
*/
|
|
138
|
-
export async function selectCheckboxActions<T extends string>(
|
|
139
|
-
title: string,
|
|
140
|
-
entries: readonly SelectAction[],
|
|
141
|
-
actions: readonly CheckboxAction<T>[],
|
|
142
|
-
ui: ExtensionContext["ui"],
|
|
143
|
-
opts: { signal?: AbortSignal } = {},
|
|
144
|
-
): Promise<CheckboxActionResult<T> | undefined> {
|
|
145
|
-
const selected: string[] = [];
|
|
146
|
-
for (;;) {
|
|
147
|
-
const selectedSet = new Set(selected);
|
|
148
|
-
const displayToLabel = new Map<string, string>();
|
|
149
|
-
const round: SelectAction[] = [];
|
|
150
|
-
for (const entry of entries) {
|
|
151
|
-
const display = `${selectedSet.has(entry.label) ? CHECKED_PREFIX : UNCHECKED_PREFIX}${entry.label}`;
|
|
152
|
-
displayToLabel.set(display, entry.label);
|
|
153
|
-
round.push({ ...entry, label: display });
|
|
154
|
-
}
|
|
155
|
-
const result = await selectWithOptionalInput(title, [...actions, ...round], ui, opts);
|
|
156
|
-
if (result === undefined) return undefined;
|
|
157
|
-
const action = actions.find((candidate) => candidate.label === result.label);
|
|
158
|
-
if (action !== undefined) return { selected, action: action.action, input: result.input };
|
|
159
|
-
const label = displayToLabel.get(result.label);
|
|
160
|
-
if (label === undefined) continue;
|
|
161
|
-
if (selectedSet.has(label)) selected.splice(selected.indexOf(label), 1);
|
|
162
|
-
else selected.push(label);
|
|
163
|
-
}
|
|
164
|
-
}
|