@trim21/personal-pi-extensions 0.1.562 → 0.1.564

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.562",
3
+ "version": "0.1.564",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -24,8 +24,8 @@
24
24
  "format": "prettier --write .",
25
25
  "test": "vitest run",
26
26
  "test:coverage": "vitest run --coverage",
27
- "sandbox": "tsx bin/sandbox.ts",
28
- "aft-search": "tsx bin/aft-search.ts",
27
+ "sandbox": "jiti bin/sandbox.ts",
28
+ "aft-search": "jiti bin/aft-search.ts",
29
29
  "build:holder": "esbuild src/bwrap/holder.ts --format=esm --target=node24 --outfile=src/bwrap/holder.js",
30
30
  "prepare": "husky"
31
31
  },
@@ -56,10 +56,10 @@
56
56
  "eslint-plugin-unicorn": "74.0.0",
57
57
  "eslint-plugin-unused-imports": "4.4.1",
58
58
  "husky": "^9.1.7",
59
+ "jiti": "^2.7.0",
59
60
  "lint-staged": "^17.3.0",
60
61
  "prettier": "^3.9.6",
61
62
  "shlex": "^3.0.0",
62
- "tsx": "^4.23.12",
63
63
  "typebox": "1.3.1",
64
64
  "typescript": "npm:@typescript/typescript6@^6.0.2",
65
65
  "typescript-eslint": "^8.67.0",
@@ -18,7 +18,7 @@ import { type TObject, Type } from "typebox";
18
18
  import { type CommandSpec, parseCommand } from "../lib/cli.js";
19
19
  import { fenceCodeBlock } from "../lib/markdown.js";
20
20
  import { formatDisplayPath } from "../lib/path.js";
21
- import { requestPolicy } from "../lib/request-policy.js";
21
+ import { createRequestPolicy, type RequestPolicy } from "../lib/request-policy.js";
22
22
  import { type SelectAction, selectMultiple, selectWithOptionalInput } from "../lib/ui.js";
23
23
  import { type ApprovalRule, evaluateBashApproval, matchRule } from "./approval-rules.js";
24
24
  import { commandPatternsFor } from "./approval-suggest.js";
