@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.
@@ -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
- const message = decodeHostMessage(value);
200
- if (!message.ok || message.frame.t !== "result") {
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(result.id),
210
- result.ok ? vm.true : vm.false,
211
- result.ok
212
- ? result.value === undefined
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(result.value))
215
- : vm.newString(result.error),
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
- const message = decodeHostMessage(value);
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, message.frame);
317
+ void runScript(wasm, value);
270
318
  });
271
319
  }
272
320
 
@@ -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 = ToolDefinition<TSchema, unknown, unknown> & {
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);
@@ -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
  // 不提供审批路径。
@@ -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)) {