@trim21/personal-pi-extensions 0.0.272 → 0.0.273

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.272",
3
+ "version": "0.0.273",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -224,6 +224,12 @@ export class BwrapRuntime {
224
224
  this.sandboxDisabled = pi.getFlag("no-bwrap") === true && ctx.hasUI;
225
225
  this.resolved = undefined;
226
226
  this.bwrapUnavailable = false;
227
+ if (process.platform === "win32") {
228
+ // Windows 没有 bubblewrap:不做 bwrap 检测、不显示 bwrap 状态,
229
+ // 每条 bash 命令在 execute 时逐条人工审批,模型无需知道 bwrap 的存在。
230
+ ctx.ui.notify("Every bash command requires user approval before it runs.", "info");
231
+ return;
232
+ }
227
233
  const runtime = this.resolve(ctx);
228
234
  if (runtime.bwrapEnabled) {
229
235
  try {
@@ -254,12 +260,18 @@ export class BwrapRuntime {
254
260
 
255
261
  pi.on("before_agent_start", (event, ctx) => {
256
262
  const runtime = this.resolve(ctx);
263
+ const isWindows = process.platform === "win32";
257
264
  const modeText = ctx.hasUI
258
- ? `Current bwrap mode: **${runtime.mode}**. The bwrap runtime selects sandboxing and, when requested, user approval for unsandboxed execution.`
259
- : "This headless session is forced into bwrap readonly mode. Unsandboxed execution cannot be approved.";
260
- const unavailableText = this.bwrapUnavailable
261
- ? " bwrap is unavailable (binary not found): bash commands are refused unless the user explicitly approves unsandboxed execution."
262
- : "";
265
+ ? isWindows
266
+ ? "Every bash command requires user approval before it runs."
267
+ : `Current bwrap mode: **${runtime.mode}**. The bwrap runtime selects sandboxing and, when requested, user approval for unsandboxed execution.`
268
+ : isWindows
269
+ ? "This headless session cannot approve commands: bash commands are refused."
270
+ : "This headless session is forced into bwrap readonly mode. Unsandboxed execution cannot be approved.";
271
+ const unavailableText =
272
+ !isWindows && this.bwrapUnavailable
273
+ ? " bwrap is unavailable (binary not found): bash commands are refused unless the user explicitly approves unsandboxed execution."
274
+ : "";
263
275
  return {
264
276
  systemPrompt:
265
277
  event.systemPrompt + `\n\n## Command Execution\n${modeText}${unavailableText}\n`,
@@ -283,13 +295,24 @@ export class BwrapRuntime {
283
295
 
284
296
  async execute(request: BwrapExecutionRequest): Promise<BwrapExecutionResult> {
285
297
  const runtime = this.resolve(request.ctx);
286
- if (this.bwrapUnavailable && runtime.bwrapEnabled && request.requestFullAccess !== true) {
298
+ const isWindows = process.platform === "win32";
299
+ // 非 Windows:bwrap 缺失时 fail closed,普通命令一律拒绝(除非显式 full-access 审批)。
300
+ // Windows:没有 bubblewrap,这是预期状态,降级为每条命令都走人工审核。
301
+ if (
302
+ !isWindows &&
303
+ this.bwrapUnavailable &&
304
+ runtime.bwrapEnabled &&
305
+ request.requestFullAccess !== true
306
+ ) {
287
307
  throw new Error(
288
308
  "bwrap (bubblewrap) not found; refusing to execute commands without sandboxing. " +
289
309
  "Install bubblewrap and restart the session, or pass --no-bwrap to disable the sandbox explicitly.",
290
310
  );
291
311
  }
292
- if (request.requestFullAccess === true && runtime.bwrapEnabled) {
312
+ // 需要人工审批:非 Windows requestFullAccess;Windows 上默认所有命令
313
+ // (allow-all 模式是显式 opt-out,仍直接执行)。
314
+ const needsApproval = request.requestFullAccess === true || (isWindows && runtime.bwrapEnabled);
315
+ if (needsApproval && runtime.bwrapEnabled) {
293
316
  // 先按 approvalRules 自动判定:allow 直接放行,deny 直接拒绝,未命中才弹框
294
317
  const decision = await evaluateBashApproval(request.command, runtime.approvalRules);
295
318
  if (decision === "deny") {
@@ -300,9 +323,9 @@ export class BwrapRuntime {
300
323
  }
301
324
  }
302
325
  const operations =
303
- runtime.bwrapEnabled && request.requestFullAccess !== true
304
- ? createBwrapBashOperations(runtime)
305
- : createLocalBashOperations();
326
+ isWindows || needsApproval || !runtime.bwrapEnabled
327
+ ? createLocalBashOperations()
328
+ : createBwrapBashOperations(runtime);
306
329
  const output = new BashOutput();
307
330
  const { onUpdate } = request;
308
331
 
@@ -389,15 +412,36 @@ export class BwrapRuntime {
389
412
  ): Promise<void> {
390
413
  const policy = resolveEscalation({ hasUI: ctx.hasUI });
391
414
  if (policy.kind === "deny") throw new Error(policy.reason);
415
+ // 弹框前解析命令的持久化规则(Allow forever 会写入),在弹框里预览给
416
+ // 用户:`echo 1` → `echo *`,避免用户对"永久允许"持久化什么一无所知。
417
+ const patterns = await commandPatternsFor(command);
392
418
  // dcg 扫描建议是可选的参考文本:未安装时静默跳过;已安装但扫描失败
393
419
  // 时 notify 提示,弹窗本身与无 dcg 时一致
394
420
  const outcome = await dcgSuggestion(command);
395
- const suggestionBlock =
396
- outcome.kind === "suggestion" ? `\n${outcome.suggestion.text}\n---\n` : "";
397
421
  if (outcome.kind === "failed") {
398
422
  ctx.ui.notify(`dcg 扫描失败,本次无破坏性命令建议: ${outcome.detail}`, "warning");
399
423
  }
400
- const description = `Allow this command to run without sandbox?\n---\n\nReason: ${escapeHtml(reason ?? "(No reason provided by model)")}\n---\n${suggestionBlock}${fenceCodeBlock(command)}`;
424
+ // 弹框主体按行组织('\n' join),便于 review;suggestion forever 预览
425
+ // 块带前导空行 + 尾部 "---" 分隔,输出与历史逐字符一致。
426
+ const lines: string[] = [
427
+ "Allow this command to run without sandbox?",
428
+ "---",
429
+ "",
430
+ `Reason: ${escapeHtml(reason ?? "(No reason provided by model)")}`,
431
+ "---",
432
+ ];
433
+ if (outcome.kind === "suggestion") {
434
+ lines.push("", outcome.suggestion.text, "---");
435
+ }
436
+ if (patterns.length > 0) {
437
+ lines.push(
438
+ "",
439
+ `Allow forever 将持久化规则: ${patterns.map((pattern) => `\`${pattern}\``).join(", ")}`,
440
+ "---",
441
+ );
442
+ }
443
+ lines.push(fenceCodeBlock(command));
444
+ const description = lines.join("\n");
401
445
 
402
446
  // 单选:允许一次 / 永久允许(写入规则)/ 拒绝 / 拒绝并附理由(弹输入框)
403
447
  const verdict = await selectWithOptionalInput(description, FULL_ACCESS_CHOICES, ctx.ui, {
@@ -413,7 +457,7 @@ export class BwrapRuntime {
413
457
  return;
414
458
  }
415
459
  case ALLOW_FOREVER: {
416
- await this.persistAllowRule(ctx, command);
460
+ await this.persistAllowRule(ctx, command, patterns);
417
461
  return;
418
462
  }
419
463
  case DENY: {
@@ -431,10 +475,14 @@ export class BwrapRuntime {
431
475
  }
432
476
 
433
477
  /** 把命令的权限模式写入项目 bwrap.json 的 approvalRules(allow forever)。 */
434
- private async persistAllowRule(ctx: ExtensionContext, command: string): Promise<void> {
435
- const patterns = await commandPatternsFor(command);
436
- if (patterns.length === 0) return; // 解析失败:本次放行,不写规则
437
- const newRules: ApprovalRule[] = patterns.map((pattern) => ({ action: "allow", pattern }));
478
+ private async persistAllowRule(
479
+ ctx: ExtensionContext,
480
+ command: string,
481
+ patterns?: string[],
482
+ ): Promise<void> {
483
+ const rulePatterns = patterns ?? (await commandPatternsFor(command));
484
+ if (rulePatterns.length === 0) return; // 解析失败:本次放行,不写规则
485
+ const newRules: ApprovalRule[] = rulePatterns.map((pattern) => ({ action: "allow", pattern }));
438
486
  const { project } = getBwrapConfigPaths(ctx.cwd);
439
487
  let config: Record<string, unknown> = {};
440
488
  if (existsSync(project)) {
@@ -1086,6 +1086,15 @@ export async function writeLogFile(
1086
1086
  // ── tools ────────────────────────────────────────────────────────────────────
1087
1087
 
1088
1088
  export default function ghReadonlyTools(pi: ExtensionAPI) {
1089
+ // Windows 上禁用:gh 可执行文件的探测(无扩展名 + POSIX 路径)与进程
1090
+ // 管理(SIGTERM 信号语义)都是 POSIX 假设,不做 Windows 适配。
1091
+ if (process.platform === "win32") {
1092
+ pi.on("session_start", (_event, ctx) => {
1093
+ ctx.ui.notify("gh-readonly tools are disabled on Windows.", "warning");
1094
+ });
1095
+ return;
1096
+ }
1097
+
1089
1098
  // Fail fast: the `gh` CLI is the only backend for these tools. Without it the
1090
1099
  // extension registers nothing and reports the problem at session start, so
1091
1100
  // the user gets one clear error instead of a dozen failing tool calls.
@@ -6,6 +6,8 @@
6
6
  * - Paths outside require user approval via a confirmation dialog showing a
7
7
  * `diff` code block preview of the pending change.
8
8
  * - Headless sessions (no UI) reject outside writes outright.
9
+ * - Windows (no sandbox fallback): writes are restricted to the workspace;
10
+ * outside paths are rejected outright, with no approval path.
9
11
  *
10
12
  * Callers have already parsed their tool arguments, so the guard only takes the
11
13
  * resolved pieces: the raw target path and the pending change (oldText/newText).
@@ -122,6 +124,14 @@ export async function guardWriteAccess(
122
124
  const { absolutePath } = opts;
123
125
  if (isPathAllowed(absolutePath, ctx.cwd)) return;
124
126
 
127
+ if (process.platform === "win32") {
128
+ // Windows 上退化为「只能写工作区」:无沙箱兜底,工作区外写入一律拒绝,
129
+ // 不提供审批路径。
130
+ throw new Error(
131
+ `Path "${absolutePath}" is outside workspace. Writes outside the workspace are not allowed on Windows.`,
132
+ );
133
+ }
134
+
125
135
  if (!ctx.hasUI || !ctx.ui) {
126
136
  throw new Error(`Path "${absolutePath}" is outside workspace. No UI available for approval.`);
127
137
  }
@@ -544,6 +544,16 @@ export function formatAgentListSection(agents: AgentConfig[]): string {
544
544
  // ── extension ────────────────────────────────────────────────────────────────
545
545
 
546
546
  export default function spawnAgent(pi: ExtensionAPI) {
547
+ // Windows 上禁用:子代理进程的派生(node/bun 运行时下回退到 `pi` 命令,
548
+ // 而 npm 安装的 pi 是 .cmd shim,spawn 无法直接启动)与信号管理都是
549
+ // POSIX 假设,不做 Windows 适配。
550
+ if (process.platform === "win32") {
551
+ pi.on("session_start", (_event, ctx) => {
552
+ ctx.ui.notify("spawn-agent is disabled on Windows.", "warning");
553
+ });
554
+ return;
555
+ }
556
+
547
557
  // Discover the available subagent types once at extension startup. The
548
558
  // extension owns this discovery: the model never has to guess agent names
549
559
  // or read the agent directory itself. Editing ~/.pi/agent/agents/*.md