@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 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 列表输出对应的任务(pendant 面板自动展开)
254
+ - **渲染**:每次调用用完整 markdown 列表输出对应的任务
255
255
 
256
256
  与 pi 内置 `todo` 工具(`create`/`update`/`list`/… 单条动作)不同,本工具没有单条增删改动作,模型必须每次都传完整列表。
257
257
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.582",
3
+ "version": "0.1.585",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
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,
@@ -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
- * 用户在审批框点 Deny 完全一致——模型看到的是一次普通拒绝,而不是另一套错误语义。
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: ${writes}; ${describeNetwork(resolved)}.`,
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 --no-bwrap or the bwrap-allow-all command.
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
- : isWindows
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 pass --no-bwrap to disable the sandbox explicitly.",
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
- await this.approveFullAccess(request.ctx, request.command, request.description, execCwd);
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
- const local = isWindows || needsApproval || !runtime.bwrapEnabled;
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
- const sandboxHint = describeSandbox(runtime, local);
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" | "hasUI">): ResolvedBwrap {
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<void> {
585
- const policy = resolveEscalation({ hasUI: ctx.hasUI });
586
- if (policy.kind === "deny") throw new Error(policy.reason);
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 ALLOW_ONCE: {
596
- if (foreverApprovedPattern.length > 0) {
597
- await this.persistAllowRule(ctx, command, foreverApprovedPattern);
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 / Deny 系列中做出
692
- // 选择。勾选的规则在放行时持久化;Deny 系列同样持久化(用户确认该模式可信,
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 =
@@ -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
- const resolved =
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
  /**
@@ -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: [{ type: "text", text: text ? `${text}\n\n${status}` : status }],
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: [{ type: "text", text: full }, ...sandboxHintBlock(error.sandboxHint)],
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
  };
@@ -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,