@@ -320,6 +320,15 @@ export class BwrapRuntime {
320
320
  private bwrapUnavailable = false;
321
321
  /** net-allowlist 首次执行时解析一次 mihomo 路径,之后随 runtime 复用,不逐命令扫描 PATH。 */
322
322
  private mihomoPath: string | undefined;
323
+ /**
324
+ * 非沙盒请求策略由创建方注入:聚合入口把它与文件工具的 write-guard 共享同一实例,
325
+ * 独立入口各持一份、经 pi.events 同步。
326
+ */
327
+ private readonly policy: RequestPolicy;
328
+
329
+ constructor(policy: RequestPolicy) {
330
+ this.policy = policy;
331
+ }
323
332
 
324
333
  setup(pi: ExtensionAPI): void {
325
334
  pi.registerFlag("no-bwrap", {
@@ -332,7 +341,7 @@ export class BwrapRuntime {
332
341
  this.sandboxDisabled = pi.getFlag("no-bwrap") === true && ctx.hasUI;
333
342
  this.resolved = undefined;
334
343
  this.bwrapUnavailable = false;
335
- requestPolicy.setDenyRequests(false);
344
+ this.policy.setDenyRequests(false);
336
345
  if (process.platform === "win32") {
337
346
  // Windows 没有 bubblewrap:不做 bwrap 检测、不显示 bwrap 状态,
338
347
  // 每条 bash 命令在 execute 时逐条人工审批,模型无需知道 bwrap 的存在。
@@ -382,7 +391,7 @@ export class BwrapRuntime {
382
391
  ? " bwrap is unavailable (binary not found): bash commands are refused unless the user explicitly approves unsandboxed execution."
383
392
  : "";
384
393
  const denyRequestsText =
385
- !isWindows && requestPolicy.deniesRequests()
394
+ !isWindows && this.policy.deniesRequests()
386
395
  ? " Unsandboxed execution is currently denied by the user: `dangerouslyDisableSandbox` and writes outside the workspace are refused without approval."
387
396
  : "";
388
397
  return {
@@ -433,7 +442,7 @@ export class BwrapRuntime {
433
442
  if (needsApproval && runtime.bwrapEnabled) {
434
443
  // /bwrap-deny-request:非沙盒请求直接拒绝——审批规则与审批框都不再参与,
435
444
  // 拒绝文案与用户点 Deny 相同,直到用户用 /bwrap-allow-request 恢复审批。
436
- if (request.requestFullAccess === true && requestPolicy.deniesRequests()) {
445
+ if (request.requestFullAccess === true && this.policy.deniesRequests()) {
437
446
  throw new Error(UNSANDBOXED_DENIED);
438
447
  }
439
448
  // 先按 approvalRules 自动判定:allow 直接放行,deny 直接拒绝,未命中才弹框
@@ -879,7 +888,7 @@ export class BwrapRuntime {
879
888
 
880
889
  /** 状态行文案:模式 + 非沙盒请求策略(拒绝时标注,便于解释模型的请求为何被拒)。 */
881
890
  private statusLabel(mode: BwrapMode): string {
882
- return `bwrap: ${mode}${requestPolicy.deniesRequests() ? " (requests denied)" : ""}`;
891
+ return `bwrap: ${mode}${this.policy.deniesRequests() ? " (requests denied)" : ""}`;
883
892
  }
884
893
 
885
894
  /**
@@ -887,7 +896,7 @@ export class BwrapRuntime {
887
896
  * 编辑类工具的工作区外写入)直接按用户拒绝处理,不再弹审批框。
888
897
  */
889
898
  private setDenyRequests(deny: boolean, ctx: ExtensionCommandContext): void {
890
- requestPolicy.setDenyRequests(deny);
899
+ this.policy.setDenyRequests(deny);
891
900
  ctx.ui.notify(
892
901
  deny
893
902
  ? "Non-sandbox requests are denied without approval; /bwrap-allow-request restores approval."
@@ -916,6 +925,7 @@ export class BwrapRuntime {
916
925
  }
917
926
  }
918
927
 
919
- export function createBwrapRuntime(): BwrapRuntime {
920
- return new BwrapRuntime();
928
+ /** 缺省自建一份不跨入口同步的策略;入口通常显式传入 `createRequestPolicy(pi.events)`。 */
929
+ export function createBwrapRuntime(policy: RequestPolicy = createRequestPolicy()): BwrapRuntime {
930
+ return new BwrapRuntime(policy);
921
931
  }
@@ -20,6 +20,7 @@ import { createLspManager, type LspService, type LspServiceOptions } from "../li
20
20
  import { registerLspRenameTool } from "../lib/lsp/rename-tool.js";
21
21
  import { formatSubtitlePath } from "../lib/path.js";
22
22
  import type { ToolPendant } from "../lib/pendant.ts";
23
+ import { createRequestPolicy, type RequestPolicy } from "../lib/request-policy.js";
23
24
  import { guardWriteAccess } from "../lib/write-guard.js";
24
25
  import {
25
26
  type ClaudeCodeState,
@@ -230,6 +231,7 @@ export function registerFileTools(
230
231
  pi: ExtensionAPI,
231
232
  state: ClaudeCodeState,
232
233
  getService: () => LspService,
234
+ policy: RequestPolicy,
233
235
  ): void {
234
236
  pi.registerTool({
235
237
  name: "Read",
@@ -399,6 +401,7 @@ export function registerFileTools(
399
401
  newText: params.new_string,
400
402
  replaceAll: params.replace_all,
401
403
  },
404
+ policy,
402
405
  });
403
406
  const [message, details, diagnostics] = await withFileMutationQueue<
404
407
  [string, FileToolDetails, DiagnosticReport]
@@ -594,6 +597,7 @@ export function registerFileTools(
594
597
  toolName: "Write",
595
598
  absolutePath: filePath,
596
599
  change: { oldText: "", newText: params.content },
600
+ policy,
597
601
  });
598
602
  const [message, details, diagnostics] = await withFileMutationQueue<
599
603
  [string, FileToolDetails, DiagnosticReport]
@@ -686,14 +690,25 @@ function restoreFileReads(
686
690
  }
687
691
  }
688
692
 
693
+ export interface ClaudeCodeFileToolOptions extends LspServiceOptions {
694
+ /** 与 bash runtime 共享的非沙盒请求策略;独立入口不传,自建一份。 */
695
+ policy?: RequestPolicy;
696
+ }
697
+
689
698
  /**
690
699
  * 独立扩展入口:files.ts 可单独经 `-e claude-code/files.ts` 加载(spawn-agent
691
700
  * 子代理把 Read/Edit/Write 工具名映射到本文件),无需经 index.ts。reads
692
701
  * state 归本文件所有:扩展实例内创建,并随 session 事件从历史分支恢复,
693
702
  * 与主进程 index.ts 聚合加载时的行为一致。
694
703
  */
695
- export default function claudeCodeFileTools(pi: ExtensionAPI, options?: LspServiceOptions): void {
704
+ export default function claudeCodeFileTools(
705
+ pi: ExtensionAPI,
706
+ options?: ClaudeCodeFileToolOptions,
707
+ ): void {
696
708
  const state = createClaudeCodeState();
709
+ // 聚合入口(index.ts)注入与 bash runtime 共享的那一份;独立入口(spawn-agent
710
+ // 按工具名 `-e` 加载本文件)自建一份,靠 pi.events 跟随同一开关。
711
+ const policy = options?.policy ?? createRequestPolicy(pi.events);
697
712
 
698
713
  // LSP 专属工具(lsp-rename / inspect 族)仅在 lsp.json 存在 enabled 服务器时
699
714
  // 注册(session_start 校验后);本工具集跟踪 read-before-write 状态,rename
@@ -703,6 +718,7 @@ export default function claudeCodeFileTools(pi: ExtensionAPI, options?: LspServi
703
718
  {
704
719
  onEnabled: (pi, service) => {
705
720
  registerLspRenameTool(pi, service, {
721
+ policy,
706
722
  recordReads: async (applied) => {
707
723
  const reads: Record<string, FileSnapshot> = {};
708
724
  for (const fileEdit of applied) {
@@ -736,5 +752,5 @@ export default function claudeCodeFileTools(pi: ExtensionAPI, options?: LspServi
736
752
  });
737
753
 
738
754
  // 文件工具无条件注册;service 惰性获取,disabled 时为 no-op。
739
- registerFileTools(pi, state, () => manager.mustLazyGetService());
755
+ registerFileTools(pi, state, () => manager.mustLazyGetService(), policy);
740
756
  }
@@ -1,5 +1,7 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
+ import { createBwrapRuntime } from "../bwrap/runtime.js";
4
+ import { createRequestPolicy } from "../lib/request-policy.js";
3
5
  import claudeCodeFileTools from "./files.js";
4
6
  import { registerSearchTools } from "./search.js";
5
7
  import { registerSessionTools } from "./session-tools.js";
@@ -9,12 +11,15 @@ import { registerShellTools } from "./shell.js";
9
11
  * Claude Code 风格工具集入口:聚合 files / search / shell / session 四组工具。
10
12
  *
11
13
  * reads state 的创建与 session 恢复归 files.ts 所有(见其 default export),
12
- * 本文件不再持有 state;spawn-agent 子代理按工具名直接 `-e` 加载各模块文件
13
- * (files.ts / grep.ts / glob.ts 均为独立扩展入口)。
14
+ * 非沙盒请求策略(Read/Edit/Write/lsp-rename 的工作区外写入与 Bash 的
15
+ * `dangerouslyDisableSandbox` 共用)由本文件创建后注入两侧;spawn-agent
16
+ * 子代理按工具名直接 `-e` 加载各模块文件(files.ts / grep.ts / glob.ts 均为
17
+ * 独立扩展入口),那些入口各自创建一份策略并经 pi.events 保持同步。
14
18
  */
15
19
  export default function claudeCodeTools(pi: ExtensionAPI): void {
16
- claudeCodeFileTools(pi);
20
+ const policy = createRequestPolicy(pi.events);
21
+ claudeCodeFileTools(pi, { policy });
17
22
  registerSearchTools(pi);
18
- registerShellTools(pi);
23
+ registerShellTools(pi, createBwrapRuntime(policy));
19
24
  registerSessionTools(pi);
20
25
  }
@@ -18,6 +18,7 @@ import {
18
18
  sandboxHintBlock,
19
19
  } from "../bwrap/runtime.js";
20
20
  import { resolveWorkdir } from "../lib/path.js";
21
+ import { createRequestPolicy } from "../lib/request-policy.js";
21
22
 
22
23
  const DEFAULT_TIMEOUT_MS = 120_000;
23
24
  const MAX_TIMEOUT_MS = 7_200_000;
@@ -85,7 +86,7 @@ export function formatBashSuccess(result: Awaited<ReturnType<BwrapRuntime["execu
85
86
  */
86
87
  export function registerShellTools(
87
88
  pi: ExtensionAPI,
88
- runtime: BwrapRuntime = createBwrapRuntime(),
89
+ runtime: BwrapRuntime = createBwrapRuntime(createRequestPolicy(pi.events)),
89
90
  ): void {
90
91
  runtime.setup(pi);
91
92
  pi.registerTool({
@@ -23,6 +23,7 @@ import { Type } from "typebox";
23
23
 
24
24
  import { resolvePathArg } from "../path.js";
25
25
  import type { ToolPendant } from "../pendant.ts";
26
+ import type { RequestPolicy } from "../request-policy.js";
26
27
  import { guardWriteAccess } from "../write-guard.js";
27
28
  import { RenameNotPossibleError } from "./client.js";
28
29
  import type { LspService } from "./lsp.js";
@@ -33,7 +34,9 @@ import {
33
34
  symbolCandidates,
34
35
  } from "./rename.js";
35
36
 
36
- export interface LspRenameHooks {
37
+ export interface LspRenameOptions {
38
+ /** 调用方所在扩展入口的非沙盒请求策略(与文件工具的 write-guard 同一份)。 */
39
+ policy: RequestPolicy;
37
40
  /**
38
41
  * rename 落盘后对每个被修改文件做已读记账;返回的 map 随 details.reads
39
42
  * 持久化,供 session 恢复时重放。不跟踪已读状态的工具集不提供该 hook。
@@ -49,7 +52,7 @@ const LSP_RENAME_PROMPT = readFileSync(
49
52
  export function registerLspRenameTool(
50
53
  pi: ExtensionAPI,
51
54
  service: LspService,
52
- hooks: LspRenameHooks = {},
55
+ options: LspRenameOptions,
53
56
  ): void {
54
57
  pi.registerTool({
55
58
  name: "lsp-rename",
@@ -100,7 +103,7 @@ export function registerLspRenameTool(
100
103
 
101
104
  const notify = (message: string, level: "info" | "warning" | "error") =>
102
105
  ctx.ui.notify(message, level);
103
- const options = { notify, signal };
106
+ const requestOptions = { notify, signal };
104
107
 
105
108
  // ── 按 symbol 名枚举行内候选,逐候选探测与消歧 ────────────────────────
106
109
  const candidates = symbolCandidates(
@@ -125,7 +128,7 @@ export function registerLspRenameTool(
125
128
  line: candidate.line,
126
129
  character: candidate.character,
127
130
  newName: params.new_name,
128
- options,
131
+ options: requestOptions,
129
132
  });
130
133
  successes.push({ result });
131
134
  } catch (error) {
@@ -187,6 +190,7 @@ export function registerLspRenameTool(
187
190
  toolName: "lsp-rename",
188
191
  absolutePath: fileEdit.path,
189
192
  change: { oldText: fileEdit.oldText, newText: fileEdit.newText },
193
+ policy: options.policy,
190
194
  });
191
195
  }
192
196
 
@@ -199,7 +203,8 @@ export function registerLspRenameTool(
199
203
  `### ${fileEdit.path} (${fileEdit.changeCount} edit(s))\n\n\`\`\`diff\n${generateDiffString(fileEdit.oldText, fileEdit.newText).diff}\n\`\`\``,
200
204
  );
201
205
  }
202
- const reads = hooks.recordReads === undefined ? undefined : await hooks.recordReads(applied);
206
+ const reads =
207
+ options.recordReads === undefined ? undefined : await options.recordReads(applied);
203
208
 
204
209
  let diagnosticText = "";
205
210
  for (const fileEdit of applied) {
@@ -3,27 +3,44 @@
3
3
  * (bash 的 `dangerouslyDisableSandbox`、编辑类工具的工作区外写入)不再弹审批框,
4
4
  * 一律按用户「无理由拒绝」处理;`/bwrap-allow-request` 恢复审批。
5
5
  *
6
- * 状态由闭包持有(AGENTS.md:不用模块级可变变量维护跨调用状态)。
7
- * 模块级单例让互相独立的扩展入口(bwrap runtime、claude-code / opencode 的文件工具、
8
- * lsp-rename、web_fetch)共享同一份会话状态——各入口分别 createRequestPolicy()
9
- * 会得到互不相通的策略,等于开关只对其中一半生效。
6
+ * pi 给每个扩展入口单独建 jiti 实例(loader 里 `moduleCache: false`),模块级状态
7
+ * 不跨入口共享,所以策略实例由各入口的工厂创建,生命周期随扩展实例,聚合入口
8
+ * (claude-code / opencode 的 index.ts)创建一份注入给文件工具与 bash runtime。
9
+ *
10
+ * 需要跨入口一致时(独立入口的 web_fetch 也要跟随同一个开关)把 `pi.events` 传进来:
11
+ * 变更广播给同进程内所有订阅者,订阅由 loader 随扩展卸载自动撤销。
10
12
  */
11
13
 
14
+ import type { EventBus } from "@earendil-works/pi-coding-agent";
15
+ import { type Static, Type } from "typebox";
16
+ import { Value } from "typebox/value";
17
+
18
+ /** 策略广播频道;同进程内各入口的策略副本据此保持同步。 */
19
+ export const REQUEST_POLICY_CHANNEL = "bwrap:request-policy";
20
+
21
+ const requestPolicyMessageSchema = Type.Object({ denyRequests: Type.Boolean() });
22
+
23
+ export type RequestPolicyMessage = Static<typeof requestPolicyMessageSchema>;
24
+
12
25
  export interface RequestPolicy {
13
26
  /** 是否拒绝一切非沙盒请求(不弹审批框,直接拒绝)。 */
14
27
  deniesRequests(): boolean;
15
28
  setDenyRequests(deny: boolean): void;
16
29
  }
17
30
 
18
- export function createRequestPolicy(): RequestPolicy {
31
+ /** 不传 events 时策略只在本入口内生效(测试与不需要跨入口同步的场景)。 */
32
+ export function createRequestPolicy(events?: EventBus): RequestPolicy {
19
33
  let denyRequests = false;
34
+
35
+ events?.on(REQUEST_POLICY_CHANNEL, (data) => {
36
+ if (Value.Check(requestPolicyMessageSchema, data)) denyRequests = data.denyRequests;
37
+ });
38
+
20
39
  return {
21
40
  deniesRequests: () => denyRequests,
22
41
  setDenyRequests(deny: boolean) {
23
42
  denyRequests = deny;
43
+ events?.emit(REQUEST_POLICY_CHANNEL, { denyRequests: deny } satisfies RequestPolicyMessage);
24
44
  },
25
45
  };
26
46
  }
27
-
28
- /** 进程级共享的策略实例;由 `/bwrap-deny-request` / `/bwrap-allow-request` 切换,session_start 复位。 */
29
- export const requestPolicy = createRequestPolicy();
@@ -22,7 +22,7 @@ import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
22
22
 
23
23
  import { applyEdit, normalizeToLF } from "../opencode/edit-engine.js";
24
24
  import { fenceCodeBlock } from "./markdown.js";
25
- import { requestPolicy } from "./request-policy.js";
25
+ import type { RequestPolicy } from "./request-policy.js";
26
26
 
27
27
  const ALWAYS_ALLOW = ["/tmp"];
28
28
  const MAX_PREVIEW_LINES = 100;
@@ -121,6 +121,8 @@ export interface WriteGuardOptions {
121
121
  * 待审批的变更内容;缺省时审批对话框不展示 diff 预览,仅按路径审批。
122
122
  */
123
123
  change?: PendingChange;
124
+ /** 调用方所在扩展入口持有的非沙盒请求策略(跨入口一致时绑同一个 pi.events)。 */
125
+ policy: RequestPolicy;
124
126
  }
125
127
 
126
128
  /**
@@ -138,7 +140,7 @@ export async function guardWriteAccess(
138
140
 
139
141
  // 非沙盒请求策略生效时不弹审批框:工作区外写入按用户点 "Block"(无理由)处理。
140
142
  // 放在 win32 / 无 UI 分支之前,策略优先级高于各平台的降级路径。
141
- if (requestPolicy.deniesRequests()) {
143
+ if (opts.policy.deniesRequests()) {
142
144
  throw new Error(`user deny ${opts.toolName}: blocked`);
143
145
  }
144
146
 
@@ -12,6 +12,7 @@ import {
12
12
  sandboxHintBlock,
13
13
  } from "../bwrap/runtime.js";
14
14
  import { resolveWorkdir } from "../lib/path.js";
15
+ import { createRequestPolicy } from "../lib/request-policy.js";
15
16
 
16
17
  const DEFAULT_TIMEOUT_MS = 120_000;
17
18
  const MAX_TIMEOUT_MS = 7_200_000;
@@ -41,7 +42,7 @@ function appendTruncationNotice(
41
42
  */
42
43
  export default function opencodeBash(
43
44
  pi: ExtensionAPI,
44
- runtime: BwrapRuntime = createBwrapRuntime(),
45
+ runtime: BwrapRuntime = createBwrapRuntime(createRequestPolicy(pi.events)),
45
46
  ): void {
46
47
  // 每个扩展实例持有自己的 runtime:不依赖模块级全局状态,状态随扩展
47
48
  // 实例生命周期(进程启动 / /reload / session 切换时工厂重建即重置)。
@@ -34,6 +34,7 @@ import { registerLspInspectTools } from "../lib/lsp/inspect-tool.js";
34
34
  import { createLspManager, type LspService, type LspServiceOptions } from "../lib/lsp/lsp.js";
35
35
  import { registerLspRenameTool } from "../lib/lsp/rename-tool.js";
36
36
  import { formatSubtitlePath } from "../lib/path.js";
37
+ import { createRequestPolicy, type RequestPolicy } from "../lib/request-policy.js";
37
38
  import { guardWriteAccess } from "../lib/write-guard.js";
38
39
  import { applyEdit, normalizeToLF, stripBom } from "./edit-engine.js";
39
40
 
@@ -534,7 +535,11 @@ const editSchema = Type.Object({
534
535
  ),
535
536
  });
536
537
 
537
- function registerEditTool(pi: ExtensionAPI, getService: () => LspService): void {
538
+ function registerEditTool(
539
+ pi: ExtensionAPI,
540
+ getService: () => LspService,
541
+ policy: RequestPolicy,
542
+ ): void {
538
543
  pi.registerTool({
539
544
  name: "edit",
540
545
  label: "edit",
@@ -565,6 +570,7 @@ function registerEditTool(pi: ExtensionAPI, getService: () => LspService): void
565
570
  toolName: "edit",
566
571
  absolutePath,
567
572
  change: { oldText: oldString, newText: newString, replaceAll },
573
+ policy,
568
574
  });
569
575
 
570
576
  const [message, details, diagnostics] = await withFileMutationQueue(
@@ -686,7 +692,11 @@ export function resolveBom(
686
692
  return { bom: sourceBom || nextBom, text };
687
693
  }
688
694
 
689
- function registerWriteTool(pi: ExtensionAPI, getService: () => LspService): void {
695
+ function registerWriteTool(
696
+ pi: ExtensionAPI,
697
+ getService: () => LspService,
698
+ policy: RequestPolicy,
699
+ ): void {
690
700
  pi.registerTool({
691
701
  name: "write",
692
702
  label: "write",
@@ -707,6 +717,7 @@ function registerWriteTool(pi: ExtensionAPI, getService: () => LspService): void
707
717
  toolName: "write",
708
718
  absolutePath,
709
719
  change: { oldText: "", newText: content },
720
+ policy,
710
721
  });
711
722
  const dir = dirname(absolutePath);
712
723
 
@@ -764,25 +775,40 @@ function registerWriteTool(pi: ExtensionAPI, getService: () => LspService): void
764
775
 
765
776
  // ── 入口 ─────────────────────────────────────────────────────────────────────
766
777
 
767
- export function registerFileTools(pi: ExtensionAPI, getService: () => LspService): void {
778
+ export function registerFileTools(
779
+ pi: ExtensionAPI,
780
+ getService: () => LspService,
781
+ policy: RequestPolicy,
782
+ ): void {
768
783
  registerReadTool(pi, getService);
769
- registerEditTool(pi, getService);
770
- registerWriteTool(pi, getService);
784
+ registerEditTool(pi, getService, policy);
785
+ registerWriteTool(pi, getService, policy);
771
786
  // lsp-rename / inspect 工具由 manager 的 onEnabled 回调注册,不在这里注册。
772
787
  }
773
788
 
789
+ export interface OpencodeFileToolOptions extends LspServiceOptions {
790
+ /** 与 bash runtime 共享的非沙盒请求策略;独立入口不传,自建一份。 */
791
+ policy?: RequestPolicy;
792
+ }
793
+
774
794
  /** 独立入口:创建 LSP manager(session_start 时按配置启用)并注册文件工具。 */
775
- export default function opencodeFileTools(pi: ExtensionAPI, options?: LspServiceOptions): void {
795
+ export default function opencodeFileTools(
796
+ pi: ExtensionAPI,
797
+ options?: OpencodeFileToolOptions,
798
+ ): void {
799
+ // 聚合入口(index.ts)注入与 bash runtime 共享的那一份;独立入口自建并靠
800
+ // pi.events 跟随同一开关。
801
+ const policy = options?.policy ?? createRequestPolicy(pi.events);
776
802
  const manager = createLspManager(
777
803
  pi,
778
804
  {
779
805
  onEnabled: (pi, service) => {
780
- registerLspRenameTool(pi, service);
806
+ registerLspRenameTool(pi, service, { policy });
781
807
  registerLspInspectTools(pi, service);
782
808
  },
783
809
  },
784
810
  options,
785
811
  );
786
812
  // 文件工具无条件注册;service 惰性获取,disabled 时为 no-op。
787
- registerFileTools(pi, () => manager.mustLazyGetService());
813
+ registerFileTools(pi, () => manager.mustLazyGetService(), policy);
788
814
  }
@@ -14,6 +14,8 @@
14
14
 
15
15
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
16
 
17
+ import { createBwrapRuntime } from "../bwrap/runtime.js";
18
+ import { createRequestPolicy } from "../lib/request-policy.js";
17
19
  import opencodeBash from "./bash.js";
18
20
  import opencodeFileTools from "./files.js";
19
21
  import opencodeQuestion from "./question.js";
@@ -40,8 +42,11 @@ export { default as opencodeQuestion } from "./question.js";
40
42
  export { default as opencodeTodo } from "./todo.js";
41
43
 
42
44
  export default function opencode(pi: ExtensionAPI) {
43
- opencodeFileTools(pi);
45
+ // 非沙盒请求策略由本入口创建,注入文件工具与 bash runtime 共享同一份;
46
+ // 独立入口(web/fetch.ts 等)各自创建一份并经 pi.events 保持同步。
47
+ const policy = createRequestPolicy(pi.events);
48
+ opencodeFileTools(pi, { policy });
44
49
  opencodeTodo(pi);
45
50
  opencodeQuestion(pi);
46
- opencodeBash(pi);
51
+ opencodeBash(pi, createBwrapRuntime(policy));
47
52
  }
package/src/web/fetch.ts CHANGED
@@ -23,6 +23,7 @@ import { Type } from "typebox";
23
23
 
24
24
  import { resolvePathArg } from "../lib/path.js";
25
25
  import { createHttpProxy } from "../lib/proxy.js";
26
+ import { createRequestPolicy } from "../lib/request-policy.js";
26
27
  import { guardWriteAccess } from "../lib/write-guard.js";
27
28
 
28
29
  const httpProxy = createHttpProxy();
@@ -310,6 +311,9 @@ function truncateMarkdown(text: string): { text: string; truncated: boolean } {
310
311
  }
311
312
 
312
313
  export default function webFetchTool(pi: ExtensionAPI): void {
314
+ // 本工具是独立扩展入口(pi 给每个入口单独建 jiti 实例),自建一份非沙盒请求
315
+ // 策略并订阅 pi.events:/bwrap-deny-request 在 bash 入口切换时会同步过来。
316
+ const policy = createRequestPolicy(pi.events);
313
317
  pi.registerTool({
314
318
  name: "web_fetch",
315
319
  label: "Web Fetch",
@@ -339,7 +343,7 @@ export default function webFetchTool(pi: ExtensionAPI): void {
339
343
  // 落盘位置的审批与写文件工具同一套:工作区与 /tmp 自动放行,其余问用户。
340
344
  // 放在 try 外面,拒绝的原因(user deny)不该被改写成「抓取失败」。
341
345
  if (destination !== undefined) {
342
- await guardWriteAccess(ctx, { toolName: "web_fetch", absolutePath: destination });
346
+ await guardWriteAccess(ctx, { toolName: "web_fetch", absolutePath: destination, policy });
343
347
  }
344
348
 
345
349
  try {