@trim21/personal-pi-extensions 0.0.295 → 0.0.296
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 +69 -21
- package/src/lib/ui.ts +51 -0
package/package.json
CHANGED
package/src/bwrap/runtime.ts
CHANGED
|
@@ -16,7 +16,12 @@ import {
|
|
|
16
16
|
import { type TObject, Type } from "typebox";
|
|
17
17
|
|
|
18
18
|
import { type CommandSpec, parseCommand } from "../lib/cli.js";
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
type CheckboxAction,
|
|
21
|
+
type SelectAction,
|
|
22
|
+
selectCheckboxActions,
|
|
23
|
+
selectWithOptionalInput,
|
|
24
|
+
} from "../lib/ui.js";
|
|
20
25
|
import { type ApprovalRule, commandPatternsFor, evaluateBashApproval } from "./approval-rules.js";
|
|
21
26
|
import {
|
|
22
27
|
type BwrapMode,
|
|
@@ -412,8 +417,9 @@ export class BwrapRuntime {
|
|
|
412
417
|
): Promise<void> {
|
|
413
418
|
const policy = resolveEscalation({ hasUI: ctx.hasUI });
|
|
414
419
|
if (policy.kind === "deny") throw new Error(policy.reason);
|
|
415
|
-
//
|
|
416
|
-
//
|
|
420
|
+
// 弹框前解析命令的持久化规则(勾选的 pattern 会写入),在弹框里以
|
|
421
|
+
// checkbox 列出:`echo 1 | head` → `echo *`、`head *`,逐项决定是否
|
|
422
|
+
// allow forever,避免用户对"永久允许"持久化什么一无所知。
|
|
417
423
|
const patterns = await commandPatternsFor(command);
|
|
418
424
|
// dcg 扫描建议是可选的参考文本:未安装时静默跳过;已安装但扫描失败
|
|
419
425
|
// 时 notify 提示,弹窗本身与无 dcg 时一致
|
|
@@ -421,7 +427,7 @@ export class BwrapRuntime {
|
|
|
421
427
|
if (outcome.kind === "failed") {
|
|
422
428
|
ctx.ui.notify(`dcg 扫描失败,本次无破坏性命令建议: ${outcome.detail}`, "warning");
|
|
423
429
|
}
|
|
424
|
-
// 弹框主体按行组织('\n' join),便于 review;suggestion
|
|
430
|
+
// 弹框主体按行组织('\n' join),便于 review;suggestion 与规则说明
|
|
425
431
|
// 块带前导空行 + 尾部 "---" 分隔,输出与历史逐字符一致。
|
|
426
432
|
const lines: string[] = [
|
|
427
433
|
"Allow this command to run without sandbox?",
|
|
@@ -434,36 +440,78 @@ export class BwrapRuntime {
|
|
|
434
440
|
lines.push("", outcome.suggestion.text, "---");
|
|
435
441
|
}
|
|
436
442
|
if (patterns.length > 0) {
|
|
437
|
-
lines.push(
|
|
438
|
-
"",
|
|
439
|
-
`Allow forever 将持久化规则: ${patterns.map((pattern) => `\`${pattern}\``).join(", ")}`,
|
|
440
|
-
"---",
|
|
441
|
-
);
|
|
443
|
+
lines.push("", "勾选规则将永久允许(allow forever),未勾选规则仅本次放行:", "---");
|
|
442
444
|
}
|
|
443
445
|
lines.push(fenceCodeBlock(command));
|
|
444
446
|
const description = lines.join("\n");
|
|
445
447
|
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
448
|
+
// 解析失败(无 pattern 可勾选):保持单选对话框(Allow forever 是空操作)
|
|
449
|
+
if (patterns.length === 0) {
|
|
450
|
+
// 单选:允许一次 / 永久允许(写入规则)/ 拒绝 / 拒绝并附理由(弹输入框)
|
|
451
|
+
const verdict = await selectWithOptionalInput(description, FULL_ACCESS_CHOICES, ctx.ui, {
|
|
452
|
+
signal: ctx.signal,
|
|
453
|
+
});
|
|
454
|
+
// 关闭对话框 = 中断并拒绝,不循环重问
|
|
455
|
+
if (verdict === undefined) {
|
|
456
|
+
ctx.abort();
|
|
457
|
+
throw new Error("User denied the command execution.");
|
|
458
|
+
}
|
|
459
|
+
switch (verdict.label) {
|
|
460
|
+
case ALLOW_ONCE: {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
case ALLOW_FOREVER: {
|
|
464
|
+
await this.persistAllowRule(ctx, command, patterns);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
case DENY: {
|
|
468
|
+
throw new Error("User denied unsandboxed execution.");
|
|
469
|
+
}
|
|
470
|
+
case DENY_WITH_REASON: {
|
|
471
|
+
const feedback = verdict.input?.trim() ?? "";
|
|
472
|
+
throw new Error(
|
|
473
|
+
feedback
|
|
474
|
+
? `User denied unsandboxed execution: ${feedback}`
|
|
475
|
+
: "User denied unsandboxed execution.",
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// 每个识别到的 pattern 一个 checkbox:勾选 = 该规则 allow forever。
|
|
482
|
+
// Allow once = 执行本次并持久化勾选的规则;Deny 系列不持久化任何规则。
|
|
483
|
+
const actions = [
|
|
484
|
+
{ action: "allow-once", label: ALLOW_ONCE },
|
|
485
|
+
{ action: "deny", label: DENY },
|
|
486
|
+
{
|
|
487
|
+
action: "deny-with-reason",
|
|
488
|
+
label: DENY_WITH_REASON,
|
|
489
|
+
inputPrompt: "Why was this denied?",
|
|
490
|
+
},
|
|
491
|
+
] as const satisfies readonly CheckboxAction<"allow-once" | "deny" | "deny-with-reason">[];
|
|
492
|
+
const verdict = await selectCheckboxActions(
|
|
493
|
+
description,
|
|
494
|
+
[...new Set(patterns)].map((pattern) => ({ label: pattern })),
|
|
495
|
+
actions,
|
|
496
|
+
ctx.ui,
|
|
497
|
+
{ signal: ctx.signal },
|
|
498
|
+
);
|
|
450
499
|
// 关闭对话框 = 中断并拒绝,不循环重问
|
|
451
500
|
if (verdict === undefined) {
|
|
452
501
|
ctx.abort();
|
|
453
502
|
throw new Error("User denied the command execution.");
|
|
454
503
|
}
|
|
455
|
-
switch (verdict.
|
|
456
|
-
case
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
await this.persistAllowRule(ctx, command, patterns);
|
|
504
|
+
switch (verdict.action) {
|
|
505
|
+
case "allow-once": {
|
|
506
|
+
if (verdict.selected.length > 0) {
|
|
507
|
+
await this.persistAllowRule(ctx, command, verdict.selected);
|
|
508
|
+
}
|
|
461
509
|
return;
|
|
462
510
|
}
|
|
463
|
-
case
|
|
511
|
+
case "deny": {
|
|
464
512
|
throw new Error("User denied unsandboxed execution.");
|
|
465
513
|
}
|
|
466
|
-
case
|
|
514
|
+
case "deny-with-reason": {
|
|
467
515
|
const feedback = verdict.input?.trim() ?? "";
|
|
468
516
|
throw new Error(
|
|
469
517
|
feedback
|
package/src/lib/ui.ts
CHANGED
|
@@ -111,3 +111,54 @@ 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
|
+
* 循环列出全部 `entries`(☑/☐ 前缀切换勾选)与全部 `actions`;选中任意
|
|
135
|
+
* action 即结束循环并返回勾选结果,对话框关闭(dismiss)返回 undefined。
|
|
136
|
+
* 结束动作是固定条目(无 ☑/☐ 前缀),不参与勾选。
|
|
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
|
+
while (true) {
|
|
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, [...round, ...actions], 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
|
+
}
|