@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 +1 -1
- package/src/bwrap/runtime.ts +51 -1
- package/src/claude-code/shell.ts +20 -7
- package/src/opencode/bash.ts +15 -3
package/package.json
CHANGED
package/src/bwrap/runtime.ts
CHANGED
|
@@ -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(
|
|
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 {
|
package/src/claude-code/shell.ts
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
} from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { Type } from "typebox";
|
|
12
12
|
|
|
13
|
-
import {
|
|
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
|
-
|
|
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
|
|
163
|
-
//
|
|
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
|
-
|
|
169
|
-
|
|
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
|
},
|
package/src/opencode/bash.ts
CHANGED
|
@@ -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 {
|
|
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: [
|
|
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,
|