@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 +1 -1
- package/src/bwrap/runtime.ts +66 -18
- package/src/gh-readonly.ts +9 -0
- package/src/lib/write-guard.ts +10 -0
- package/src/spawn-agent.ts +10 -0
package/package.json
CHANGED
package/src/bwrap/runtime.ts
CHANGED
|
@@ -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
|
-
?
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
304
|
-
?
|
|
305
|
-
:
|
|
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
|
-
|
|
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(
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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)) {
|
package/src/gh-readonly.ts
CHANGED
|
@@ -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.
|
package/src/lib/write-guard.ts
CHANGED
|
@@ -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
|
}
|
package/src/spawn-agent.ts
CHANGED
|
@@ -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
|