@trim21/personal-pi-extensions 0.1.20261012010044 → 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 +14 -6
- package/package.json +1 -1
- package/src/bwrap/runtime.ts +15 -0
- package/src/claude-code/shell.ts +2 -1
- package/src/codemode/declarations.ts +1 -0
- package/src/codemode/fs.ts +3 -0
- package/src/codemode/prelude.ts +28 -3
- package/src/codemode/protocol.ts +4 -38
- package/src/codemode/sandbox.ts +32 -3
- package/src/codemode/source.ts +21 -2
- package/src/codemode/tool.ts +21 -4
- package/src/codemode/worker.js +86 -683
- package/src/codemode/worker.ts +61 -13
- package/src/lib/tool-bus.ts +25 -1
- package/src/lib/write-guard.ts +9 -0
- package/src/opencode/bash.ts +2 -1
package/src/codemode/worker.ts
CHANGED
|
@@ -13,9 +13,9 @@ import { parentPort, workerData } from "node:worker_threads";
|
|
|
13
13
|
|
|
14
14
|
import { JSException, type JSValueHandle, MAX_STACK_SIZE, QuickJS } from "quickjs-wasi";
|
|
15
15
|
|
|
16
|
+
import { isRecord } from "../lib/narrow.js";
|
|
16
17
|
import { PRELUDE_SOURCE } from "./prelude.js";
|
|
17
18
|
import {
|
|
18
|
-
decodeHostMessage,
|
|
19
19
|
type HostMessage,
|
|
20
20
|
MEMORY_LIMIT_BYTES,
|
|
21
21
|
type ScriptError,
|
|
@@ -46,6 +46,28 @@ function post(message: WorkerMessage): void {
|
|
|
46
46
|
parentPort?.postMessage(message);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* 主线程发来的消息由主线程自己构造,形状由 `HostMessage` 类型约束,所以这里只做分派、
|
|
51
|
+
* 不重复 typebox 校验;但形状对不上就是内部 bug,得报错而不是静默丢弃——静默丢弃只会
|
|
52
|
+
* 让脚本永远等在 pending 上,症状比故障本身难查得多。
|
|
53
|
+
*/
|
|
54
|
+
function isResultMessage(value: unknown): value is Extract<HostMessage, { t: "result" }> {
|
|
55
|
+
return isRecord(value) && value.t === "result" && typeof value.id === "number";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isStartMessage(value: unknown): value is Extract<HostMessage, { t: "start" }> {
|
|
59
|
+
return isRecord(value) && value.t === "start";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 畸形消息的预览:截断成一行,序列化不了(循环引用、undefined 等)就退化成 String()。 */
|
|
63
|
+
function previewMessage(value: unknown): string {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.stringify(value).slice(0, 200);
|
|
66
|
+
} catch {
|
|
67
|
+
return String(value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
function emptyWrites(): StoreWrites {
|
|
50
72
|
return { set: {}, delete: [] };
|
|
51
73
|
}
|
|
@@ -111,6 +133,8 @@ async function runScript(wasm: object, start: Extract<HostMessage, { t: "start"
|
|
|
111
133
|
) => {
|
|
112
134
|
const stringOr = (value: JSValueHandle | undefined, fallback: string): string =>
|
|
113
135
|
value === undefined || value.isUndefined ? fallback : value.toString();
|
|
136
|
+
const numberOr = (value: JSValueHandle | undefined, fallback: number): number =>
|
|
137
|
+
value === undefined || value.isUndefined ? fallback : value.toNumber();
|
|
114
138
|
switch (kind.toString()) {
|
|
115
139
|
case "call": {
|
|
116
140
|
post({
|
|
@@ -138,6 +162,22 @@ async function runScript(wasm: object, start: Extract<HostMessage, { t: "start"
|
|
|
138
162
|
});
|
|
139
163
|
break;
|
|
140
164
|
}
|
|
165
|
+
case "sleep": {
|
|
166
|
+
// 用 worker 线程自己的定时器:不经主线程,也没有消息往返
|
|
167
|
+
const id = a.toNumber();
|
|
168
|
+
const ms = numberOr(b, 0);
|
|
169
|
+
setTimeout(() => {
|
|
170
|
+
try {
|
|
171
|
+
vm.withScope(() => {
|
|
172
|
+
vm.callFunction(timerDone, api, vm.newNumber(id)).dispose();
|
|
173
|
+
});
|
|
174
|
+
} catch {
|
|
175
|
+
/* VM 已中止 */
|
|
176
|
+
}
|
|
177
|
+
drain();
|
|
178
|
+
}, ms);
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
141
181
|
case "done": {
|
|
142
182
|
const writes =
|
|
143
183
|
c === undefined || c.isUndefined ? emptyWrites() : parseWrites(c.toString());
|
|
@@ -186,6 +226,7 @@ async function runScript(wasm: object, start: Extract<HostMessage, { t: "start"
|
|
|
186
226
|
),
|
|
187
227
|
);
|
|
188
228
|
const settle = api.getProp("settle");
|
|
229
|
+
const timerDone = api.getProp("timerDone");
|
|
189
230
|
const run = api.getProp("run");
|
|
190
231
|
const stalled = api.getProp("stalled");
|
|
191
232
|
|
|
@@ -196,23 +237,31 @@ async function runScript(wasm: object, start: Extract<HostMessage, { t: "start"
|
|
|
196
237
|
};
|
|
197
238
|
|
|
198
239
|
port.on("message", (value: unknown) => {
|
|
199
|
-
|
|
200
|
-
|
|
240
|
+
if (!isResultMessage(value)) {
|
|
241
|
+
// 主线程只会发 result:对不上就是宿主自己的 bug,报出去而不是静默让脚本挂住
|
|
242
|
+
post({
|
|
243
|
+
t: "done",
|
|
244
|
+
ok: false,
|
|
245
|
+
error: {
|
|
246
|
+
kind: "sandbox",
|
|
247
|
+
message: `codemode worker: unexpected host message: ${previewMessage(value)}`,
|
|
248
|
+
},
|
|
249
|
+
writes: emptyWrites(),
|
|
250
|
+
});
|
|
201
251
|
return;
|
|
202
252
|
}
|
|
203
|
-
const result = message.frame;
|
|
204
253
|
try {
|
|
205
254
|
vm.withScope(() => {
|
|
206
255
|
vm.callFunction(
|
|
207
256
|
settle,
|
|
208
257
|
api,
|
|
209
|
-
vm.newNumber(
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
?
|
|
258
|
+
vm.newNumber(value.id),
|
|
259
|
+
value.ok ? vm.true : vm.false,
|
|
260
|
+
value.ok
|
|
261
|
+
? value.value === undefined
|
|
213
262
|
? vm.undefined
|
|
214
|
-
: vm.newString(JSON.stringify(
|
|
215
|
-
: vm.newString(
|
|
263
|
+
: vm.newString(JSON.stringify(value.value))
|
|
264
|
+
: vm.newString(value.error),
|
|
216
265
|
);
|
|
217
266
|
});
|
|
218
267
|
} catch {
|
|
@@ -256,8 +305,7 @@ function main(): void {
|
|
|
256
305
|
const { wasm } = workerData as WorkerBootstrap;
|
|
257
306
|
// 主线程先发 start;worker 收到后才实例化 VM(wasm 已在主线程编译好)
|
|
258
307
|
port.once("message", (value: unknown) => {
|
|
259
|
-
|
|
260
|
-
if (!message.ok || message.frame.t !== "start") {
|
|
308
|
+
if (!isStartMessage(value)) {
|
|
261
309
|
post({
|
|
262
310
|
t: "done",
|
|
263
311
|
ok: false,
|
|
@@ -266,7 +314,7 @@ function main(): void {
|
|
|
266
314
|
});
|
|
267
315
|
return;
|
|
268
316
|
}
|
|
269
|
-
void runScript(wasm,
|
|
317
|
+
void runScript(wasm, value);
|
|
270
318
|
});
|
|
271
319
|
}
|
|
272
320
|
|
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)) {
|