@trim21/personal-pi-extensions 0.1.561 → 0.1.562

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/README.md CHANGED
@@ -50,6 +50,10 @@ bash 工具(opencode 风格 `bash`、Claude Code 风格 `Bash`)注册了 `da
50
50
 
51
51
  建议模型不确定时先尝试沙箱模式,若因沙箱限制失败,再以完整权限重试。
52
52
 
53
+ 另一条提权路径是编辑类工具(`Edit` / `Write` / `lsp-rename` / `web_fetch`)写工作区外路径,同样经审批框放行。
54
+
55
+ 对话中执行 `/bwrap-deny-request` 后,上述两类提权请求都不再弹审批框,直接按用户无理由拒绝处理(bash 报 `User denied unsandboxed execution.`,编辑类工具报 `user deny <tool>: blocked`);`/bwrap-allow-request` 恢复审批。该开关只影响提权路径,沙箱内可执行的命令与工作区内写入不受影响,新会话开始时复位。
56
+
53
57
  ### 保护目录
54
58
 
55
59
  `.pi`、`.agent` 即使在 `workspace-write` 模式下也始终只读;`.git` 同样只读:工作区根是 git 仓库时保护根 `.git`,根不是 git 仓库时才递归扫描嵌套仓库(monorepo 子仓库,跳过 `node_modules`、`.venv` 等包目录)。
@@ -62,6 +66,8 @@ bash 工具(opencode 风格 `bash`、Claude Code 风格 `Bash`)注册了 `da
62
66
  - `/bwrap-allow-all` — 切换到 allow-all 模式
63
67
  - `/bwrap-workspace-write` — 切换到 workspace-write 模式
64
68
  - `/bwrap-readonly` — 切换到 readonly 模式
69
+ - `/bwrap-deny-request` — 拒绝模型的非沙盒请求,不再弹审批框
70
+ - `/bwrap-allow-request` — 恢复非沙盒请求的审批
65
71
 
66
72
  ### 配置
67
73
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.561",
3
+ "version": "0.1.562",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -18,6 +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 { requestPolicy } from "../lib/request-policy.js";
21
22
  import { type SelectAction, selectMultiple, selectWithOptionalInput } from "../lib/ui.js";
22
23
  import { type ApprovalRule, evaluateBashApproval, matchRule } from "./approval-rules.js";
23
24
  import { commandPatternsFor } from "./approval-suggest.js";
@@ -283,6 +284,12 @@ function describeNetwork(resolved: ResolvedBwrap): string {
283
284
  return "network access is unrestricted";
284
285
  }
285
286
 
287
+ /**
288
+ * 用户无理由拒绝非沙盒请求的文案。`/bwrap-deny-request` 生效时的拒绝必须与
289
+ * 用户在审批框点 Deny 完全一致——模型看到的是一次普通拒绝,而不是另一套错误语义。
290
+ */
291
+ const UNSANDBOXED_DENIED = "User denied unsandboxed execution.";
292
+
286
293
  /** 命令没经沙箱时沿用 prompt 里的说法,给出在沙盒外重跑的手段。 */
287
294
  const SANDBOX_ESCAPE_HATCH =
288
295
  "[Sandbox] If the command needs more than that, use the `dangerouslyDisableSandbox` parameter to request unsandboxed execution; the user must approve this request.";
@@ -325,6 +332,7 @@ export class BwrapRuntime {
325
332
  this.sandboxDisabled = pi.getFlag("no-bwrap") === true && ctx.hasUI;
326
333
  this.resolved = undefined;
327
334
  this.bwrapUnavailable = false;
335
+ requestPolicy.setDenyRequests(false);
328
336
  if (process.platform === "win32") {
329
337
  // Windows 没有 bubblewrap:不做 bwrap 检测、不显示 bwrap 状态,
330
338
  // 每条 bash 命令在 execute 时逐条人工审批,模型无需知道 bwrap 的存在。
@@ -346,7 +354,7 @@ export class BwrapRuntime {
346
354
  return;
347
355
  }
348
356
  }
349
- ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("accent", `bwrap: ${runtime.mode}`));
357
+ ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("accent", this.statusLabel(runtime.mode)));
350
358
  ctx.ui.notify(
351
359
  runtime.bwrapEnabled
352
360
  ? `bwrap initialized (${runtime.mode})`
@@ -373,9 +381,14 @@ export class BwrapRuntime {
373
381
  !isWindows && this.bwrapUnavailable
374
382
  ? " bwrap is unavailable (binary not found): bash commands are refused unless the user explicitly approves unsandboxed execution."
375
383
  : "";
384
+ const denyRequestsText =
385
+ !isWindows && requestPolicy.deniesRequests()
386
+ ? " Unsandboxed execution is currently denied by the user: `dangerouslyDisableSandbox` and writes outside the workspace are refused without approval."
387
+ : "";
376
388
  return {
377
389
  systemPrompt:
378
- event.systemPrompt + `\n\n## Command Execution\n${modeText}${unavailableText}\n`,
390
+ event.systemPrompt +
391
+ `\n\n## Command Execution\n${modeText}${unavailableText}${denyRequestsText}\n`,
379
392
  };
380
393
  });
