@trim21/personal-pi-extensions 0.1.552 → 0.1.554

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.1.552",
3
+ "version": "0.1.554",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -91,6 +91,8 @@ export interface BwrapExecutionRequest {
91
91
  */
92
92
  export interface BwrapExecutionResult {
93
93
  exitCode: number | null;
94
+ /** 沙箱状态说明(写边界 + 网络层级),失败时由上层工具作为独立信息块附上;未沙箱执行时为 undefined。 */
95
+ sandboxHint: string | undefined;
94
96
  /** 截断后的输出(尾部),未截断时为完整输出;空输出为空字符串。 */
95
97
  output: string;
96
98
  /** 完整输出的文件路径;无输出时不存在。 */
@@ -112,16 +114,19 @@ export interface BashExecutionPartial {
112
114
  export class BashInterruptedError extends Error {
113
115
  readonly kind: "timeout" | "aborted";
114
116
  readonly partial: BashExecutionPartial;
117
+ readonly sandboxHint: string | undefined;
115
118
 
116
119
  constructor(
117
120
  kind: "timeout" | "aborted",
118
121
  message: string,
119
122
  partial: BashExecutionPartial,
123
+ sandboxHint: string | undefined,
120
124
  cause: unknown,
121
125
  ) {
122
126
  super(message, { cause });
123
127
  this.kind = kind;
124
128
  this.partial = partial;
129
+ this.sandboxHint = sandboxHint;
125
130
  // 对齐标准错误分类:中断=AbortError(用户取消),超时=TimeoutError
126
131
  this.name = kind === "aborted" ? "AbortError" : "TimeoutError";
127
132
  }
@@ -255,6 +260,41 @@ function notifyMode(
255
260
  ctx.ui.notify(labels[mode], "info");
256
261
  }
257
262
 
263
+ /** 沙箱默认写边界:根只读、工作区可写、.git 只读。不展开用户配置的额外可写路径。 */
264
+ const SANDBOX_WRITE_RULES = "/ is read-only, ./ is writable, ./.git/ is read-only";
265
+
266
+ /** 网络层级:关 / 只放行白名单 / 完全放开(白名单域名本身不列出,对判断失败无用)。 */
267
+ function describeNetwork(resolved: ResolvedBwrap): string {
268
+ if (!resolved.network) return "network access is off";
269
+ if (resolved.networkAllowlist.length > 0)
270
+ return "network access is limited to allowlisted addresses";
271
+ return "network access is unrestricted";
272
+ }
273
+
274
+ /** 命令没经沙箱时沿用 prompt 里的说法,给出在沙盒外重跑的手段。 */
275
+ const SANDBOX_ESCAPE_HATCH =
276
+ "[Sandbox] If the command needs more than that, use the `dangerouslyDisableSandbox` parameter to request unsandboxed execution; the user must approve this request.";
277
+
278
+ /**
279
+ * 命令失败时作为独立信息块附上的沙箱状态:默认写边界 + 网络层级,以及在沙盒外重跑的手段。
280
+ * 写边界只说沙箱布局(不展开用户配置的可写路径),网络只说层级(不列白名单域名)。
281
+ * 命令没经沙箱(allow-all、审批通过的全权限、Windows)时返回 undefined:
282
+ * 没有沙箱就没什么可提示的。
283
+ */
284
+ export function describeSandbox(resolved: ResolvedBwrap, unsandboxed: boolean): string | undefined {
285
+ if (unsandboxed) return undefined;
286
+ const writes = resolved.mode === "readonly" ? "the filesystem is read-only" : SANDBOX_WRITE_RULES;
287
+ return [
288
+ `[Sandbox] This command ran in a sandbox: ${writes}; ${describeNetwork(resolved)}.`,
289
+ SANDBOX_ESCAPE_HATCH,
290
+ ].join("\n");
291
+ }
292
+
293
+ /** 失败结果里附加的沙箱状态块;未沙箱执行(hint 为 undefined)时没有这一块。 */
294
+ export function sandboxHintBlock(hint: string | undefined): { type: "text"; text: string }[] {
295
+ return hint === undefined ? [] : [{ type: "text", text: hint }];
296
+ }
297
+
258
298
  export class BwrapRuntime {
259
299
  private resolved: ResolvedBwrap | undefined;
260
300
  private sandboxDisabled = false;
@@ -383,6 +423,8 @@ export class BwrapRuntime {
383
423
  runtime.mihomoPath ??= this.mihomoPath ?? findMihomo();
384
424
  this.mihomoPath = runtime.mihomoPath;
385
425
  }
426
+ // 命令失败时附带的沙箱状态(写边界 + 网络层级),由上层拼进错误文本
427
+ const sandboxHint = describeSandbox(runtime, local);
386
428
  await using output = new BashOutput(request.ctx.sessionManager.getSessionId());
387
429
  const { onUpdate } = request;
388
430
 
@@ -415,6 +457,7 @@ export class BwrapRuntime {
415
457
  const partial = await this.finalizeOutput(output);
416
458
  return {
417
459
  exitCode,
460
+ sandboxHint,
418
461
  output: partial.output,
419
462
  ...(partial.fullOutputPath && { fullOutputPath: partial.fullOutputPath }),
420
463
  truncation: partial.truncation,
@@ -433,6 +476,7 @@ export class BwrapRuntime {
433
476
  "timeout",
434
477
  `Command timed out after ${error.message.slice("timeout:".length)} seconds`,
435
478
  partial,
479
+ sandboxHint,
436
480
  error,
437
481
  );
438
482
  }
@@ -440,7 +484,13 @@ export class BwrapRuntime {
440
484
  // 兼容 pi local ops 抛的 new Error("aborted")
441
485
  if (error instanceof Error && (error.name === "AbortError" || error.message === "aborted")) {
442
486
  const partial = await this.finalizeOutput(output);
443
- throw new BashInterruptedError("aborted", "Command aborted by user", partial, error);
487
+ throw new BashInterruptedError(
488
+ "aborted",
489
+ "Command aborted by user",
490
+ partial,
491
+ sandboxHint,
492
+ error,
493
+ );
444
494
  }
445
495
  throw error;
446
496
  } finally {
@@ -10,7 +10,12 @@ import {
10
10
  } from "@earendil-works/pi-coding-agent";
11
11
  import { Type } from "typebox";
12
12
 
13
- import { BashInterruptedError, type BwrapRuntime, createBwrapRuntime } from "../bwrap/runtime.js";
13
+ import {
14
+ BashInterruptedError,
15
+ type BwrapRuntime,
16
+ createBwrapRuntime,
17
+ sandboxHintBlock,
18
+ } from "../bwrap/runtime.js";
14
19
  import { resolveWorkdir } from "../lib/path.js";
15
20
 
16
21
  const DEFAULT_TIMEOUT_MS = 120_000;
@@ -154,20 +159,28 @@ export function registerShellTools(
154
159
  const full = text
155
160
  ? `${text}\n\nCommand timed out after ${timeout} milliseconds`
156
161
  : `Command timed out after ${timeout} milliseconds`;
157
- throw new Error(full, { cause: error });
162
+ return {
163
+ content: [{ type: "text", text: full }, ...sandboxHintBlock(error.sandboxHint)],
164
+ details: undefined,
165
+ };
158
166
  }
159
167
  throw error;
160
168
  }
161
169
 
162
- // 对齐 Claude Code:非 0 退出码视为错误(不做 grep/find 等命令语义化特判,
163
- // 任何非 0 都抛错);错误文本用完整输出(从落盘文件读取,必要时头尾截断)
170
+ // 对齐 Claude Code:非 0 退出码都算失败(不做 grep/find 等命令语义化特判)。
171
+ // 失败是命令的正常结果而不是异常:与成功一样 return,文本用完整输出
172
+ // (从落盘文件读取,必要时头尾截断),沙箱状态另行附一块
164
173
  if (result.exitCode !== 0 && result.exitCode !== null) {
165
174
  const full = result.fullOutputPath
166
175
  ? await readFile(result.fullOutputPath, "utf8")
167
176
  : result.output;
168
- throw new Error(formatBashError(result.exitCode, full), {
169
- cause: result,
170
- });
177
+ return {
178
+ content: [
179
+ { type: "text", text: formatBashError(result.exitCode, full) },
180
+ ...sandboxHintBlock(result.sandboxHint),
181
+ ],
182
+ details: undefined,
183
+ };
171
184
  }
172
185
  return formatBashSuccess(result);
173
186
  },
@@ -4,7 +4,12 @@ import { fileURLToPath } from "node:url";
4
4
  import type { ExtensionAPI, TruncationResult } from "@earendil-works/pi-coding-agent";
5
5
  import { Type } from "typebox";
6
6
 
7
- import { BashInterruptedError, type BwrapRuntime, createBwrapRuntime } from "../bwrap/runtime.js";
7
+ import {
8
+ BashInterruptedError,
9
+ type BwrapRuntime,
10
+ createBwrapRuntime,
11
+ sandboxHintBlock,
12
+ } from "../bwrap/runtime.js";
8
13
  import { resolveWorkdir } from "../lib/path.js";
9
14
 
10
15
  const DEFAULT_TIMEOUT_MS = 120_000;
@@ -111,9 +116,13 @@ export default function opencodeBash(
111
116
  ? `Command exceeded timeout of ${timeout} ms. Retry with a larger timeout if the command is expected to take longer.`
112
117
  : "Command aborted by user";
113
118
  const full = text ? `${text}\n\n${status}` : status;
114
- // 对齐上游 opencode:超时与中断都不抛错,输出与状态文本一起返回
119
+ // 对齐上游 opencode:超时与中断都不抛错,输出与状态文本一起返回;
120
+ // 超时可能是沙箱的网络限制导致的,附加沙箱状态(用户中断与沙箱无关)
115
121
  return {
116
- content: [{ type: "text" as const, text: full }],
122
+ content: [
123
+ { type: "text" as const, text: full },
124
+ ...sandboxHintBlock(error.kind === "timeout" ? error.sandboxHint : undefined),
125
+ ],
117
126
  details: error.kind === "timeout" ? { timeout: true } : {},
118
127
  };
119
128
  }
@@ -123,10 +132,13 @@ export default function opencodeBash(
123
132
  // 命令失败(非 0 退出码)不抛错:输出与状态文本一起返回
124
133
  let text = result.output || "(no output)";
125
134
  text = appendTruncationNotice(text, result.truncation, result.fullOutputPath);
135
+ const failed = result.exitCode !== 0 && result.exitCode !== null;
126
136
  return {
127
137
  content: [
128
138
  { type: "text" as const, text },
129
139
  { type: "text" as const, text: `Command exited with code ${result.exitCode}.` },
140
+ // 失败可能是被沙箱的写边界或网络限制挡住的,附一块沙箱状态
141
+ ...sandboxHintBlock(failed ? result.sandboxHint : undefined),
130
142
  ],
131
143
  details: {
132
144
  exitCode: result.exitCode,