@trim21/personal-pi-extensions 0.1.20261012023047 → 0.1.20261012031237
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 +10 -5
- package/package.json +1 -1
- package/src/bwrap/runtime.ts +15 -0
- package/src/claude-code/shell.ts +2 -1
- package/src/codemode/fs.ts +3 -0
- package/src/codemode/prelude.ts +1 -1
- package/src/codemode/protocol.ts +1 -1
- package/src/codemode/sandbox.ts +32 -3
- package/src/codemode/source.ts +21 -2
- package/src/codemode/tool.ts +19 -4
- package/src/codemode/worker.js +1 -1
- package/src/lib/tool-bus.ts +25 -1
- package/src/lib/write-guard.ts +9 -0
- package/src/opencode/bash.ts +2 -1
package/README.md
CHANGED
|
@@ -35,8 +35,9 @@
|
|
|
35
35
|
`sleep`。
|
|
36
36
|
|
|
37
37
|
- **嵌套调用经工具总线**:`call("Bash", { command })` 最终执行的是 `Bash` 工具自己的
|
|
38
|
-
`execute
|
|
39
|
-
|
|
38
|
+
`execute`。**脚本里不弹审批**:需要人工授权的调用(写工作区外、Bash 申请沙箱外执行)
|
|
39
|
+
在 codemode 内直接以 `CallFailedError` 失败,错误信息提示改用直调;模型直调同一个工具时
|
|
40
|
+
审批照常出现。此外 codemode 不额外加确认层:不需要审批的调用不会多出任何确认框。
|
|
40
41
|
- **可调用集合 = 声明了结构化输出的工具**(有 `structuredSchema` 的才进),执行时再与当前
|
|
41
42
|
active 列表求交,所以 `personalExtensions.disabledTools`、pi 的 `defaultTools` /
|
|
42
43
|
`--tools`、子代理的工具白名单都同样约束脚本。脚本拿到的返回值必须有确定的形状,因此没给
|
|
@@ -62,10 +63,14 @@
|
|
|
62
63
|
结构的工具不进集合,`call()` 会报「not available in codemode」。
|
|
63
64
|
- **脚本接口**:`call` / `CallFailedError` / `ALL_TOOLS` / `fs.read` / `fs.write` /
|
|
64
65
|
`text` / `image` / `exit` / `sleep` / `console.*` / `store.set` / `store.get` / `store.list`
|
|
65
|
-
(会话内持久的键值表);首行可选
|
|
66
|
-
|
|
66
|
+
(会话内持久的键值表);首行可选
|
|
67
|
+
`// @options: {"max_output_tokens": 10000, "timeout_ms": 600000}`——选项留在脚本里而不是工具参数,
|
|
68
|
+
因为 pi 的 grammar 约束(`constrainedSampling`)只支持单个自由文本参数,多出来的参数在支持
|
|
69
|
+
grammar 的模型上会被静默忽略。不声明 `timeout_ms` 时脚本没有时间上限:死循环由调用方中止(Esc)
|
|
70
|
+
结束,等嵌套调用返回(含慢命令)多久都不算超时。声明了 `timeout_ms` 就有墙钟预算(含等待嵌套
|
|
71
|
+
调用的时间),到点时正在跑的调用被中止、结果以超时结束,已产生的输出与调用记录仍然返回。
|
|
67
72
|
`await sleep(seconds)`(秒,可为小数)是脚本唯一的等待手段,由 worker 线程自己的定时器驱动
|
|
68
|
-
|
|
73
|
+
(不经主线程、不产生进度帧),它自己也没有上限——但会被 `timeout_ms` 结束。
|
|
69
74
|
- **工具描述**里给出每个可调用工具的 `declare function call(name, args): Promise<T>` 重载,
|
|
70
75
|
参数与返回类型都取自工具自己的 schema,所以模型在写脚本前就知道返回值形状。
|
|
71
76
|
- **store** 记在每次成功调用工具结果的 `details.store` 上(与 `src/lib/file-reads.ts` 的
|
package/package.json
CHANGED
package/src/bwrap/runtime.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface BwrapExecutionRequest {
|
|
|
86
86
|
signal?: AbortSignal;
|
|
87
87
|
onUpdate?: AgentToolUpdateCallback;
|
|
88
88
|
ctx: ExtensionContext;
|
|
89
|
+
/** 本次调用不可请求用户审批(codemode 的嵌套调用):提权请求直接失败,不弹框。 */
|
|
90
|
+
unapprovable?: boolean;
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
/**
|
|
@@ -333,6 +335,13 @@ function describeLimits(resolved: ResolvedBwrap): string {
|
|
|
333
335
|
*/
|
|
334
336
|
const UNSANDBOXED_DENIED = "User denied unsandboxed execution.";
|
|
335
337
|
|
|
338
|
+
/**
|
|
339
|
+
* codemode 内的提权请求:脚本里弹不了审批框(那次工具调用只经 codemode 暴露),
|
|
340
|
+
* 因此直接拒绝,并让模型知道改用直调就能走正常审批。
|
|
341
|
+
*/
|
|
342
|
+
const UNSANDBOXED_DENIED_IN_CODEMODE =
|
|
343
|
+
"Unsandboxed execution needs user approval, which codemode cannot ask for. Call Bash directly instead of from a script.";
|
|
344
|
+
|
|
336
345
|
/** 命令没经沙箱时沿用 prompt 里的说法,给出在沙盒外重跑的手段。 */
|
|
337
346
|
const SANDBOX_ESCAPE_HATCH =
|
|
338
347
|
"If the command needs more than that, use the `dangerouslyDisableSandbox` parameter to request unsandboxed execution; the user must approve this request.";
|
|
@@ -571,6 +580,7 @@ export class BwrapRuntime {
|
|
|
571
580
|
request.description,
|
|
572
581
|
execCwd,
|
|
573
582
|
ruleSet,
|
|
583
|
+
request.unapprovable === true,
|
|
574
584
|
)) === "sandbox";
|
|
575
585
|
}
|
|
576
586
|
}
|
|
@@ -706,11 +716,16 @@ export class BwrapRuntime {
|
|
|
706
716
|
reason: string | undefined,
|
|
707
717
|
execCwd: string,
|
|
708
718
|
ruleSet: ApprovalRuleSet,
|
|
719
|
+
unapprovable: boolean,
|
|
709
720
|
): Promise<FullAccessGrant> {
|
|
710
721
|
// hasUI 判定推迟到审批时刻:无 UI 会话弹不了审批框,按用户点 Deny 的标准文案拒绝
|
|
711
722
|
if (!ctx.hasUI) {
|
|
712
723
|
throw new Error(UNSANDBOXED_DENIED);
|
|
713
724
|
}
|
|
725
|
+
// codemode 内一律不弹审批:提权请求直接失败,并提示改用直调
|
|
726
|
+
if (unapprovable) {
|
|
727
|
+
throw new Error(UNSANDBOXED_DENIED_IN_CODEMODE);
|
|
728
|
+
}
|
|
714
729
|
const decision = await this.approveFullAccessUI(ctx, command, reason, execCwd, ruleSet);
|
|
715
730
|
// 关闭对话框 = 中断并拒绝,不循环重问
|
|
716
731
|
if (decision === undefined) {
|
package/src/claude-code/shell.ts
CHANGED
|
@@ -127,7 +127,7 @@ export function registerShellTools(bus: ToolBus, pi: ExtensionAPI, runtime: Bwra
|
|
|
127
127
|
},
|
|
128
128
|
{ additionalProperties: false },
|
|
129
129
|
),
|
|
130
|
-
async execute(id, params, signal, onUpdate, ctx) {
|
|
130
|
+
async execute(id, params, signal, onUpdate, ctx, callOptions) {
|
|
131
131
|
const timeout = params.timeout ?? BASH_DEFAULT_TIMEOUT_MS;
|
|
132
132
|
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > BASH_MAX_TIMEOUT_MS) {
|
|
133
133
|
throw new Error(`timeout must be between 1 and ${BASH_MAX_TIMEOUT_MS} milliseconds`);
|
|
@@ -147,6 +147,7 @@ export function registerShellTools(bus: ToolBus, pi: ExtensionAPI, runtime: Bwra
|
|
|
147
147
|
description: params.description,
|
|
148
148
|
signal,
|
|
149
149
|
onUpdate,
|
|
150
|
+
unapprovable: callOptions?.unapprovable,
|
|
150
151
|
});
|
|
151
152
|
} catch (error) {
|
|
152
153
|
if (!(error instanceof Error)) {
|
package/src/codemode/fs.ts
CHANGED
|
@@ -36,6 +36,8 @@ const FS_WRITE = "fs.write";
|
|
|
36
36
|
export interface FsCallContext {
|
|
37
37
|
ctx: ExtensionContext;
|
|
38
38
|
signal?: AbortSignal;
|
|
39
|
+
/** 本次调用不可请求用户审批(脚本里弹不了框):区外写入直接失败。 */
|
|
40
|
+
unapprovable?: boolean;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/** 一次 fs 原语的结果:`value` 回给脚本,`reads` 是本次新增的已读记账(进 details.reads)。 */
|
|
@@ -114,6 +116,7 @@ export function createCodemodeFs(options: CodemodeFsOptions): CodemodeFs {
|
|
|
114
116
|
},
|
|
115
117
|
policy,
|
|
116
118
|
signal: call.signal,
|
|
119
|
+
unapprovable: call.unapprovable,
|
|
117
120
|
});
|
|
118
121
|
await mkdir(dirname(absolutePath), { recursive: true });
|
|
119
122
|
await writeFile(absolutePath, content, { encoding: "utf8", signal: call.signal });
|
package/src/codemode/prelude.ts
CHANGED
|
@@ -351,7 +351,7 @@ export const PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson
|
|
|
351
351
|
stringify({
|
|
352
352
|
name: "Error",
|
|
353
353
|
message:
|
|
354
|
-
"The script is waiting on a promise that can never settle: no tool call
|
|
354
|
+
"The script is waiting on a promise that can never settle: no tool call and no sleep is pending.",
|
|
355
355
|
}),
|
|
356
356
|
);
|
|
357
357
|
return true;
|
package/src/codemode/protocol.ts
CHANGED
|
@@ -84,7 +84,7 @@ export interface WorkerBootstrap {
|
|
|
84
84
|
export type CodemodeOutputItem =
|
|
85
85
|
{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string };
|
|
86
86
|
|
|
87
|
-
export type ScriptErrorKind = "script" | "aborted" | "sandbox";
|
|
87
|
+
export type ScriptErrorKind = "script" | "aborted" | "timeout" | "sandbox";
|
|
88
88
|
|
|
89
89
|
export interface ScriptError {
|
|
90
90
|
kind: ScriptErrorKind;
|
package/src/codemode/sandbox.ts
CHANGED
|
@@ -52,8 +52,18 @@ export interface SandboxRunOptions {
|
|
|
52
52
|
toolCallId: string;
|
|
53
53
|
/** 调用方中止本次执行;脚本没有自己的超时,死循环只能靠它结束。 */
|
|
54
54
|
signal?: AbortSignal;
|
|
55
|
-
/**
|
|
56
|
-
|
|
55
|
+
/**
|
|
56
|
+
* 本次执行的墙钟预算(毫秒),含等待嵌套调用的时间;缺省不设上限。
|
|
57
|
+
* 到点后执行以 `kind: "timeout"` 结束,正在跑的嵌套调用收到中止信号。
|
|
58
|
+
*/
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
/** 每次嵌套调用:由调用方执行工具并把结果回给脚本。`signal` 含调用方中止与本次预算。 */
|
|
61
|
+
onCall(request: {
|
|
62
|
+
id: number;
|
|
63
|
+
name: string;
|
|
64
|
+
args: unknown;
|
|
65
|
+
signal: AbortSignal;
|
|
66
|
+
}): Promise<SandboxCallOutcome>;
|
|
57
67
|
/** 脚本流式产生的输出项,用于 toolcall 进度。 */
|
|
58
68
|
onOutput?(items: readonly CodemodeOutputItem[]): void;
|
|
59
69
|
/** 嵌套调用的开始与结束,用于 toolcall 进度;事件里带截至此刻的调用记录快照。 */
|
|
@@ -108,6 +118,15 @@ function runInWorker(wasm: object, options: SandboxRunOptions): Promise<SandboxO
|
|
|
108
118
|
const started = new Map<number, { name: string; at: number }>();
|
|
109
119
|
let callSequence = 0;
|
|
110
120
|
let settled = false;
|
|
121
|
+
let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
|
|
122
|
+
|
|
123
|
+
// 超时与调用方中止合成一条信号,交给嵌套调用:预算到点时正在跑的工具也要停,
|
|
124
|
+
// 否则 terminate 只杀 worker,工具会在主线程继续跑完并留下副作用
|
|
125
|
+
const aborter = new AbortController();
|
|
126
|
+
const signal =
|
|
127
|
+
options.signal === undefined
|
|
128
|
+
? aborter.signal
|
|
129
|
+
: AbortSignal.any([options.signal, aborter.signal]);
|
|
111
130
|
|
|
112
131
|
// 进度帧发出去的是拷贝:记录随后会被改成终态,已经发出去的帧不能被它改掉
|
|
113
132
|
const snapshotCalls = (): ScriptCall[] => calls.map((call) => ({ ...call }));
|
|
@@ -121,6 +140,7 @@ function runInWorker(wasm: object, options: SandboxRunOptions): Promise<SandboxO
|
|
|
121
140
|
return;
|
|
122
141
|
}
|
|
123
142
|
settled = true;
|
|
143
|
+
clearTimeout(timeoutTimer);
|
|
124
144
|
void worker.terminate();
|
|
125
145
|
resolve({ ...body, output, calls });
|
|
126
146
|
};
|
|
@@ -143,6 +163,15 @@ function runInWorker(wasm: object, options: SandboxRunOptions): Promise<SandboxO
|
|
|
143
163
|
);
|
|
144
164
|
}
|
|
145
165
|
|
|
166
|
+
if (options.timeoutMs !== undefined) {
|
|
167
|
+
const timeoutMs = options.timeoutMs;
|
|
168
|
+
timeoutTimer = setTimeout(() => {
|
|
169
|
+
// 先中止正在跑的嵌套调用,再定结果:terminate 不会自动停主线程里的工具
|
|
170
|
+
aborter.abort();
|
|
171
|
+
fail(`Execution timed out after ${timeoutMs} ms`, "timeout");
|
|
172
|
+
}, timeoutMs);
|
|
173
|
+
}
|
|
174
|
+
|
|
146
175
|
const handleCall = async (id: number, name: string, args: unknown): Promise<void> => {
|
|
147
176
|
callSequence += 1;
|
|
148
177
|
const record: ScriptCall = {
|
|
@@ -157,7 +186,7 @@ function runInWorker(wasm: object, options: SandboxRunOptions): Promise<SandboxO
|
|
|
157
186
|
options.onCallProgress?.({ phase: "start", name, args, calls: snapshotCalls() });
|
|
158
187
|
let outcome: SandboxCallOutcome;
|
|
159
188
|
try {
|
|
160
|
-
outcome = await options.onCall({ id, name, args });
|
|
189
|
+
outcome = await options.onCall({ id, name, args, signal });
|
|
161
190
|
} catch (error) {
|
|
162
191
|
outcome = { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
163
192
|
}
|
package/src/codemode/source.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* codemode 脚本源码的解析:首行可选的 `// @options: {"max_output_tokens":…}`。
|
|
2
|
+
* codemode 脚本源码的解析:首行可选的 `// @options: {"max_output_tokens":…, "timeout_ms":…}`。
|
|
3
3
|
*
|
|
4
4
|
* 选项行与脚本首行共用一行,解析后原样保留为空行,脚本里报错的行号因此与用户写的
|
|
5
5
|
* 一致。解析失败(空输入、JSON 非法、未知字段、只有选项行没有代码)直接报错。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const OPTIONS_PREFIX = "// @options:";
|
|
9
|
-
const SUPPORTED_FIELDS = ["max_output_tokens"] as const;
|
|
9
|
+
const SUPPORTED_FIELDS = ["max_output_tokens", "timeout_ms"] as const;
|
|
10
|
+
|
|
11
|
+
/** `timeout_ms` 的上限:`setTimeout` 能接受的最大延时(毫秒)。 */
|
|
12
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
10
13
|
|
|
11
14
|
export const DEFAULT_OUTPUT_TOKENS = 10_000;
|
|
12
15
|
|
|
@@ -19,6 +22,8 @@ export class CodemodeSourceError extends Error {
|
|
|
19
22
|
|
|
20
23
|
export interface CodemodeSourceOptions {
|
|
21
24
|
maxOutputTokens: number;
|
|
25
|
+
/** 本次执行的墙钟预算(毫秒)。不声明时脚本没有时间上限。 */
|
|
26
|
+
timeoutMs: number;
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
export interface ParsedCodemodeSource {
|
|
@@ -45,6 +50,20 @@ function parseOptions(json: string): Partial<CodemodeSourceOptions> {
|
|
|
45
50
|
`${OPTIONS_PREFIX} does not support ${JSON.stringify(key)}; supported fields are ${SUPPORTED_FIELDS.map((field) => `\`${field}\``).join(" and ")}`,
|
|
46
51
|
);
|
|
47
52
|
}
|
|
53
|
+
if (key === "timeout_ms") {
|
|
54
|
+
if (
|
|
55
|
+
typeof value !== "number" ||
|
|
56
|
+
!Number.isSafeInteger(value) ||
|
|
57
|
+
value <= 0 ||
|
|
58
|
+
value > MAX_TIMEOUT_MS
|
|
59
|
+
) {
|
|
60
|
+
throw new CodemodeSourceError(
|
|
61
|
+
`${OPTIONS_PREFIX} \`timeout_ms\` must be a positive integer up to ${MAX_TIMEOUT_MS}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
options.timeoutMs = value;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
48
67
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
49
68
|
throw new CodemodeSourceError(`${OPTIONS_PREFIX} ${key} must be a positive number`);
|
|
50
69
|
}
|
package/src/codemode/tool.ts
CHANGED
|
@@ -115,6 +115,10 @@ function buildDescription(tools: readonly CallableTool[]): string {
|
|
|
115
115
|
" return: urls, ids and metadata you will not use cost context and buy nothing.",
|
|
116
116
|
"- `await sleep(seconds)` waits that many seconds (fractions allowed). It is the only way to",
|
|
117
117
|
' wait: the sandbox has no timers, and it beats `call("Bash", { command: "sleep 1" })`.',
|
|
118
|
+
"- Nested calls cannot ask for user approval: a call that would need it (writing outside the",
|
|
119
|
+
" workspace, unsandboxed Bash) fails. Make those calls directly instead of from a script.",
|
|
120
|
+
'- `// @options: {"timeout_ms": N}` bounds the whole run in milliseconds; without it the script',
|
|
121
|
+
" has no time limit beyond the user aborting it.",
|
|
118
122
|
"- `store.set(key, value)`, `store.get(key)` and `store.list()` are a small key/value store that",
|
|
119
123
|
" persists across codemode calls in this session; `store.set(key, undefined)` removes a key.",
|
|
120
124
|
"- The sandbox has no file system, network, timers or Node APIs: calling tools is its only",
|
|
@@ -294,9 +298,12 @@ export function createCodemodeTools(pi: ExtensionAPI): CodemodeTools {
|
|
|
294
298
|
parameters: Type.Object({
|
|
295
299
|
code: Type.String({
|
|
296
300
|
description:
|
|
297
|
-
'Raw JavaScript source. Top-level await and return work. May start with a `// @options: {"max_output_tokens": 10000}` line.',
|
|
301
|
+
'Raw JavaScript source. Top-level await and return work. May start with a `// @options: {"max_output_tokens": 10000, "timeout_ms": 600000}` line.',
|
|
298
302
|
}),
|
|
299
303
|
}),
|
|
304
|
+
// 采样约束与 pi 内置 codemode 同形:grammar 只能约束一个自由文本参数,所以
|
|
305
|
+
// options 只能待在脚本里(多参数会让这个工具在支持 grammar 的模型上退化成
|
|
306
|
+
// 单字段 custom tool,额外参数被静默忽略)
|
|
300
307
|
constrainedSampling: {
|
|
301
308
|
type: "grammar",
|
|
302
309
|
variants: { openai_lark: CODEMODE_SOURCE_GRAMMAR },
|
|
@@ -306,10 +313,12 @@ export function createCodemodeTools(pi: ExtensionAPI): CodemodeTools {
|
|
|
306
313
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
307
314
|
let code: string;
|
|
308
315
|
let maxOutputTokens = DEFAULT_OUTPUT_TOKENS;
|
|
316
|
+
let timeoutMs: number | undefined;
|
|
309
317
|
try {
|
|
310
318
|
const parsed = parseCodemodeSource(params.code);
|
|
311
319
|
code = parsed.code;
|
|
312
320
|
maxOutputTokens = parsed.options.maxOutputTokens ?? maxOutputTokens;
|
|
321
|
+
timeoutMs = parsed.options.timeoutMs;
|
|
313
322
|
} catch (error) {
|
|
314
323
|
const message = error instanceof Error ? error.message : String(error);
|
|
315
324
|
return {
|
|
@@ -335,6 +344,7 @@ export function createCodemodeTools(pi: ExtensionAPI): CodemodeTools {
|
|
|
335
344
|
store: readStore(ctx),
|
|
336
345
|
toolCallId,
|
|
337
346
|
signal,
|
|
347
|
+
timeoutMs,
|
|
338
348
|
onOutput: (items) => {
|
|
339
349
|
const text = textOf(items);
|
|
340
350
|
if (text === "") {
|
|
@@ -364,10 +374,15 @@ export function createCodemodeTools(pi: ExtensionAPI): CodemodeTools {
|
|
|
364
374
|
},
|
|
365
375
|
});
|
|
366
376
|
},
|
|
367
|
-
onCall: async ({ name, args }) => {
|
|
377
|
+
onCall: async ({ name, args, signal: callSignal }) => {
|
|
368
378
|
if (fs.handles(name)) {
|
|
369
379
|
try {
|
|
370
|
-
|
|
380
|
+
// 脚本里弹不了审批:需要授权的操作(工作区外写入)直接失败
|
|
381
|
+
const result = await fs.execute(name, args, {
|
|
382
|
+
ctx,
|
|
383
|
+
signal: callSignal,
|
|
384
|
+
unapprovable: true,
|
|
385
|
+
});
|
|
371
386
|
if (result.reads !== undefined) {
|
|
372
387
|
Object.assign(recordedReads, result.reads);
|
|
373
388
|
}
|
|
@@ -382,7 +397,7 @@ export function createCodemodeTools(pi: ExtensionAPI): CodemodeTools {
|
|
|
382
397
|
if (!callable.has(name)) {
|
|
383
398
|
return { ok: false, error: `Tool "${name}" is not available in codemode.` };
|
|
384
399
|
}
|
|
385
|
-
return bus.executeCodemodeTool(name, args, { ctx, signal });
|
|
400
|
+
return bus.executeCodemodeTool(name, args, { ctx, signal: callSignal });
|
|
386
401
|
},
|
|
387
402
|
});
|
|
388
403
|
|
package/src/codemode/worker.js
CHANGED
|
@@ -344,7 +344,7 @@ var PRELUDE_SOURCE = String.raw`(function (bridge, toolsJson, storeJson) {
|
|
|
344
344
|
stringify({
|
|
345
345
|
name: "Error",
|
|
346
346
|
message:
|
|
347
|
-
"The script is waiting on a promise that can never settle: no tool call
|
|
347
|
+
"The script is waiting on a promise that can never settle: no tool call and no sleep is pending.",
|
|
348
348
|
}),
|
|
349
349
|
);
|
|
350
350
|
return true;
|
package/src/lib/tool-bus.ts
CHANGED
|
@@ -55,6 +55,7 @@ export interface StructuredToolDefinition<
|
|
|
55
55
|
signal: AbortSignal | undefined,
|
|
56
56
|
onUpdate: AgentToolUpdateCallback<TDetails> | undefined,
|
|
57
57
|
ctx: ExtensionContext,
|
|
58
|
+
callOptions?: ToolCallOptions,
|
|
58
59
|
): Promise<AgentToolResult<TDetails> & { structuredResult: StructuredResult<Static<TOutput>> }>;
|
|
59
60
|
}
|
|
60
61
|
|
|
@@ -111,16 +112,37 @@ export interface CodemodeToolDefinition<
|
|
|
111
112
|
): Promise<StructuredResult<Static<TOutput>>>;
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
/**
|
|
116
|
+
* 工具 `execute` 的额外调用选项:只有本仓库自己的调用路径会传(目前是 codemode 的嵌套
|
|
117
|
+
* 调用),pi 直接调用工具时它永远是 `undefined`。因此在 pi 的 `ToolDefinition` 之外,
|
|
118
|
+
* 总线单独把 `execute` 的签名放宽一个可选参数。
|
|
119
|
+
*/
|
|
120
|
+
export interface ToolCallOptions {
|
|
121
|
+
/** 本次调用不可请求用户审批:需要授权的操作直接失败,不弹审批框。 */
|
|
122
|
+
unapprovable?: boolean;
|
|
123
|
+
}
|
|
124
|
+
|
|
114
125
|
/**
|
|
115
126
|
* 注册后的定义:`codemodeOnly` 标出未交给 pi 的脚本工具。`structuredSchema` 供 codemode
|
|
116
127
|
* 渲染脚本侧返回类型;`scripted` 区分「`execute` 直接返回结构化结果」的脚本工具与经配置
|
|
117
128
|
* 转成 codemode-only 的普通工具(后者仍返回 `AgentToolResult`,结构化结果在
|
|
118
129
|
* `structuredResult` 里)。
|
|
119
130
|
*/
|
|
120
|
-
export type RegisteredToolDefinition =
|
|
131
|
+
export type RegisteredToolDefinition = Omit<
|
|
132
|
+
ToolDefinition<TSchema, unknown, unknown>,
|
|
133
|
+
"execute"
|
|
134
|
+
> & {
|
|
121
135
|
structuredSchema?: TSchema;
|
|
122
136
|
codemodeOnly?: boolean;
|
|
123
137
|
scripted?: true;
|
|
138
|
+
execute(
|
|
139
|
+
toolCallId: string,
|
|
140
|
+
params: unknown,
|
|
141
|
+
signal: AbortSignal | undefined,
|
|
142
|
+
onUpdate: AgentToolUpdateCallback<unknown> | undefined,
|
|
143
|
+
ctx: ExtensionContext,
|
|
144
|
+
callOptions?: ToolCallOptions,
|
|
145
|
+
): Promise<AgentToolResult<unknown>>;
|
|
124
146
|
};
|
|
125
147
|
|
|
126
148
|
export interface ToolBus {
|
|
@@ -416,6 +438,8 @@ export function createToolBus(pi: ExtensionAPI, options: ToolBusOptions = {}): T
|
|
|
416
438
|
options.signal,
|
|
417
439
|
options.onUpdate,
|
|
418
440
|
options.ctx,
|
|
441
|
+
// codemode 的嵌套调用弹不了审批框:需要授权的操作直接失败
|
|
442
|
+
{ unapprovable: true },
|
|
419
443
|
);
|
|
420
444
|
if (definition.scripted === true) {
|
|
421
445
|
return checkScriptedResult(name, definition.structuredSchema, result);
|
package/src/lib/write-guard.ts
CHANGED
|
@@ -139,6 +139,8 @@ export interface WriteGuardOptions {
|
|
|
139
139
|
policy: RequestPolicy;
|
|
140
140
|
/** 工具调用的中止信号:透传给审批对话框,工具被取消时对话框一起关掉。 */
|
|
141
141
|
signal?: AbortSignal;
|
|
142
|
+
/** 本次调用不可请求用户审批(codemode 的嵌套调用):区外写入直接失败,不弹框。 */
|
|
143
|
+
unapprovable?: boolean;
|
|
142
144
|
}
|
|
143
145
|
|
|
144
146
|
/**
|
|
@@ -164,6 +166,13 @@ export async function guardWriteAccess(
|
|
|
164
166
|
throw new Error(`user deny ${opts.toolName}: blocked`);
|
|
165
167
|
}
|
|
166
168
|
|
|
169
|
+
// codemode 内不弹审批:那次嵌套调用只经 codemode 暴露,弹不了框,因此直接失败并提示直调
|
|
170
|
+
if (opts.unapprovable === true) {
|
|
171
|
+
throw new Error(
|
|
172
|
+
`${opts.toolName} needs user approval to write outside the workspace, which codemode cannot ask for. Call the tool directly instead of from a script.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
167
176
|
if (process.platform === "win32") {
|
|
168
177
|
// Windows 上退化为「只能写工作区」:无沙箱兜底,工作区外写入一律拒绝,
|
|
169
178
|
// 不提供审批路径。
|
package/src/opencode/bash.ts
CHANGED
|
@@ -95,7 +95,7 @@ export function registerBashTool(bus: ToolBus, pi: ExtensionAPI, runtime: BwrapR
|
|
|
95
95
|
},
|
|
96
96
|
{ additionalProperties: false },
|
|
97
97
|
),
|
|
98
|
-
async execute(id, params, signal, onUpdate, ctx) {
|
|
98
|
+
async execute(id, params, signal, onUpdate, ctx, callOptions) {
|
|
99
99
|
const timeout = params.timeout ?? BASH_DEFAULT_TIMEOUT_MS;
|
|
100
100
|
if (!Number.isFinite(timeout) || timeout <= 0 || timeout > BASH_MAX_TIMEOUT_MS) {
|
|
101
101
|
throw new Error(`timeout must be between 1 and ${BASH_MAX_TIMEOUT_MS} milliseconds`);
|
|
@@ -115,6 +115,7 @@ export function registerBashTool(bus: ToolBus, pi: ExtensionAPI, runtime: BwrapR
|
|
|
115
115
|
description: params.description,
|
|
116
116
|
signal,
|
|
117
117
|
onUpdate,
|
|
118
|
+
unapprovable: callOptions?.unapprovable,
|
|
118
119
|
});
|
|
119
120
|
} catch (error) {
|
|
120
121
|
if (!(error instanceof Error)) {
|