381
394
 
@@ -418,6 +431,11 @@ export class BwrapRuntime {
418
431
  // (allow-all 模式是显式 opt-out,仍直接执行)。
419
432
  const needsApproval = request.requestFullAccess === true || (isWindows && runtime.bwrapEnabled);
420
433
  if (needsApproval && runtime.bwrapEnabled) {
434
+ // /bwrap-deny-request:非沙盒请求直接拒绝——审批规则与审批框都不再参与,
435
+ // 拒绝文案与用户点 Deny 相同,直到用户用 /bwrap-allow-request 恢复审批。
436
+ if (request.requestFullAccess === true && requestPolicy.deniesRequests()) {
437
+ throw new Error(UNSANDBOXED_DENIED);
438
+ }
421
439
  // 先按 approvalRules 自动判定:allow 直接放行,deny 直接拒绝,未命中才弹框
422
440
  const decision = await evaluateBashApproval(request.command, runtime.approvalRules);
423
441
  if (decision === "deny") {
@@ -572,7 +590,7 @@ export class BwrapRuntime {
572
590
  if (foreverApprovedPattern.length > 0) {
573
591
  await this.persistAllowRule(ctx, command, foreverApprovedPattern);
574
592
  }
575
- throw new Error("User denied unsandboxed execution.");
593
+ throw new Error(UNSANDBOXED_DENIED);
576
594
  }
577
595
  case DENY_WITH_REASON: {
578
596
  if (foreverApprovedPattern.length > 0) {
@@ -757,6 +775,18 @@ export class BwrapRuntime {
757
775
  description: "Reload bwrap config and restart the network stack",
758
776
  flags: Type.Object({}),
759
777
  },
778
+ "bwrap-deny-request": {
779
+ name: "bwrap-deny-request",
780
+ usage: "",
781
+ description: "Deny unsandboxed execution requests without approval",
782
+ flags: Type.Object({}),
783
+ },
784
+ "bwrap-allow-request": {
785
+ name: "bwrap-allow-request",
786
+ usage: "",
787
+ description: "Require user approval for unsandboxed execution requests again",
788
+ flags: Type.Object({}),
789
+ },
760
790
  } as const satisfies Record<string, CommandSpec<TObject>>;
761
791
 
762
792
  pi.registerCommand("bwrap", {
@@ -809,13 +839,26 @@ export class BwrapRuntime {
809
839
  this.reload(commandCtx);
810
840
  }),
811
841
  });
842
+
843
+ for (const [name, deny] of [
844
+ ["bwrap-deny-request", true],
845
+ ["bwrap-allow-request", false],
846
+ ] as const) {
847
+ pi.registerCommand(name, {
848
+ description: specs[name].description,
849
+ handler: (args, ctx) =>
850
+ this.runCommand(pi, specs[name], args, ctx, (commandCtx) =>
851
+ this.setDenyRequests(deny, commandCtx),
852
+ ),
853
+ });
854
+ }
812
855
  }
813
856
 
814
857
  private reload(ctx: ExtensionCommandContext): void {
815
858
  this.resolved = undefined;
816
859
  this.bwrapUnavailable = false;
817
860
  const runtime = this.resolve(ctx);
818
- ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("accent", `bwrap: ${runtime.mode}`));
861
+ ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("accent", this.statusLabel(runtime.mode)));
819
862
  ctx.ui.notify(`bwrap config reloaded (mode: ${runtime.mode})`, "info");
820
863
  }
821
864
 
@@ -825,7 +868,7 @@ export class BwrapRuntime {
825
868
  return;
826
869
  }
827
870
  this.setMode(ctx.cwd, mode);
828
- ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("accent", `bwrap: ${mode}`));
871
+ ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("accent", this.statusLabel(mode)));
829
872
  notifyMode(ctx, mode);
830
873
  pi.sendMessage({
831
874
  customType: "info",
@@ -834,6 +877,29 @@ export class BwrapRuntime {
834
877
  });
835
878
  }
836
879
 
