@trim21/personal-pi-extensions 0.1.581 → 0.1.584

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 trim21
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -251,7 +251,7 @@ opencode 风格的任务列表工具,参数与语义和 opencode 的 [`todowri
251
251
  - `status`:`pending` | `in_progress` | `completed` | `cancelled`
252
252
  - `priority`:`high` | `medium` | `low`
253
253
  - **持久化**:列表存进工具结果 `details.todos`,跟随会话分支自动恢复
254
- - **渲染**:每次调用用完整 markdown 列表输出对应的任务(pendant 面板自动展开)
254
+ - **渲染**:每次调用用完整 markdown 列表输出对应的任务
255
255
 
256
256
  与 pi 内置 `todo` 工具(`create`/`update`/`list`/… 单条动作)不同,本工具没有单条增删改动作,模型必须每次都传完整列表。
257
257
 
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.581",
3
+ "version": "0.1.584",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
7
7
  "pi-package"
8
8
  ],
9
+ "license": "MIT",
9
10
  "repository": {
10
11
  "type": "git",
11
12
  "url": "https://github.com/trim21/pi-extensions"
package/src/bwrap/core.ts CHANGED
@@ -134,17 +134,6 @@ export function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
134
134
  }
135
135
  }
136
136
 
137
- export function resolveHeadlessBwrap(config: BwrapConfig): ResolvedBwrap {
138
- return resolveBwrap({
139
- ...config,
140
- mode: "readonly",
141
- writablePaths: [],
142
- extraWritablePaths: [],
143
- denyPaths: [],
144
- extraArgs: [],
145
- });
146
- }
147
-
148
137
  function deepMerge(base: BwrapConfig, overrides: Partial<BwrapConfig>): BwrapConfig {
149
138
  return {
150
139
  mode: overrides.mode ?? base.mode,
@@ -31,24 +31,10 @@ import {
31
31
  resolveBwrap,
32
32
  resolveBwrapPath,
33
33
  type ResolvedBwrap,
34
- resolveHeadlessBwrap,
35
34
  } from "./core.js";
36
35
  import { dcgSuggestion } from "./dcg-scan.js";
37
36
  import { loadSandboxConfig, runInSandbox } from "./sandbox.js";
38
37
 
39
- export type EscalationDecision = { kind: "dialog" } | { kind: "deny"; reason: string };
40
-
41
- export function resolveEscalation(opts: { hasUI: boolean }): EscalationDecision {
42
- if (!opts.hasUI) {
43
- return {
44
- kind: "deny",
45
- reason:
46
- "request_full_access requires an interactive session with user approval; no UI is available in this session.",
47
- };
48
- }
49
- return { kind: "dialog" };
50
- }
51
-
52
38
  /** 全权限审批对话框的选项 label(也作为 switch 匹配键与测试引用)。 */
53
39
  export const ALLOW_ONCE = "Allow once";
54
40
  export const DENY = "Deny";
@@ -285,8 +271,9 @@ function describeNetwork(resolved: ResolvedBwrap): string {
285
271
  }
286
272
 
287
273
  /**
288
- * 用户无理由拒绝非沙盒请求的文案。`/bwrap-deny-request` 生效时的拒绝必须与
289
- * 用户在审批框点 Deny 完全一致——模型看到的是一次普通拒绝,而不是另一套错误语义。
274
+ * 用户无理由拒绝非沙盒请求的文案。`/bwrap-deny-request` 生效时与无 UI 会话里
275
+ * 需要审批的请求,都必须与用户在审批框点 Deny 完全一致——模型看到的是一次
276
+ * 普通拒绝,而不是另一套错误语义。
290
277
  */
291
278
  const UNSANDBOXED_DENIED = "User denied unsandboxed execution.";
292
279
 
@@ -319,7 +306,6 @@ export function sandboxHintBlock(hint: string | undefined): { type: "text"; text
319
306
 
320
307
  export class BwrapRuntime {
321
308
  private resolved: ResolvedBwrap | undefined;
322
- private sandboxDisabled = false;
323
309
  private bwrapUnavailable = false;
324
310
  /** net-allowlist 首次执行时解析一次 mihomo 路径,之后随 runtime 复用,不逐命令扫描 PATH。 */
325
311
  private mihomoPath: string | undefined;
@@ -334,14 +320,7 @@ export class BwrapRuntime {
334
320
  }
335
321
 
336
322
  setup(pi: ExtensionAPI): void {
337
- pi.registerFlag("no-bwrap", {
338
- description: "Disable bwrap sandboxing for bash commands",
339
- type: "boolean",
340
- default: false,
341
- });
342
-
343
323
  pi.on("session_start", (_event, ctx) => {
344
- this.sandboxDisabled = pi.getFlag("no-bwrap") === true && ctx.hasUI;
345
324
  this.resolved = undefined;
346
325
  this.bwrapUnavailable = false;
347
326
  this.policy.setDenyRequests(false);
@@ -358,7 +337,7 @@ export class BwrapRuntime {
358
337
  } catch (error) {
359
338
  // Fail closed: a missing bwrap binary must not silently degrade to an
360
339
  // unsandboxed allow-all session. Commands are refused until the user
361
- // explicitly opts out via --no-bwrap or the bwrap-allow-all command.
340
+ // explicitly opts out via the bwrap-allow-all command.
362
341
  this.bwrapUnavailable = true;
363
342
  this.resolved = undefined;
364
343
  ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("error", "bwrap: unavailable"));
@@ -386,9 +365,7 @@ export class BwrapRuntime {
386
365
  ? isWindows
387
366
  ? "Every bash command requires user approval before it runs."
388
367
  : `Current bwrap mode: **${runtime.mode}**. The bwrap runtime selects sandboxing and, when requested, user approval for unsandboxed execution.`
389
- : isWindows
390
- ? "This headless session cannot approve commands: bash commands are refused."
391
- : "This headless session is forced into bwrap readonly mode. Unsandboxed execution cannot be approved.";
368
+ : `Current bwrap mode: **${runtime.mode}**. This headless session cannot ask for approval: commands that require user approval are denied.`;
392
369
  const unavailableText =
393
370
  !isWindows && this.bwrapUnavailable
394
371
  ? " bwrap is unavailable (binary not found): bash commands are refused unless the user explicitly approves unsandboxed execution."
@@ -409,13 +386,11 @@ export class BwrapRuntime {
409
386
 
410
387
  setMode(cwd: string, mode: BwrapMode): ResolvedBwrap {
411
388
  this.resolved = loadSandboxConfig({ workspace: cwd, mode });
412
- this.sandboxDisabled = false;
413
389
  return this.resolved;
414
390
  }
415
391
 
416
392
  reset(): void {
417
393
  this.resolved = undefined;
418
- this.sandboxDisabled = false;
419
394
  this.bwrapUnavailable = false;
420
395
  }
421
396
 
@@ -432,7 +407,7 @@ export class BwrapRuntime {
432
407
  ) {
433
408
  throw new Error(
434
409
  "bwrap (bubblewrap) not found; refusing to execute commands without sandboxing. " +
435
- "Install bubblewrap and restart the session, or pass --no-bwrap to disable the sandbox explicitly.",
410
+ "Install bubblewrap and restart the session, or disable the sandbox explicitly with /bwrap-allow-all.",
436
411
  );
437
412
  }
438
413
  // workspace 恒为 session 工作区;cwd 只是本次命令的进程执行目录,
@@ -568,10 +543,8 @@ export class BwrapRuntime {
568
543
  };
569
544
  }
570
545
 
571
- private resolve(ctx: Pick<ExtensionContext, "cwd" | "hasUI">): ResolvedBwrap {
546
+ private resolve(ctx: Pick<ExtensionContext, "cwd">): ResolvedBwrap {
572
547
  const config = loadBwrapConfig(ctx.cwd);
573
- if (!ctx.hasUI) return resolveHeadlessBwrap(config);
574
- if (this.sandboxDisabled) return resolveBwrap({ ...config, mode: "allow-all" });
575
548
  if (!this.resolved) this.resolved = resolveBwrap(config);
576
549
  return this.resolved;
577
550
  }
@@ -582,8 +555,8 @@ export class BwrapRuntime {
582
555
  reason: string | undefined,
583
556
  execCwd: string,
584
557
  ): Promise<void> {
585
- const policy = resolveEscalation({ hasUI: ctx.hasUI });
586
- if (policy.kind === "deny") throw new Error(policy.reason);
558
+ // hasUI 判定推迟到审批时刻:无 UI 会话弹不了审批框,按用户点 Deny 的标准文案拒绝
559
+ if (!ctx.hasUI) throw new Error(UNSANDBOXED_DENIED);
587
560
  const decision = await this.approveFullAccessUI(ctx, command, reason, execCwd);
588
561
  // 关闭对话框 = 中断并拒绝,不循环重问
589
562
  if (decision === undefined) {
@@ -28,7 +28,6 @@ import {
28
28
  resolveBwrap,
29
29
  resolveBwrapPath,
30
30
  type ResolvedBwrap,
31
- resolveHeadlessBwrap,
32
31
  } from "./core.js";
33
32
  import type { NetworkStack } from "./network-stack.js";
34
33
 
@@ -39,8 +38,6 @@ export interface SandboxConfigInput {
39
38
  configPath?: string;
40
39
  /** 覆盖配置中的 mode(等价于会话内切 /bwrap 模式)。 */
41
40
  mode?: BwrapMode;
42
- /** 无 UI 会话策略:无论配置如何都强制 readonly。 */
43
- headless?: boolean;
44
41
  }
45
42
 
46
43
  export interface SandboxRunOptions {
@@ -97,9 +94,7 @@ export function loadSandboxConfig(input: SandboxConfigInput): ResolvedBwrap {
97
94
  const config: BwrapConfig = loadBwrapConfig(workspace, paths);
98
95
  const configured: BwrapConfig =
99
96
  input.mode === undefined ? config : { ...config, mode: input.mode };
100
- const resolved =
101
- input.headless === true ? resolveHeadlessBwrap(configured) : resolveBwrap(configured);
102
- return resolveSandboxPaths(resolved, workspace);
97
+ return resolveSandboxPaths(resolveBwrap(configured), workspace);
103
98
  }
104
99
 
105
100
  /**