@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 +4 -4
- package/src/bwrap/runtime.ts +18 -8
- package/src/claude-code/files.ts +18 -2
- package/src/claude-code/index.ts +9 -4
- package/src/claude-code/shell.ts +2 -1
- package/src/lib/lsp/rename-tool.ts +10 -5
- package/src/lib/request-policy.ts +25 -8
- package/src/lib/write-guard.ts +4 -2
- package/src/opencode/bash.ts +2 -1
- package/src/opencode/files.ts +34 -8
- package/src/opencode/index.ts +7 -2
- package/src/web/fetch.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
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": "
|
|
28
|
-
"aft-search": "
|
|
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",
|
package/src/bwrap/runtime.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 &&
|
|
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 &&
|
|
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}${
|
|
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
|
-
|
|
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
|
-
|
|
920
|
-
|
|
928
|
+
/** 缺省自建一份不跨入口同步的策略;入口通常显式传入 `createRequestPolicy(pi.events)`。 */
|
|
929
|
+
export function createBwrapRuntime(policy: RequestPolicy = createRequestPolicy()): BwrapRuntime {
|
|
930
|
+
return new BwrapRuntime(policy);
|
|
921
931
|
}
|
package/src/claude-code/files.ts
CHANGED
|
@@ -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(
|
|
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
|
}
|
package/src/claude-code/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
13
|
-
*
|
|
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
|
-
|
|
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
|
}
|
package/src/claude-code/shell.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
|
|
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();
|
package/src/lib/write-guard.ts
CHANGED
|
@@ -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 {
|
|
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 (
|
|
143
|
+
if (opts.policy.deniesRequests()) {
|
|
142
144
|
throw new Error(`user deny ${opts.toolName}: blocked`);
|
|
143
145
|
}
|
|
144
146
|
|
package/src/opencode/bash.ts
CHANGED
|
@@ -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 切换时工厂重建即重置)。
|
package/src/opencode/files.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|
package/src/opencode/index.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|