880
+ /** 状态行文案:模式 + 非沙盒请求策略(拒绝时标注,便于解释模型的请求为何被拒)。 */
881
+ private statusLabel(mode: BwrapMode): string {
882
+ return `bwrap: ${mode}${requestPolicy.deniesRequests() ? " (requests denied)" : ""}`;
883
+ }
884
+
885
+ /**
886
+ * 切换非沙盒请求策略:拒绝时模型的提权请求(bash 的 `dangerouslyDisableSandbox`、
887
+ * 编辑类工具的工作区外写入)直接按用户拒绝处理,不再弹审批框。
888
+ */
889
+ private setDenyRequests(deny: boolean, ctx: ExtensionCommandContext): void {
890
+ requestPolicy.setDenyRequests(deny);
891
+ ctx.ui.notify(
892
+ deny
893
+ ? "Non-sandbox requests are denied without approval; /bwrap-allow-request restores approval."
894
+ : "Non-sandbox requests require user approval again.",
895
+ "info",
896
+ );
897
+ const runtime = this.resolve(ctx);
898
+ if (ctx.hasUI && runtime.bwrapEnabled && !this.bwrapUnavailable) {
899
+ ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("accent", this.statusLabel(runtime.mode)));
900
+ }
901
+ }
902
+
837
903
  private runCommand(
838
904
  pi: ExtensionAPI,
839
905
  spec: CommandSpec<TObject>,
@@ -0,0 +1,29 @@
1
+ /**
2
+ * 非沙盒请求策略:用户执行 `/bwrap-deny-request` 后,模型的提权请求
3
+ * (bash 的 `dangerouslyDisableSandbox`、编辑类工具的工作区外写入)不再弹审批框,
4
+ * 一律按用户「无理由拒绝」处理;`/bwrap-allow-request` 恢复审批。
5
+ *
6
+ * 状态由闭包持有(AGENTS.md:不用模块级可变变量维护跨调用状态)。
7
+ * 模块级单例让互相独立的扩展入口(bwrap runtime、claude-code / opencode 的文件工具、
8
+ * lsp-rename、web_fetch)共享同一份会话状态——各入口分别 createRequestPolicy()
9
+ * 会得到互不相通的策略,等于开关只对其中一半生效。
10
+ */
11
+
12
+ export interface RequestPolicy {
13
+ /** 是否拒绝一切非沙盒请求(不弹审批框,直接拒绝)。 */
14
+ deniesRequests(): boolean;
15
+ setDenyRequests(deny: boolean): void;
16
+ }
17
+
18
+ export function createRequestPolicy(): RequestPolicy {
19
+ let denyRequests = false;
20
+ return {
21
+ deniesRequests: () => denyRequests,
22
+ setDenyRequests(deny: boolean) {
23
+ denyRequests = deny;
24
+ },
25
+ };
26
+ }
27
+
28
+ /** 进程级共享的策略实例;由 `/bwrap-deny-request` / `/bwrap-allow-request` 切换,session_start 复位。 */
29
+ export const requestPolicy = createRequestPolicy();
@@ -8,6 +8,8 @@
8
8
  * - Headless sessions (no UI) reject outside writes outright.
9
9
  * - Windows (no sandbox fallback): writes are restricted to the workspace;
10
10
  * outside paths are rejected outright, with no approval path.
11
+ * - `/bwrap-deny-request` (non-sandbox request policy) refuses outside writes
12
+ * outright, with the same error as the user picking "Block".
11
13
  *
12
14
  * Callers have already parsed their tool arguments, so the guard only takes the
13
15
  * resolved pieces: the raw target path and the pending change (oldText/newText).
@@ -20,6 +22,7 @@ import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
20
22
 
21
23
  import { applyEdit, normalizeToLF } from "../opencode/edit-engine.js";
22
24
  import { fenceCodeBlock } from "./markdown.js";
25
+ import { requestPolicy } from "./request-policy.js";
23
26
 
24
27
  const ALWAYS_ALLOW = ["/tmp"];
25
28
  const MAX_PREVIEW_LINES = 100;
@@ -133,6 +136,12 @@ export async function guardWriteAccess(
133
136
  const { absolutePath } = opts;
134
137
  if (isPathAllowed(absolutePath, ctx.cwd)) return;
135
138
 
139
+ // 非沙盒请求策略生效时不弹审批框:工作区外写入按用户点 "Block"(无理由)处理。
140
+ // 放在 win32 / 无 UI 分支之前,策略优先级高于各平台的降级路径。
141
+ if (requestPolicy.deniesRequests()) {
142
+ throw new Error(`user deny ${opts.toolName}: blocked`);
143
+ }
144
+
136
145
  if (process.platform === "win32") {
137
146
  // Windows 上退化为「只能写工作区」:无沙箱兜底,工作区外写入一律拒绝,
138
147
  // 不提供审批路径。