@trim21/personal-pi-extensions 0.1.582 → 0.1.585
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/README.md +1 -1
- package/package.json +1 -1
- package/src/bwrap/core.ts +0 -11
- package/src/bwrap/runtime.ts +85 -53
- package/src/bwrap/sandbox.ts +1 -6
- package/src/claude-code/shell.ts +11 -3
- package/src/opencode/bash.ts +2 -0
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
|
|
254
|
+
- **渲染**:每次调用用完整 markdown 列表输出对应的任务
|
|
255
255
|
|
|
256
256
|
与 pi 内置 `todo` 工具(`create`/`update`/`list`/… 单条动作)不同,本工具没有单条增删改动作,模型必须每次都传完整列表。
|
|
257
257
|
|
package/package.json
CHANGED
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,
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -31,26 +31,14 @@ 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";
|
|
40
|
+
/** 拒绝提权、命令降级为在沙盒内执行。 */
|
|
41
|
+
export const RUN_IN_SANDBOX = "Run this in sandbox";
|
|
54
42
|
export const DENY = "Deny";
|
|
55
43
|
export const DENY_WITH_REASON = "Deny with reason";
|
|
56
44
|
/** 第一层的折叠入口:进入按 pattern 勾选持久化规则的子菜单。 */
|
|
@@ -63,7 +51,7 @@ export const BACK = "Back";
|
|
|
63
51
|
* 决定放行、拒绝并持久化勾选的规则,UI 层不直接产生副作用。
|
|
64
52
|
*/
|
|
65
53
|
export interface FullAccessUIDecision {
|
|
66
|
-
/** 用户选择的动作 label(ALLOW_ONCE / DENY / DENY_WITH_REASON)。 */
|
|
54
|
+
/** 用户选择的动作 label(ALLOW_ONCE / RUN_IN_SANDBOX / DENY / DENY_WITH_REASON)。 */
|
|
67
55
|
result: string;
|
|
68
56
|
/** 用户勾选、需持久化为 allow 规则的 pattern;未勾选时为空数组。 */
|
|
69
57
|
foreverApprovedPattern: string[];
|
|
@@ -71,6 +59,12 @@ export interface FullAccessUIDecision {
|
|
|
71
59
|
reason?: string;
|
|
72
60
|
}
|
|
73
61
|
|
|
62
|
+
/**
|
|
63
|
+
* 全权限审批的放行结果:允许一次性全权限执行,或用户拒绝提权、
|
|
64
|
+
* 命令降级为在沙盒内执行(输出后附系统提醒)。
|
|
65
|
+
*/
|
|
66
|
+
type FullAccessGrant = "full-access" | "sandbox";
|
|
67
|
+
|
|
74
68
|
export interface BwrapExecutionRequest {
|
|
75
69
|
toolCallId: string;
|
|
76
70
|
command: string;
|
|
@@ -92,6 +86,8 @@ export interface BwrapExecutionRequest {
|
|
|
92
86
|
*/
|
|
93
87
|
export interface BwrapExecutionResult {
|
|
94
88
|
exitCode: number | null;
|
|
89
|
+
/** 用户选「Run this in sandbox」拒绝提权时的系统提醒,成功与失败都由上层附上;其余情况缺省。 */
|
|
90
|
+
sandboxReminder?: string;
|
|
95
91
|
/** 沙箱状态说明(写边界 + 网络层级),失败时由上层工具作为独立信息块附上;未沙箱执行时为 undefined。 */
|
|
96
92
|
sandboxHint: string | undefined;
|
|
97
93
|
/** 截断后的输出(尾部),未截断时为完整输出;空输出为空字符串。 */
|
|
@@ -116,6 +112,8 @@ export class BashInterruptedError extends Error {
|
|
|
116
112
|
readonly kind: "timeout" | "aborted";
|
|
117
113
|
readonly partial: BashExecutionPartial;
|
|
118
114
|
readonly sandboxHint: string | undefined;
|
|
115
|
+
/** 用户选「Run this in sandbox」拒绝提权时的系统提醒;其余情况为 undefined。 */
|
|
116
|
+
readonly sandboxReminder: string | undefined;
|
|
119
117
|
/**
|
|
120
118
|
* 命令实际运行时长(毫秒):从审批结束、命令真正开始执行算到终止,
|
|
121
119
|
* 不含审批弹窗等用户 UI 交互耗时。
|
|
@@ -129,11 +127,13 @@ export class BashInterruptedError extends Error {
|
|
|
129
127
|
sandboxHint: string | undefined,
|
|
130
128
|
elapsedMs: number,
|
|
131
129
|
cause: unknown,
|
|
130
|
+
sandboxReminder?: string,
|
|
132
131
|
) {
|
|
133
132
|
super(message, { cause });
|
|
134
133
|
this.kind = kind;
|
|
135
134
|
this.partial = partial;
|
|
136
135
|
this.sandboxHint = sandboxHint;
|
|
136
|
+
this.sandboxReminder = sandboxReminder;
|
|
137
137
|
this.elapsedMs = elapsedMs;
|
|
138
138
|
// 对齐标准错误分类:中断=AbortError(用户取消),超时=TimeoutError
|
|
139
139
|
this.name = kind === "aborted" ? "AbortError" : "TimeoutError";
|
|
@@ -284,9 +284,16 @@ function describeNetwork(resolved: ResolvedBwrap): string {
|
|
|
284
284
|
return "network access is unrestricted";
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
/** 沙箱作用域一句话:写边界 + 网络层级(沙箱状态块与沙盒内执行提醒共用)。 */
|
|
288
|
+
function describeLimits(resolved: ResolvedBwrap): string {
|
|
289
|
+
const writes = resolved.mode === "readonly" ? "the filesystem is read-only" : SANDBOX_WRITE_RULES;
|
|
290
|
+
return `${writes}; ${describeNetwork(resolved)}`;
|
|
291
|
+
}
|
|
292
|
+
|
|
287
293
|
/**
|
|
288
|
-
* 用户无理由拒绝非沙盒请求的文案。`/bwrap-deny-request`
|
|
289
|
-
*
|
|
294
|
+
* 用户无理由拒绝非沙盒请求的文案。`/bwrap-deny-request` 生效时与无 UI 会话里
|
|
295
|
+
* 需要审批的请求,都必须与用户在审批框点 Deny 完全一致——模型看到的是一次
|
|
296
|
+
* 普通拒绝,而不是另一套错误语义。
|
|
290
297
|
*/
|
|
291
298
|
const UNSANDBOXED_DENIED = "User denied unsandboxed execution.";
|
|
292
299
|
|
|
@@ -303,15 +310,28 @@ const SANDBOX_ESCAPE_HATCH =
|
|
|
303
310
|
*/
|
|
304
311
|
export function describeSandbox(resolved: ResolvedBwrap, unsandboxed: boolean): string | undefined {
|
|
305
312
|
if (unsandboxed) return undefined;
|
|
306
|
-
const writes = resolved.mode === "readonly" ? "the filesystem is read-only" : SANDBOX_WRITE_RULES;
|
|
307
313
|
return [
|
|
308
314
|
"<system-reminder>",
|
|
309
|
-
`This command ran in a sandbox: ${
|
|
315
|
+
`This command ran in a sandbox: ${describeLimits(resolved)}.`,
|
|
310
316
|
SANDBOX_ESCAPE_HATCH,
|
|
311
317
|
"</system-reminder>",
|
|
312
318
|
].join("\n");
|
|
313
319
|
}
|
|
314
320
|
|
|
321
|
+
/**
|
|
322
|
+
* 用户在审批框选「Run this in sandbox」拒绝提权后,命令输出后附带的系统提醒:
|
|
323
|
+
* 说明命令在沙盒内执行及其作用域,成功与失败都附上。此时 describeSandbox 的
|
|
324
|
+
* 状态块不再附——提权请求刚被用户拒绝,再提示 dangerouslyDisableSandbox 只会
|
|
325
|
+
* 诱导重复请求,作用域描述由本提醒承担。
|
|
326
|
+
*/
|
|
327
|
+
export function describeSandboxChoice(resolved: ResolvedBwrap): string {
|
|
328
|
+
return [
|
|
329
|
+
"<system-reminder>",
|
|
330
|
+
`The user ran this command in the sandbox instead of approving unsandboxed execution: ${describeLimits(resolved)}.`,
|
|
331
|
+
"</system-reminder>",
|
|
332
|
+
].join("\n");
|
|
333
|
+
}
|
|
334
|
+
|
|
315
335
|
/** 失败结果里附加的沙箱状态块;未沙箱执行(hint 为 undefined)时没有这一块。 */
|
|
316
336
|
export function sandboxHintBlock(hint: string | undefined): { type: "text"; text: string }[] {
|
|
317
337
|
return hint === undefined ? [] : [{ type: "text", text: hint }];
|
|
@@ -319,7 +339,6 @@ export function sandboxHintBlock(hint: string | undefined): { type: "text"; text
|
|
|
319
339
|
|
|
320
340
|
export class BwrapRuntime {
|
|
321
341
|
private resolved: ResolvedBwrap | undefined;
|
|
322
|
-
private sandboxDisabled = false;
|
|
323
342
|
private bwrapUnavailable = false;
|
|
324
343
|
/** net-allowlist 首次执行时解析一次 mihomo 路径,之后随 runtime 复用,不逐命令扫描 PATH。 */
|
|
325
344
|
private mihomoPath: string | undefined;
|
|
@@ -334,14 +353,7 @@ export class BwrapRuntime {
|
|
|
334
353
|
}
|
|
335
354
|
|
|
336
355
|
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
356
|
pi.on("session_start", (_event, ctx) => {
|
|
344
|
-
this.sandboxDisabled = pi.getFlag("no-bwrap") === true && ctx.hasUI;
|
|
345
357
|
this.resolved = undefined;
|
|
346
358
|
this.bwrapUnavailable = false;
|
|
347
359
|
this.policy.setDenyRequests(false);
|
|
@@ -358,7 +370,7 @@ export class BwrapRuntime {
|
|
|
358
370
|
} catch (error) {
|
|
359
371
|
// Fail closed: a missing bwrap binary must not silently degrade to an
|
|
360
372
|
// unsandboxed allow-all session. Commands are refused until the user
|
|
361
|
-
// explicitly opts out via
|
|
373
|
+
// explicitly opts out via the bwrap-allow-all command.
|
|
362
374
|
this.bwrapUnavailable = true;
|
|
363
375
|
this.resolved = undefined;
|
|
364
376
|
ctx.ui.setStatus("bwrap", ctx.ui.theme.fg("error", "bwrap: unavailable"));
|
|
@@ -386,9 +398,7 @@ export class BwrapRuntime {
|
|
|
386
398
|
? isWindows
|
|
387
399
|
? "Every bash command requires user approval before it runs."
|
|
388
400
|
: `Current bwrap mode: **${runtime.mode}**. The bwrap runtime selects sandboxing and, when requested, user approval for unsandboxed execution.`
|
|
389
|
-
:
|
|
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.";
|
|
401
|
+
: `Current bwrap mode: **${runtime.mode}**. This headless session cannot ask for approval: commands that require user approval are denied.`;
|
|
392
402
|
const unavailableText =
|
|
393
403
|
!isWindows && this.bwrapUnavailable
|
|
394
404
|
? " bwrap is unavailable (binary not found): bash commands are refused unless the user explicitly approves unsandboxed execution."
|
|
@@ -409,13 +419,11 @@ export class BwrapRuntime {
|
|
|
409
419
|
|
|
410
420
|
setMode(cwd: string, mode: BwrapMode): ResolvedBwrap {
|
|
411
421
|
this.resolved = loadSandboxConfig({ workspace: cwd, mode });
|
|
412
|
-
this.sandboxDisabled = false;
|
|
413
422
|
return this.resolved;
|
|
414
423
|
}
|
|
415
424
|
|
|
416
425
|
reset(): void {
|
|
417
426
|
this.resolved = undefined;
|
|
418
|
-
this.sandboxDisabled = false;
|
|
419
427
|
this.bwrapUnavailable = false;
|
|
420
428
|
}
|
|
421
429
|
|
|
@@ -432,7 +440,7 @@ export class BwrapRuntime {
|
|
|
432
440
|
) {
|
|
433
441
|
throw new Error(
|
|
434
442
|
"bwrap (bubblewrap) not found; refusing to execute commands without sandboxing. " +
|
|
435
|
-
"Install bubblewrap and restart the session, or
|
|
443
|
+
"Install bubblewrap and restart the session, or disable the sandbox explicitly with /bwrap-allow-all.",
|
|
436
444
|
);
|
|
437
445
|
}
|
|
438
446
|
// workspace 恒为 session 工作区;cwd 只是本次命令的进程执行目录,
|
|
@@ -442,6 +450,8 @@ export class BwrapRuntime {
|
|
|
442
450
|
// 需要人工审批:非 Windows 仅 requestFullAccess;Windows 上默认所有命令
|
|
443
451
|
// (allow-all 模式是显式 opt-out,仍直接执行)。
|
|
444
452
|
const needsApproval = request.requestFullAccess === true || (isWindows && runtime.bwrapEnabled);
|
|
453
|
+
// 用户在审批框选「Run this in sandbox」拒绝提权:命令降级为沙盒内执行
|
|
454
|
+
let userChoseSandbox = false;
|
|
445
455
|
if (needsApproval && runtime.bwrapEnabled) {
|
|
446
456
|
// /bwrap-deny-request:非沙盒请求直接拒绝——审批规则与审批框都不再参与,
|
|
447
457
|
// 拒绝文案与用户点 Deny 相同,直到用户用 /bwrap-allow-request 恢复审批。
|
|
@@ -454,19 +464,29 @@ export class BwrapRuntime {
|
|
|
454
464
|
throw new Error(`Command denied by bwrap approval rule: ${request.command}`);
|
|
455
465
|
}
|
|
456
466
|
if (decision === undefined) {
|
|
457
|
-
|
|
467
|
+
userChoseSandbox =
|
|
468
|
+
(await this.approveFullAccess(
|
|
469
|
+
request.ctx,
|
|
470
|
+
request.command,
|
|
471
|
+
request.description,
|
|
472
|
+
execCwd,
|
|
473
|
+
)) === "sandbox";
|
|
458
474
|
}
|
|
459
475
|
}
|
|
460
|
-
// 不经沙箱的三种情形:Windows(无 bubblewrap)、审批通过的全权限、allow-all
|
|
461
|
-
|
|
476
|
+
// 不经沙箱的三种情形:Windows(无 bubblewrap)、审批通过的全权限、allow-all 模式;
|
|
477
|
+
// 例外是用户选「Run this in sandbox」:提权被拒,命令照常进沙箱
|
|
478
|
+
const local = !userChoseSandbox && (isWindows || needsApproval || !runtime.bwrapEnabled);
|
|
462
479
|
// mihomoPath 是 ResolvedBwrap 的 override 语义:首次 net-allowlist 执行时解析
|
|
463
480
|
// 一次存入私有字段,之后写回 resolved 直达 createNetworkStack,不逐命令扫描 PATH
|
|
464
481
|
if (runtime.network && runtime.networkAllowlist.length > 0) {
|
|
465
482
|
runtime.mihomoPath ??= this.mihomoPath ?? findMihomo();
|
|
466
483
|
this.mihomoPath = runtime.mihomoPath;
|
|
467
484
|
}
|
|
468
|
-
// 命令失败时附带的沙箱状态(写边界 +
|
|
469
|
-
|
|
485
|
+
// 命令失败时附带的沙箱状态(写边界 + 网络层级),由上层拼进错误文本。
|
|
486
|
+
// 用户选「Run this in sandbox」时不附:沙箱作用域改由 sandboxReminder 说明
|
|
487
|
+
const sandboxHint = userChoseSandbox ? undefined : describeSandbox(runtime, local);
|
|
488
|
+
// 提权被拒、命令在沙盒内执行的系统提醒:成功与失败都由上层附上
|
|
489
|
+
const sandboxReminder = userChoseSandbox ? describeSandboxChoice(runtime) : undefined;
|
|
470
490
|
// 计时起点放在审批之后:审批弹窗的等待时长属于用户 UI 操作,不是命令运行时间
|
|
471
491
|
const startedAt = Date.now();
|
|
472
492
|
await using output = new BashOutput(request.ctx.sessionManager.getSessionId());
|
|
@@ -502,6 +522,7 @@ export class BwrapRuntime {
|
|
|
502
522
|
return {
|
|
503
523
|
exitCode,
|
|
504
524
|
sandboxHint,
|
|
525
|
+
...(sandboxReminder && { sandboxReminder }),
|
|
505
526
|
output: partial.output,
|
|
506
527
|
...(partial.fullOutputPath && { fullOutputPath: partial.fullOutputPath }),
|
|
507
528
|
truncation: partial.truncation,
|
|
@@ -524,6 +545,7 @@ export class BwrapRuntime {
|
|
|
524
545
|
sandboxHint,
|
|
525
546
|
elapsedMs,
|
|
526
547
|
error,
|
|
548
|
+
sandboxReminder,
|
|
527
549
|
);
|
|
528
550
|
}
|
|
529
551
|
// 中断识别:优先 name=AbortError(throwIfAborted/signal.reason),
|
|
@@ -537,6 +559,7 @@ export class BwrapRuntime {
|
|
|
537
559
|
sandboxHint,
|
|
538
560
|
elapsedMs,
|
|
539
561
|
error,
|
|
562
|
+
sandboxReminder,
|
|
540
563
|
);
|
|
541
564
|
}
|
|
542
565
|
throw error;
|
|
@@ -568,10 +591,8 @@ export class BwrapRuntime {
|
|
|
568
591
|
};
|
|
569
592
|
}
|
|
570
593
|
|
|
571
|
-
private resolve(ctx: Pick<ExtensionContext, "cwd"
|
|
594
|
+
private resolve(ctx: Pick<ExtensionContext, "cwd">): ResolvedBwrap {
|
|
572
595
|
const config = loadBwrapConfig(ctx.cwd);
|
|
573
|
-
if (!ctx.hasUI) return resolveHeadlessBwrap(config);
|
|
574
|
-
if (this.sandboxDisabled) return resolveBwrap({ ...config, mode: "allow-all" });
|
|
575
596
|
if (!this.resolved) this.resolved = resolveBwrap(config);
|
|
576
597
|
return this.resolved;
|
|
577
598
|
}
|
|
@@ -581,9 +602,9 @@ export class BwrapRuntime {
|
|
|
581
602
|
command: string,
|
|
582
603
|
reason: string | undefined,
|
|
583
604
|
execCwd: string,
|
|
584
|
-
): Promise<
|
|
585
|
-
|
|
586
|
-
if (
|
|
605
|
+
): Promise<FullAccessGrant> {
|
|
606
|
+
// hasUI 判定推迟到审批时刻:无 UI 会话弹不了审批框,按用户点 Deny 的标准文案拒绝
|
|
607
|
+
if (!ctx.hasUI) throw new Error(UNSANDBOXED_DENIED);
|
|
587
608
|
const decision = await this.approveFullAccessUI(ctx, command, reason, execCwd);
|
|
588
609
|
// 关闭对话框 = 中断并拒绝,不循环重问
|
|
589
610
|
if (decision === undefined) {
|
|
@@ -592,11 +613,10 @@ export class BwrapRuntime {
|
|
|
592
613
|
}
|
|
593
614
|
const { result, foreverApprovedPattern } = decision;
|
|
594
615
|
switch (result) {
|
|
595
|
-
case
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
return;
|
|
616
|
+
case RUN_IN_SANDBOX: {
|
|
617
|
+
// 用户拒绝提权、改为沙盒内执行:allow 规则意味着自动放行非沙盒执行,
|
|
618
|
+
// 与该选择矛盾,即使在子菜单勾选了 pattern 也不持久化
|
|
619
|
+
return "sandbox";
|
|
600
620
|
}
|
|
601
621
|
case DENY: {
|
|
602
622
|
if (foreverApprovedPattern.length > 0) {
|
|
@@ -615,6 +635,16 @@ export class BwrapRuntime {
|
|
|
615
635
|
: "User denied command execution.",
|
|
616
636
|
);
|
|
617
637
|
}
|
|
638
|
+
case ALLOW_ONCE: {
|
|
639
|
+
if (foreverApprovedPattern.length > 0) {
|
|
640
|
+
await this.persistAllowRule(ctx, command, foreverApprovedPattern);
|
|
641
|
+
}
|
|
642
|
+
return "full-access";
|
|
643
|
+
}
|
|
644
|
+
default: {
|
|
645
|
+
// UI 层只会产出上面几种 label;未识别的按历史行为放行,不持久化规则
|
|
646
|
+
return "full-access";
|
|
647
|
+
}
|
|
618
648
|
}
|
|
619
649
|
}
|
|
620
650
|
|
|
@@ -674,6 +704,7 @@ export class BwrapRuntime {
|
|
|
674
704
|
// 追加折叠入口,进入子菜单逐项勾选。
|
|
675
705
|
const actions: SelectAction[] = [
|
|
676
706
|
{ label: ALLOW_ONCE },
|
|
707
|
+
{ label: RUN_IN_SANDBOX },
|
|
677
708
|
{ label: DENY },
|
|
678
709
|
{ label: DENY_WITH_REASON, inputPrompt: "Why was this denied?" },
|
|
679
710
|
];
|
|
@@ -688,9 +719,10 @@ export class BwrapRuntime {
|
|
|
688
719
|
description,
|
|
689
720
|
].join("\n");
|
|
690
721
|
|
|
691
|
-
// 两层循环:子菜单勾选后回到主决策,直到用户在 Allow once /
|
|
692
|
-
//
|
|
693
|
-
//
|
|
722
|
+
// 两层循环:子菜单勾选后回到主决策,直到用户在 Allow once / Run this in sandbox /
|
|
723
|
+
// Deny 系列中做出选择。勾选的规则在放行时持久化;Deny 系列同样持久化(用户确认
|
|
724
|
+
// 该模式可信,只是本次命令不执行);Run this in sandbox 不持久化。
|
|
725
|
+
// 子菜单关闭 = 返回主决策;主决策关闭 = 取消(上层按拒绝处理)。
|
|
694
726
|
let selected: string[] = [];
|
|
695
727
|
for (;;) {
|
|
696
728
|
const pending =
|
package/src/bwrap/sandbox.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
/**
|
package/src/claude-code/shell.ts
CHANGED
|
@@ -75,7 +75,7 @@ export function formatBashSuccess(result: Awaited<ReturnType<BwrapRuntime["execu
|
|
|
75
75
|
const { output, truncation, fullOutputPath } = result;
|
|
76
76
|
const text = appendTruncationNotice(output || "(no output)", truncation, fullOutputPath);
|
|
77
77
|
return {
|
|
78
|
-
content: [{ type: "text", text }],
|
|
78
|
+
content: [{ type: "text", text }, ...sandboxHintBlock(result.sandboxReminder)],
|
|
79
79
|
details: fullOutputPath && truncation.truncated ? { truncation, fullOutputPath } : undefined,
|
|
80
80
|
};
|
|
81
81
|
}
|
|
@@ -158,7 +158,10 @@ export function registerShellTools(
|
|
|
158
158
|
// 不含审批弹窗等 UI 交互
|
|
159
159
|
const status = `Command aborted by user after ${formatElapsedSeconds(error.elapsedMs)}`;
|
|
160
160
|
return {
|
|
161
|
-
content: [
|
|
161
|
+
content: [
|
|
162
|
+
{ type: "text", text: text ? `${text}\n\n${status}` : status },
|
|
163
|
+
...sandboxHintBlock(error.sandboxReminder),
|
|
164
|
+
],
|
|
162
165
|
details: undefined,
|
|
163
166
|
};
|
|
164
167
|
}
|
|
@@ -166,7 +169,11 @@ export function registerShellTools(
|
|
|
166
169
|
? `${text}\n\nCommand timed out after ${timeout} milliseconds`
|
|
167
170
|
: `Command timed out after ${timeout} milliseconds`;
|
|
168
171
|
return {
|
|
169
|
-
content: [
|
|
172
|
+
content: [
|
|
173
|
+
{ type: "text", text: full },
|
|
174
|
+
...sandboxHintBlock(error.sandboxHint),
|
|
175
|
+
...sandboxHintBlock(error.sandboxReminder),
|
|
176
|
+
],
|
|
170
177
|
details: undefined,
|
|
171
178
|
};
|
|
172
179
|
}
|
|
@@ -184,6 +191,7 @@ export function registerShellTools(
|
|
|
184
191
|
content: [
|
|
185
192
|
{ type: "text", text: formatBashError(result.exitCode, full) },
|
|
186
193
|
...sandboxHintBlock(result.sandboxHint),
|
|
194
|
+
...sandboxHintBlock(result.sandboxReminder),
|
|
187
195
|
],
|
|
188
196
|
details: undefined,
|
|
189
197
|
};
|
package/src/opencode/bash.ts
CHANGED
|
@@ -124,6 +124,7 @@ export default function opencodeBash(
|
|
|
124
124
|
content: [
|
|
125
125
|
{ type: "text" as const, text: full },
|
|
126
126
|
...sandboxHintBlock(error.kind === "timeout" ? error.sandboxHint : undefined),
|
|
127
|
+
...sandboxHintBlock(error.sandboxReminder),
|
|
127
128
|
],
|
|
128
129
|
details: error.kind === "timeout" ? { timeout: true } : {},
|
|
129
130
|
};
|
|
@@ -141,6 +142,7 @@ export default function opencodeBash(
|
|
|
141
142
|
{ type: "text" as const, text: `Command exited with code ${result.exitCode}.` },
|
|
142
143
|
// 失败可能是被沙箱的写边界或网络限制挡住的,附一块沙箱状态
|
|
143
144
|
...sandboxHintBlock(failed ? result.sandboxHint : undefined),
|
|
145
|
+
...sandboxHintBlock(result.sandboxReminder),
|
|
144
146
|
],
|
|
145
147
|
details: {
|
|
146
148
|
exitCode: result.exitCode,
|