@webskill/sdk 0.2.6 → 0.2.8

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/dist/node.js CHANGED
@@ -1,6 +1,2066 @@
1
- import { i as SKILL_MANIFEST_FILE, r as SKILLS_LOCKFILE } from "./dist-BQzncxXg.js";
2
- import { T as createScriptContext } from "./dist-BXpDDZpR.js";
1
+ import { A as resolveArchiveLimits, C as messageOf, D as readResponseWithLimit, E as parseSkillPackManifest, M as unzipWithLimits, N as validateSkills, P as verifyManifest, T as parseSkillMarkdown, b as exportSkills, d as assertRemoteUrlAllowed, f as assertSafePathSegment, h as buildManifest, i as SKILL_MANIFEST_FILE, j as resolveInsideRoot, p as atomicWriteText, r as SKILLS_LOCKFILE, s as SKILL_PACK_FILE, u as WebSkillError, x as isValidSkillName } from "./dist-BQzncxXg.js";
2
+ import { C as bridgeError, I as normalizeToolContent, L as normalizeToolError, N as networkPolicyLibSource, R as parseBridgeRequest, S as WebSkillRuntime, T as createScriptContext, c as FsArtifactStore, l as FsMemoryStore, o as CapabilityApproval } from "./dist-B9VLwOME.js";
3
3
  import { i as probeLlmCapabilities } from "./env--jJB-TSX-04klhTYi.js";
4
- import { a as NodeScriptExecutor, c as SandboxedScriptExecutor, d as readArchiveManifest, i as NodeFS, l as SkillManager, n as FileArtifactStore, o as OxcSchemaInferer, r as FileMemoryStore, s as ProcessSandboxExecutor, t as CliUiBridge, u as exportArchive } from "./dist-DWFDb1Ww.js";
4
+ import { createRequire } from "node:module";
5
+ import { unzipSync, zipSync } from "fflate";
6
+ import { existsSync, promises, realpathSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ import { fileURLToPath, pathToFileURL } from "node:url";
10
+ import { format, promisify } from "node:util";
11
+ import { Worker } from "node:worker_threads";
12
+ import { mkdtemp, rm } from "node:fs/promises";
13
+ import { execFile, fork } from "node:child_process";
14
+ import { createInterface } from "node:readline/promises";
15
+ import { createHash } from "node:crypto";
5
16
 
6
- export { CliUiBridge, FileArtifactStore, FileMemoryStore, NodeFS, NodeScriptExecutor, OxcSchemaInferer, ProcessSandboxExecutor, SKILLS_LOCKFILE, SKILL_MANIFEST_FILE, SandboxedScriptExecutor, SkillManager, createScriptContext, exportArchive, probeLlmCapabilities, readArchiveManifest };
17
+ //#region ../node/dist/index.js
18
+ const toPlatform$4 = (p) => p.split("/").join(path.sep);
19
+ const toPosix = (p) => p.split(path.sep).join("/");
20
+ const isEnoent = (e) => typeof e === "object" && e !== null && e.code === "ENOENT";
21
+ /** realpath 最近现存祖先并拼回剩余段(写入目标尚不存在时的等价判定) */
22
+ async function realpathNearest(platformPath) {
23
+ let current = platformPath;
24
+ const rest = [];
25
+ for (;;) try {
26
+ const real = await promises.realpath(current);
27
+ return rest.length === 0 ? real : [real, ...rest].join(path.sep);
28
+ } catch (e) {
29
+ if (!isEnoent(e)) throw e;
30
+ rest.unshift(path.basename(current));
31
+ const parent = path.dirname(current);
32
+ if (parent === current) return platformPath;
33
+ current = parent;
34
+ }
35
+ }
36
+ /**
37
+ * 基于 node:fs/promises 的 FileSystemProvider;接受 `/` 风格路径,写入自动创建父目录。
38
+ * 可选 root 模式:构造传入 root 后,read/write 操作先做 realpath 包含校验
39
+ * (root 与目标都 realpath 后前缀比对),经符号链接逃逸 root → FS_PATH_OUTSIDE_ROOT。
40
+ */
41
+ var NodeFS = class NodeFS {
42
+ kind = "node";
43
+ #root;
44
+ constructor(deps = {}) {
45
+ this.#root = deps.root;
46
+ }
47
+ /** root 模式:全部方法(read/write/exists/stat/list/mkdir/remove/rename)目标 realpath 必须落在 root realpath 前缀内 */
48
+ withRoot(root) {
49
+ return new NodeFS({ root });
50
+ }
51
+ async #assertContained(p) {
52
+ if (!this.#root) return;
53
+ const rootReal = await promises.realpath(toPlatform$4(this.#root));
54
+ const targetReal = await realpathNearest(toPlatform$4(p));
55
+ if (targetReal !== rootReal && !targetReal.startsWith(rootReal + path.sep)) throw new WebSkillError("FS_PATH_OUTSIDE_ROOT", `Path escapes root via symbolic link: ${p} (resolves outside ${this.#root})`);
56
+ }
57
+ async readText(p) {
58
+ await this.#assertContained(p);
59
+ try {
60
+ return await promises.readFile(toPlatform$4(p), "utf8");
61
+ } catch (e) {
62
+ throw this.#mapError(e, p);
63
+ }
64
+ }
65
+ async writeText(p, content) {
66
+ await this.#assertContained(p);
67
+ const target = toPlatform$4(p);
68
+ await promises.mkdir(path.dirname(target), { recursive: true });
69
+ await promises.writeFile(target, content, "utf8");
70
+ }
71
+ async appendText(p, content) {
72
+ await this.#assertContained(p);
73
+ const target = toPlatform$4(p);
74
+ await promises.mkdir(path.dirname(target), { recursive: true });
75
+ await promises.appendFile(target, content, "utf8");
76
+ }
77
+ async readBinary(p) {
78
+ await this.#assertContained(p);
79
+ try {
80
+ return await promises.readFile(toPlatform$4(p));
81
+ } catch (e) {
82
+ throw this.#mapError(e, p);
83
+ }
84
+ }
85
+ async writeBinary(p, content) {
86
+ await this.#assertContained(p);
87
+ const target = toPlatform$4(p);
88
+ await promises.mkdir(path.dirname(target), { recursive: true });
89
+ await promises.writeFile(target, content);
90
+ }
91
+ async exists(p) {
92
+ await this.#assertContained(p);
93
+ try {
94
+ await promises.access(toPlatform$4(p));
95
+ return true;
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+ async stat(p) {
101
+ await this.#assertContained(p);
102
+ try {
103
+ const s = await promises.stat(toPlatform$4(p));
104
+ return {
105
+ path: toPosix(p),
106
+ type: s.isDirectory() ? "directory" : "file",
107
+ size: s.isFile() ? s.size : void 0,
108
+ mtimeMs: s.mtimeMs
109
+ };
110
+ } catch (e) {
111
+ throw this.#mapError(e, p);
112
+ }
113
+ }
114
+ async list(p) {
115
+ await this.#assertContained(p);
116
+ let dirents;
117
+ try {
118
+ dirents = await promises.readdir(toPlatform$4(p), { withFileTypes: true });
119
+ } catch (e) {
120
+ throw this.#mapError(e, p);
121
+ }
122
+ return dirents.map((d) => ({
123
+ path: toPosix(path.join(toPlatform$4(p), d.name)),
124
+ type: d.isDirectory() ? "directory" : "file"
125
+ }));
126
+ }
127
+ async mkdir(p) {
128
+ await this.#assertContained(p);
129
+ await promises.mkdir(toPlatform$4(p), { recursive: true });
130
+ }
131
+ async remove(p, options) {
132
+ await this.#assertContained(p);
133
+ try {
134
+ await promises.rm(toPlatform$4(p), { recursive: options?.recursive ?? false });
135
+ } catch (e) {
136
+ throw this.#mapError(e, p);
137
+ }
138
+ }
139
+ async rename(from, to) {
140
+ await this.#assertContained(from);
141
+ await this.#assertContained(to);
142
+ await promises.mkdir(path.dirname(toPlatform$4(to)), { recursive: true });
143
+ try {
144
+ await promises.rename(toPlatform$4(from), toPlatform$4(to));
145
+ } catch (e) {
146
+ throw this.#mapError(e, from);
147
+ }
148
+ }
149
+ #mapError(e, p) {
150
+ if (isEnoent(e)) return new WebSkillError("FS_NOT_FOUND", `Path not found: ${p}`);
151
+ return e;
152
+ }
153
+ };
154
+ const baseName$2 = (p) => p.split("/").pop() ?? p;
155
+ /**
156
+ * 进程内脚本执行器。接口按沙箱语义设计(context 无隐式宿主访问),
157
+ * worker_threads 沙箱见 deferred-items D1。
158
+ */
159
+ var NodeScriptExecutor = class {
160
+ #fs;
161
+ constructor(fs) {
162
+ this.#fs = fs;
163
+ }
164
+ /** 定位 scripts/<name>.ts|js;冲突/缺失抛结构化错误 */
165
+ async #locateScript(skillRoot, scriptName) {
166
+ const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
167
+ const jsPath = `${skillRoot}/scripts/${scriptName}.js`;
168
+ const [hasTs, hasJs] = await Promise.all([this.#fs.exists(tsPath), this.#fs.exists(jsPath)]);
169
+ if (hasTs && hasJs) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Script "${scriptName}" is ambiguous: both .ts and .js exist under ${skillRoot}/scripts`);
170
+ if (!hasTs && !hasJs) throw new WebSkillError("TOOL_NOT_FOUND", `Script "${scriptName}" not found under ${skillRoot}/scripts`);
171
+ return hasTs ? tsPath : jsPath;
172
+ }
173
+ async #loadModule(scriptPath) {
174
+ if (scriptPath.endsWith(".ts")) {
175
+ const source = await this.#fs.readText(scriptPath);
176
+ const dir = await promises.mkdtemp(path.join(tmpdir(), "webskill-ts-"));
177
+ try {
178
+ const file = path.join(dir, "script.ts");
179
+ await promises.writeFile(file, source);
180
+ return await import(`${pathToFileURL(file).href}?t=${Date.now()}`);
181
+ } finally {
182
+ await promises.rm(dir, {
183
+ recursive: true,
184
+ force: true
185
+ });
186
+ }
187
+ }
188
+ const source = await this.#fs.readText(scriptPath);
189
+ return await import(`data:text/javascript;charset=utf-8,${encodeURIComponent(`${source}\n//# ${Date.now()}`)}`);
190
+ }
191
+ async loadDefinition(skillRoot, scriptName) {
192
+ const scriptPath = await this.#locateScript(skillRoot, scriptName);
193
+ const skillName = baseName$2(skillRoot);
194
+ let module;
195
+ try {
196
+ module = await this.#loadModule(scriptPath);
197
+ } catch (e) {
198
+ throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${e instanceof Error ? e.message : String(e)}`, e);
199
+ }
200
+ return {
201
+ name: `${skillName}__${scriptName}`,
202
+ description: typeof module.description === "string" ? module.description : void 0,
203
+ inputSchema: typeof module.inputSchema === "object" && module.inputSchema !== null ? module.inputSchema : void 0,
204
+ source: "script",
205
+ skillName
206
+ };
207
+ }
208
+ async execute(input) {
209
+ const { skillRoot, scriptName, args, context, timeoutMs } = input;
210
+ let scriptPath;
211
+ try {
212
+ scriptPath = await this.#locateScript(skillRoot, scriptName);
213
+ } catch (e) {
214
+ return this.#failure(e);
215
+ }
216
+ let module;
217
+ try {
218
+ module = await this.#loadModule(scriptPath);
219
+ } catch (e) {
220
+ return this.#failure(e);
221
+ }
222
+ if (typeof module.run !== "function") return {
223
+ ok: false,
224
+ content: [],
225
+ error: {
226
+ code: "TOOL_EXECUTION_FAILED",
227
+ message: `Script "${scriptName}" does not export a run function`
228
+ }
229
+ };
230
+ const missing = this.#missingRequiredArgs(module.inputSchema, args);
231
+ if (missing.length > 0) return {
232
+ ok: false,
233
+ content: [],
234
+ error: {
235
+ code: "TOOL_EXECUTION_FAILED",
236
+ message: `Missing required argument(s): ${missing.join(", ")}`
237
+ }
238
+ };
239
+ const stdout = [];
240
+ const stderr = [];
241
+ const originals = {
242
+ log: console.log,
243
+ info: console.info,
244
+ debug: console.debug,
245
+ warn: console.warn,
246
+ error: console.error
247
+ };
248
+ console.log = console.info = console.debug = (...a) => stdout.push(format(...a));
249
+ console.warn = console.error = (...a) => stderr.push(format(...a));
250
+ let timer;
251
+ try {
252
+ const runFn = module.run;
253
+ const timeout = new Promise((_, reject) => {
254
+ timer = setTimeout(() => reject(new WebSkillError("RUN_TIMEOUT", `Script "${scriptName}" timed out after ${timeoutMs}ms`)), timeoutMs);
255
+ });
256
+ const content = normalizeToolContent(await Promise.race([Promise.resolve().then(() => runFn(args, context)), timeout]));
257
+ if (stdout.length > 0) content.push({
258
+ type: "text",
259
+ text: stdout.join("\n")
260
+ });
261
+ return {
262
+ ok: true,
263
+ content
264
+ };
265
+ } catch (e) {
266
+ return this.#failure(e, stderr);
267
+ } finally {
268
+ if (timer) clearTimeout(timer);
269
+ console.log = originals.log;
270
+ console.info = originals.info;
271
+ console.debug = originals.debug;
272
+ console.warn = originals.warn;
273
+ console.error = originals.error;
274
+ }
275
+ }
276
+ #missingRequiredArgs(schema, args) {
277
+ if (typeof schema !== "object" || schema === null) return [];
278
+ const required = schema.required;
279
+ if (!Array.isArray(required)) return [];
280
+ return required.filter((key) => typeof key === "string" && args[key] === void 0);
281
+ }
282
+ #failure(e, stderr = []) {
283
+ const isTimeout = e instanceof WebSkillError && e.code === "RUN_TIMEOUT";
284
+ const code = e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED";
285
+ const base = e instanceof Error ? e.message : String(e);
286
+ const stderrSummary = stderr.length > 0 ? ` | stderr: ${stderr.join(" | ").slice(0, 500)}` : "";
287
+ return {
288
+ ok: false,
289
+ content: [],
290
+ error: {
291
+ code: isTimeout ? "RUN_TIMEOUT" : code,
292
+ message: `${base}${stderrSummary}`
293
+ }
294
+ };
295
+ }
296
+ };
297
+ const baseName$1 = (p) => p.split("/").pop() ?? p;
298
+ const toPlatform$3 = (p) => p.split("/").join(path.sep);
299
+ /**
300
+ * 网络策略判定函数源码(注入 Worker;匹配逻辑单一来源在
301
+ * runtime/sandbox/networkPolicy.ts,Worker 线程不走 vitest 别名故注入而非 import)
302
+ */
303
+ const NETWORK_POLICY_LIB$1 = networkPolicyLibSource();
304
+ /** Worker 入口文件:src 为同目录 .ts;node dist 为 dist/executor/;sdk dist 为平铺 dist/ */
305
+ function workerEntryPath() {
306
+ const dir = path.dirname(fileURLToPath(import.meta.url));
307
+ const candidates = [
308
+ path.join(dir, "sandboxWorkerEntry.js"),
309
+ path.join(dir, "sandboxWorkerEntry.ts"),
310
+ path.join(dir, "executor", "sandboxWorkerEntry.js")
311
+ ];
312
+ for (const candidate of candidates) if (existsSync(candidate)) return candidate;
313
+ throw new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox worker entry not found (tried: ${candidates.join(", ")})`);
314
+ }
315
+ /**
316
+ * D1:worker_threads 沙箱脚本执行器。
317
+ * 每次执行独立 Worker(resourceLimits + env 白名单),loadDefinition 同样在
318
+ * Worker 内完成(禁止主进程 import 不可信脚本);超时 terminate(可杀同步死循环)。
319
+ * 能力桥协议与浏览器同一来源(runtime/sandbox/bridgeProtocol)。
320
+ *
321
+ * 诚实标注:本执行器做的是**能力面收敛**(网络策略、模块 allowlist、资源限额、
322
+ * 超时强杀),**不是安全边界**——Worker 内脚本与宿主共享进程;
323
+ * 已知主动逃逸面(process.binding/_linkedBinding/dlopen/openStdin/reallyExit/abort)
324
+ * 已在入口删除(0.2.3),常规 import 拦截之外不承诺防御其它宿主共享面;
325
+ * 禁止假定其可隔离不可信脚本(隔离级需求用 ProcessSandboxExecutor)。
326
+ */
327
+ var SandboxedScriptExecutor = class {
328
+ #fs;
329
+ #options;
330
+ #capabilities;
331
+ #networkPolicy;
332
+ #approval;
333
+ constructor(fs, options = {}) {
334
+ this.#fs = fs;
335
+ this.#options = options;
336
+ this.#capabilities = {
337
+ readReference: options.capabilities?.readReference ?? true,
338
+ writeArtifact: options.capabilities?.writeArtifact ?? true,
339
+ confirm: options.capabilities?.confirm ?? true
340
+ };
341
+ this.#networkPolicy = options.networkPolicy ?? "deny-all";
342
+ this.#approval = new CapabilityApproval({
343
+ ...options.uiBridge ? { uiBridge: options.uiBridge } : {},
344
+ ...options.approvalScope ? { scope: options.approvalScope } : {}
345
+ });
346
+ }
347
+ async #locateScript(skillRoot, scriptName) {
348
+ const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
349
+ const jsPath = `${skillRoot}/scripts/${scriptName}.js`;
350
+ const [hasTs, hasJs] = await Promise.all([this.#fs.exists(tsPath), this.#fs.exists(jsPath)]);
351
+ if (hasTs && hasJs) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Script "${scriptName}" is ambiguous: both .ts and .js exist under ${skillRoot}/scripts`);
352
+ if (!hasTs && !hasJs) throw new WebSkillError("TOOL_NOT_FOUND", `Script "${scriptName}" not found under ${skillRoot}/scripts`);
353
+ return hasTs ? tsPath : jsPath;
354
+ }
355
+ async loadDefinition(skillRoot, scriptName) {
356
+ const scriptPath = await this.#locateScript(skillRoot, scriptName);
357
+ const skillName = baseName$1(skillRoot);
358
+ const result = await this.#runWorker({
359
+ mode: "load",
360
+ scriptPath: toPlatform$3(scriptPath),
361
+ scriptName,
362
+ scriptSource: await this.#fs.readText(scriptPath)
363
+ });
364
+ if (!result.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${result.error?.message ?? "unknown error"}`);
365
+ return {
366
+ name: `${skillName}__${scriptName}`,
367
+ description: result.definition?.description,
368
+ inputSchema: result.definition?.inputSchema,
369
+ source: "script",
370
+ skillName
371
+ };
372
+ }
373
+ async execute(input) {
374
+ const { skillRoot, scriptName, args, context, timeoutMs } = input;
375
+ let scriptPath;
376
+ try {
377
+ scriptPath = await this.#locateScript(skillRoot, scriptName);
378
+ } catch (e) {
379
+ return {
380
+ ok: false,
381
+ content: [],
382
+ error: {
383
+ code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
384
+ message: messageOf(e)
385
+ }
386
+ };
387
+ }
388
+ try {
389
+ const result = await this.#runWorker({
390
+ mode: "execute",
391
+ scriptPath: toPlatform$3(scriptPath),
392
+ scriptName,
393
+ skillName: context.skillName,
394
+ runId: context.runId,
395
+ args,
396
+ scriptSource: await this.#fs.readText(scriptPath)
397
+ }, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
398
+ if (!result.ok) {
399
+ const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
400
+ const normalized = normalizeToolError(result.error?.code, result.error?.message ?? "script execution failed");
401
+ return {
402
+ ok: false,
403
+ content: [],
404
+ error: {
405
+ code: normalized.code,
406
+ message: `${normalized.message}${stderrSummary}`
407
+ }
408
+ };
409
+ }
410
+ const content = normalizeToolContent(result.value);
411
+ if (result.stdout?.length) content.push({
412
+ type: "text",
413
+ text: result.stdout.join("\n")
414
+ });
415
+ return {
416
+ ok: true,
417
+ content
418
+ };
419
+ } catch (e) {
420
+ return {
421
+ ok: false,
422
+ content: [],
423
+ error: {
424
+ code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
425
+ message: messageOf(e)
426
+ }
427
+ };
428
+ }
429
+ }
430
+ /** 起独立 Worker 执行任务;超时 terminate(同步死循环可杀);非零退出码 → TOOL_EXECUTION_FAILED */
431
+ #runWorker(workerData, timeoutMs = 3e4, onBridge, onNetworkBlocked) {
432
+ const envWhitelist = this.#options.envWhitelist ?? [];
433
+ const env = Object.fromEntries(envWhitelist.filter((key) => process.env[key] !== void 0).map((key) => [key, process.env[key]]));
434
+ return new Promise((resolve, reject) => {
435
+ const worker = new Worker(workerEntryPath(), {
436
+ workerData: {
437
+ ...workerData,
438
+ networkPolicy: this.#networkPolicy,
439
+ networkPolicyLib: NETWORK_POLICY_LIB$1,
440
+ allowedModules: this.#options.allowedModules ?? []
441
+ },
442
+ env,
443
+ resourceLimits: this.#options.resourceLimits ?? {
444
+ maxOldGenerationSizeMb: 64,
445
+ maxYoungGenerationSizeMb: 16
446
+ }
447
+ });
448
+ let settled = false;
449
+ const timer = setTimeout(() => {
450
+ if (settled) return;
451
+ settled = true;
452
+ worker.terminate();
453
+ reject(new WebSkillError("RUN_TIMEOUT", `Sandboxed script timed out after ${timeoutMs}ms (worker terminated)`));
454
+ }, timeoutMs);
455
+ const done = (fn) => {
456
+ if (settled) return;
457
+ settled = true;
458
+ clearTimeout(timer);
459
+ fn();
460
+ };
461
+ worker.on("message", (msg) => {
462
+ if (msg?.type === "network-blocked") {
463
+ if (typeof msg.host === "string") onNetworkBlocked?.(msg.host);
464
+ return;
465
+ }
466
+ if (msg?.type === "bridge" && onBridge) {
467
+ const request = parseBridgeRequest(msg.request);
468
+ const respond = (response) => worker.postMessage({
469
+ type: "bridge-response",
470
+ response
471
+ });
472
+ if (!request) {
473
+ respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
474
+ return;
475
+ }
476
+ onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf(e))));
477
+ return;
478
+ }
479
+ if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
480
+ });
481
+ worker.on("error", (e) => done(() => reject(e)));
482
+ worker.on("exit", (code) => {
483
+ if (!settled) done(() => reject(new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox worker exited ${code === null ? "by signal" : `with code ${code}`} without producing a result`)));
484
+ });
485
+ });
486
+ }
487
+ /** 能力桥宿主侧处理:能力关闭/授权拒绝即 TOOL_UNSUPPORTED;判定逻辑共用 runtime/sandbox/approval */
488
+ async #handleBridge(request, context) {
489
+ const gate = async (capability, message, details) => {
490
+ const decision = await this.#approval.authorize({
491
+ runId: context.runId,
492
+ capability,
493
+ mode: this.#capabilities[capability],
494
+ message,
495
+ ...details !== void 0 ? { details } : {}
496
+ });
497
+ if (decision === "allowed") return void 0;
498
+ return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled` : `Capability "${capability}" was denied by the user`);
499
+ };
500
+ try {
501
+ switch (request.kind) {
502
+ case "readReference": {
503
+ const denied = await gate("readReference", `Script "${context.skillName}" wants to read reference "${request.path}"`, { path: request.path });
504
+ if (denied) return denied;
505
+ return {
506
+ id: request.id,
507
+ ok: true,
508
+ value: await context.readReference(request.path)
509
+ };
510
+ }
511
+ case "writeArtifact": {
512
+ const denied = await gate("writeArtifact", `Script "${context.skillName}" wants to write artifact "${request.path}"`, {
513
+ path: request.path,
514
+ mimeType: request.mimeType
515
+ });
516
+ if (denied) return denied;
517
+ const content = typeof request.content === "string" ? request.content : new Uint8Array(request.content);
518
+ const artifact = await context.writeArtifact(request.path, content, { ...request.mimeType ? { mimeType: request.mimeType } : {} });
519
+ return {
520
+ id: request.id,
521
+ ok: true,
522
+ value: artifact
523
+ };
524
+ }
525
+ case "confirm": {
526
+ if (!context.confirm) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"confirm\" is disabled");
527
+ const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`);
528
+ if (denied) return denied;
529
+ return {
530
+ id: request.id,
531
+ ok: true,
532
+ value: await context.confirm(request.message)
533
+ };
534
+ }
535
+ }
536
+ } catch (e) {
537
+ return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
538
+ }
539
+ }
540
+ };
541
+ const baseName = (p) => p.split("/").pop() ?? p;
542
+ const toPlatform$2 = (p) => p.split("/").join(path.sep);
543
+ const NETWORK_POLICY_LIB = networkPolicyLibSource();
544
+ const readAllow = (p) => [`--allow-fs-read=${p}`, `--allow-fs-read=${realpathSync(p)}`];
545
+ const writeAllow = (p) => [`--allow-fs-write=${p}`, `--allow-fs-write=${realpathSync(p)}`];
546
+ /** 子进程入口文件:src 为同目录 .ts;node dist 为 dist/executor/;sdk dist 为平铺 dist/ */
547
+ function processEntryPath() {
548
+ const dir = path.dirname(fileURLToPath(import.meta.url));
549
+ const candidates = [
550
+ path.join(dir, "processSandboxEntry.js"),
551
+ path.join(dir, "processSandboxEntry.ts"),
552
+ path.join(dir, "executor", "processSandboxEntry.js")
553
+ ];
554
+ for (const candidate of candidates) if (existsSync(candidate)) return candidate;
555
+ throw new WebSkillError("TOOL_EXECUTION_FAILED", `Process sandbox entry not found (tried: ${candidates.join(", ")})`);
556
+ }
557
+ /**
558
+ * child_process.fork + --permission 进程沙箱(真实进程隔离)。
559
+ * 每个子进程以 `--permission --allow-fs-read=<入口目录> --allow-fs-read=<skillRoot>
560
+ * --allow-fs-write=<artifactDir>` 启动:fs 维度由权限模型管控(experimental),
561
+ * 子进程/Worker/addons 默认禁;网络无权限维度仍靠 fetch/WebSocket patch。
562
+ * 能力桥协议与 worker_threads 沙箱同一形态(readReference/writeArtifact/confirm + 授权)。
563
+ * 温池(默认 2):同 skillRoot 复用子进程;执行后 kill 并补位重生;并发上限即池大小。
564
+ * 超时 kill(同步死循环可杀);非零退出码 → TOOL_EXECUTION_FAILED。
565
+ */
566
+ var ProcessSandboxExecutor = class {
567
+ #fs;
568
+ #options;
569
+ #capabilities;
570
+ #networkPolicy;
571
+ #approval;
572
+ #slots = [];
573
+ #waiters = [];
574
+ #disposed = false;
575
+ constructor(fs, options = {}) {
576
+ this.#fs = fs;
577
+ this.#options = options;
578
+ this.#capabilities = {
579
+ readReference: options.capabilities?.readReference ?? true,
580
+ writeArtifact: options.capabilities?.writeArtifact ?? true,
581
+ confirm: options.capabilities?.confirm ?? true
582
+ };
583
+ this.#networkPolicy = options.networkPolicy ?? "deny-all";
584
+ this.#approval = new CapabilityApproval({
585
+ ...options.uiBridge ? { uiBridge: options.uiBridge } : {},
586
+ ...options.approvalScope ? { scope: options.approvalScope } : {}
587
+ });
588
+ }
589
+ get poolSize() {
590
+ return this.#options.poolSize ?? 2;
591
+ }
592
+ /** 池全部子进程销毁(测试收尾/进程退出前调用);排队中的 acquire 一律 reject(不悬挂) */
593
+ async dispose() {
594
+ this.#disposed = true;
595
+ for (const slot of this.#slots) {
596
+ slot.child.kill();
597
+ await rm(slot.artifactDir, {
598
+ recursive: true,
599
+ force: true
600
+ }).catch(() => void 0);
601
+ }
602
+ this.#slots = [];
603
+ const waiters = this.#waiters.splice(0);
604
+ for (const waiter of waiters) waiter.reject(new WebSkillError("TOOL_EXECUTION_FAILED", "Process sandbox executor disposed while a caller was waiting for a pool slot"));
605
+ }
606
+ async #locateScript(skillRoot, scriptName) {
607
+ const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
608
+ const jsPath = `${skillRoot}/scripts/${scriptName}.js`;
609
+ const [hasTs, hasJs] = await Promise.all([this.#fs.exists(tsPath), this.#fs.exists(jsPath)]);
610
+ if (hasTs && hasJs) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Script "${scriptName}" is ambiguous: both .ts and .js exist under ${skillRoot}/scripts`);
611
+ if (!hasTs && !hasJs) throw new WebSkillError("TOOL_NOT_FOUND", `Script "${scriptName}" not found under ${skillRoot}/scripts`);
612
+ return hasTs ? tsPath : jsPath;
613
+ }
614
+ /** 取一个可用子进程(同 key 复用 / 淘汰 idle 重生 / 扩容 / 排队) */
615
+ async #acquire(key) {
616
+ for (;;) {
617
+ if (this.#disposed) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Process sandbox executor is disposed");
618
+ const idle = this.#slots.filter((s) => !s.busy);
619
+ const match = idle.find((s) => s.key === key);
620
+ if (match) {
621
+ match.busy = true;
622
+ return match;
623
+ }
624
+ if (idle.length > 0) {
625
+ const victim = idle[0];
626
+ victim.child.kill();
627
+ await rm(victim.artifactDir, {
628
+ recursive: true,
629
+ force: true
630
+ }).catch(() => void 0);
631
+ this.#slots.splice(this.#slots.indexOf(victim), 1);
632
+ }
633
+ if (this.#slots.length < this.poolSize) {
634
+ const slot = await this.#spawnSlot(key);
635
+ slot.busy = true;
636
+ this.#slots.push(slot);
637
+ return slot;
638
+ }
639
+ await new Promise((resolve, reject) => this.#waiters.push({
640
+ resolve,
641
+ reject
642
+ }));
643
+ }
644
+ }
645
+ /** 执行后回收:kill 旧子进程,补位重生同 key 新子进程(温池保持),唤醒排队 */
646
+ #recycle(slot) {
647
+ slot.child.kill();
648
+ const index = this.#slots.indexOf(slot);
649
+ if (index >= 0) this.#slots.splice(index, 1);
650
+ rm(slot.artifactDir, {
651
+ recursive: true,
652
+ force: true
653
+ }).catch(() => void 0);
654
+ this.#spawnSlot(slot.key).then(async (fresh) => {
655
+ if (this.#disposed) {
656
+ fresh.child.kill();
657
+ await rm(fresh.artifactDir, {
658
+ recursive: true,
659
+ force: true
660
+ }).catch(() => void 0);
661
+ return;
662
+ }
663
+ this.#slots.push(fresh);
664
+ }).catch((e) => {
665
+ this.#warn(`Failed to respawn sandbox child for pool maintenance: ${messageOf(e)}`);
666
+ }).finally(() => {
667
+ const waiter = this.#waiters.shift();
668
+ if (waiter) waiter.resolve();
669
+ });
670
+ }
671
+ #warn(message) {
672
+ if (this.#options.onWarning) this.#options.onWarning(message);
673
+ else console.warn(message);
674
+ }
675
+ async #spawnSlot(key) {
676
+ const artifactDir = await mkdtemp(path.join(tmpdir(), "webskill-psbx-out-")).then((d) => d.split(path.sep).join("/"));
677
+ const entry = processEntryPath();
678
+ const child = fork(entry, [], {
679
+ execArgv: [
680
+ "--permission",
681
+ ...readAllow(path.dirname(entry)),
682
+ ...readAllow(toPlatform$2(key)),
683
+ ...readAllow(artifactDir),
684
+ ...writeAllow(artifactDir)
685
+ ],
686
+ env: Object.fromEntries((this.#options.envWhitelist ?? []).filter((key) => process.env[key] !== void 0).map((key) => [key, process.env[key]])),
687
+ silent: true
688
+ });
689
+ child.stdout?.resume();
690
+ child.stderr?.resume();
691
+ child.unref();
692
+ child.channel?.unref();
693
+ return {
694
+ key,
695
+ child,
696
+ busy: false,
697
+ artifactDir
698
+ };
699
+ }
700
+ async loadDefinition(skillRoot, scriptName) {
701
+ const scriptPath = await this.#locateScript(skillRoot, scriptName);
702
+ const skillName = baseName(skillRoot);
703
+ const slot = await this.#acquire(toPlatform$2(skillRoot));
704
+ try {
705
+ const response = await this.#runTask(slot, {
706
+ mode: "load",
707
+ scriptPath: toPlatform$2(scriptPath),
708
+ scriptName,
709
+ scriptSource: await this.#fs.readText(scriptPath),
710
+ scratchDir: slot.artifactDir
711
+ }, 3e4);
712
+ if (!response.ok) throw new WebSkillError("TOOL_EXECUTION_FAILED", `Failed to load script "${scriptName}": ${response.error?.message ?? "unknown error"}`);
713
+ return {
714
+ name: `${skillName}__${scriptName}`,
715
+ description: response.definition?.description,
716
+ inputSchema: response.definition?.inputSchema,
717
+ source: "script",
718
+ skillName
719
+ };
720
+ } finally {
721
+ this.#recycle(slot);
722
+ }
723
+ }
724
+ async execute(input) {
725
+ const { skillRoot, scriptName, args, context, timeoutMs } = input;
726
+ let scriptPath;
727
+ try {
728
+ scriptPath = await this.#locateScript(skillRoot, scriptName);
729
+ } catch (e) {
730
+ return {
731
+ ok: false,
732
+ content: [],
733
+ error: {
734
+ code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
735
+ message: messageOf(e)
736
+ }
737
+ };
738
+ }
739
+ const slot = await this.#acquire(toPlatform$2(skillRoot));
740
+ try {
741
+ const result = await this.#runTask(slot, {
742
+ mode: "execute",
743
+ scriptPath: toPlatform$2(scriptPath),
744
+ scriptName,
745
+ skillName: context.skillName,
746
+ runId: context.runId,
747
+ args,
748
+ scriptSource: await this.#fs.readText(scriptPath),
749
+ scratchDir: slot.artifactDir
750
+ }, timeoutMs, (request) => this.#handleBridge(request, context), (host) => context.onWarning?.(`Network request blocked by sandbox network policy: ${host}`));
751
+ if (!result.ok) {
752
+ const stderrSummary = result.stderr?.length ? ` | stderr: ${result.stderr.join(" | ").slice(0, 500)}` : "";
753
+ const normalized = normalizeToolError(result.error?.code, result.error?.message ?? "script execution failed");
754
+ return {
755
+ ok: false,
756
+ content: [],
757
+ error: {
758
+ code: normalized.code,
759
+ message: `${normalized.message}${stderrSummary}`
760
+ }
761
+ };
762
+ }
763
+ const content = normalizeToolContent(result.value);
764
+ if (result.stdout?.length) content.push({
765
+ type: "text",
766
+ text: result.stdout.join("\n")
767
+ });
768
+ return {
769
+ ok: true,
770
+ content
771
+ };
772
+ } catch (e) {
773
+ return {
774
+ ok: false,
775
+ content: [],
776
+ error: {
777
+ code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
778
+ message: messageOf(e)
779
+ }
780
+ };
781
+ } finally {
782
+ this.#recycle(slot);
783
+ }
784
+ }
785
+ /** 在子进程上跑一个任务;超时 kill;非零退出码 → TOOL_EXECUTION_FAILED */
786
+ #runTask(slot, task, timeoutMs, onBridge, onNetworkBlocked) {
787
+ const { child } = slot;
788
+ return new Promise((resolve, reject) => {
789
+ let settled = false;
790
+ const timer = setTimeout(() => {
791
+ if (settled) return;
792
+ settled = true;
793
+ child.kill();
794
+ reject(new WebSkillError("RUN_TIMEOUT", `Process sandbox task timed out after ${timeoutMs}ms (child killed)`));
795
+ }, timeoutMs);
796
+ const done = (fn) => {
797
+ if (settled) return;
798
+ settled = true;
799
+ clearTimeout(timer);
800
+ fn();
801
+ };
802
+ child.on("message", (msg) => {
803
+ if (msg?.type === "network-blocked") {
804
+ if (typeof msg.host === "string") onNetworkBlocked?.(msg.host);
805
+ return;
806
+ }
807
+ if (msg?.type === "bridge" && onBridge) {
808
+ const request = parseBridgeRequest(msg.request);
809
+ const respond = (response) => child.send({
810
+ type: "bridge-response",
811
+ response
812
+ });
813
+ if (!request) {
814
+ respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
815
+ return;
816
+ }
817
+ onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf(e))));
818
+ return;
819
+ }
820
+ if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
821
+ });
822
+ child.on("error", (e) => done(() => reject(e)));
823
+ child.on("exit", (code) => {
824
+ if (!settled) done(() => reject(new WebSkillError("TOOL_EXECUTION_FAILED", `Sandbox child exited ${code === null ? "by signal" : `with code ${code}`} without producing a result`)));
825
+ });
826
+ child.send({
827
+ type: "task",
828
+ task: {
829
+ ...task,
830
+ networkPolicy: this.#networkPolicy,
831
+ networkPolicyLib: NETWORK_POLICY_LIB
832
+ }
833
+ });
834
+ });
835
+ }
836
+ /** 能力桥宿主侧处理:与 worker_threads 沙箱同一判定(runtime/sandbox/approval 单一来源) */
837
+ async #handleBridge(request, context) {
838
+ const gate = async (capability, message, details) => {
839
+ const decision = await this.#approval.authorize({
840
+ runId: context.runId,
841
+ capability,
842
+ mode: this.#capabilities[capability],
843
+ message,
844
+ ...details !== void 0 ? { details } : {}
845
+ });
846
+ if (decision === "allowed") return void 0;
847
+ return bridgeError(request.id, "TOOL_UNSUPPORTED", decision === "disabled" ? `Capability "${capability}" is disabled` : `Capability "${capability}" was denied by the user`);
848
+ };
849
+ try {
850
+ switch (request.kind) {
851
+ case "readReference": {
852
+ const denied = await gate("readReference", `Script "${context.skillName}" wants to read reference "${request.path}"`, { path: request.path });
853
+ if (denied) return denied;
854
+ return {
855
+ id: request.id,
856
+ ok: true,
857
+ value: await context.readReference(request.path)
858
+ };
859
+ }
860
+ case "writeArtifact": {
861
+ const denied = await gate("writeArtifact", `Script "${context.skillName}" wants to write artifact "${request.path}"`, {
862
+ path: request.path,
863
+ mimeType: request.mimeType
864
+ });
865
+ if (denied) return denied;
866
+ const content = typeof request.content === "string" ? request.content : new Uint8Array(request.content);
867
+ const artifact = await context.writeArtifact(request.path, content, { ...request.mimeType ? { mimeType: request.mimeType } : {} });
868
+ return {
869
+ id: request.id,
870
+ ok: true,
871
+ value: artifact
872
+ };
873
+ }
874
+ case "confirm": {
875
+ if (!context.confirm) return bridgeError(request.id, "TOOL_UNSUPPORTED", "Capability \"confirm\" is disabled");
876
+ const denied = await gate("confirm", `Script "${context.skillName}" asks for confirmation: ${request.message}`);
877
+ if (denied) return denied;
878
+ return {
879
+ id: request.id,
880
+ ok: true,
881
+ value: await context.confirm(request.message)
882
+ };
883
+ }
884
+ }
885
+ } catch (e) {
886
+ return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
887
+ }
888
+ }
889
+ };
890
+ const require_ = createRequire(import.meta.url);
891
+ let parseSyncCache;
892
+ function loadParseSync() {
893
+ if (!parseSyncCache) try {
894
+ parseSyncCache = require_("oxc-parser").parseSync;
895
+ } catch (e) {
896
+ throw new WebSkillError("TOOL_UNSUPPORTED", "The \"oxc-parser\" package is required for schema inference; install it first (npm i oxc-parser)", e);
897
+ }
898
+ return parseSyncCache;
899
+ }
900
+ const PLACEHOLDER_PREFIX = "Inferred placeholder (unsupported type: ";
901
+ const placeholder = (text) => ({
902
+ type: "string",
903
+ description: `${PLACEHOLDER_PREFIX}${text})`,
904
+ "x-inferred": true
905
+ });
906
+ /** 主路径映射:基础类型/数组/嵌套对象/可选/字面量 union;其余降级 placeholder */
907
+ var TypeMapper = class {
908
+ #decls = /* @__PURE__ */ new Map();
909
+ #source;
910
+ constructor(body, source) {
911
+ this.#source = source;
912
+ for (const node of body) {
913
+ if (node.type === "TSInterfaceDeclaration" || node.type === "TSTypeAliasDeclaration") this.#decls.set(node.id?.name, node);
914
+ if (node.type === "ExportNamedDeclaration" && (node.declaration?.type === "TSInterfaceDeclaration" || node.declaration?.type === "TSTypeAliasDeclaration")) this.#decls.set(node.declaration.id?.name, node.declaration);
915
+ }
916
+ }
917
+ objectSchema(t, seen) {
918
+ const mapped = this.map(t, seen);
919
+ return mapped?.type === "object" ? mapped : void 0;
920
+ }
921
+ map(t, seen) {
922
+ if (!t) return void 0;
923
+ switch (t.type) {
924
+ case "TSStringKeyword": return { type: "string" };
925
+ case "TSNumberKeyword": return { type: "number" };
926
+ case "TSBooleanKeyword": return { type: "boolean" };
927
+ case "TSAnyKeyword":
928
+ case "TSUnknownKeyword": return {};
929
+ case "TSArrayType": return {
930
+ type: "array",
931
+ items: this.map(t.elementType, seen) ?? {}
932
+ };
933
+ case "TSTypeLiteral": return this.#literalObject(t, seen);
934
+ case "TSLiteralType": return this.#literal(t);
935
+ case "TSUnionType": return this.#union(t, seen);
936
+ case "TSTypeReference": return this.#reference(t, seen);
937
+ case "TSParenthesizedType": return this.map(t.typeAnnotation, seen);
938
+ default: return placeholder(this.#text(t));
939
+ }
940
+ }
941
+ #literal(t) {
942
+ const value = t.literal?.value ?? t.value;
943
+ if (typeof value === "string") return {
944
+ type: "string",
945
+ enum: [value]
946
+ };
947
+ return placeholder(this.#text(t));
948
+ }
949
+ #union(t, _seen) {
950
+ const values = [];
951
+ for (const member of t.types ?? []) {
952
+ const value = member.literal?.value ?? member.value;
953
+ if (member.type === "TSLiteralType" && typeof value === "string") values.push(value);
954
+ else return placeholder(this.#text(t));
955
+ }
956
+ return {
957
+ type: "string",
958
+ enum: values
959
+ };
960
+ }
961
+ #literalObject(t, seen) {
962
+ const properties = {};
963
+ const required = [];
964
+ for (const member of t.members ?? []) {
965
+ if (member.type !== "TSPropertySignature") continue;
966
+ const name = member.key?.name ?? member.key?.value;
967
+ if (typeof name !== "string") continue;
968
+ const annotation = member.typeAnnotation?.typeAnnotation;
969
+ properties[name] = annotation ? this.map(annotation, seen) ?? {} : { type: "string" };
970
+ if (!member.optional) required.push(name);
971
+ }
972
+ return {
973
+ type: "object",
974
+ properties,
975
+ ...required.length ? { required } : {}
976
+ };
977
+ }
978
+ #reference(t, seen) {
979
+ const name = t.typeName?.name ?? t.typeName?.left?.name;
980
+ const params = t.typeArguments?.params ?? t.typeParameters?.params ?? [];
981
+ if (name === "Array" && params.length === 1) return {
982
+ type: "array",
983
+ items: this.map(params[0], seen) ?? {}
984
+ };
985
+ if (name === "string" || name === "number" || name === "boolean") return { type: name };
986
+ if (!name || params.length > 0) return placeholder(this.#text(t));
987
+ if (seen.has(name)) return placeholder(`recursive reference ${name}`);
988
+ const decl = this.#decls.get(name);
989
+ if (!decl) return placeholder(this.#text(t));
990
+ const nextSeen = new Set(seen).add(name);
991
+ if (decl.type === "TSInterfaceDeclaration") return this.#literalObject({ members: decl.body?.body ?? [] }, nextSeen);
992
+ return this.map(decl.typeAnnotation, nextSeen) ?? placeholder(this.#text(t));
993
+ }
994
+ #text(t) {
995
+ if (typeof t.start === "number" && typeof t.end === "number") return this.#source.slice(t.start, t.end);
996
+ return String(t.type);
997
+ }
998
+ };
999
+ const findRunFunction = (body) => {
1000
+ for (const node of body) {
1001
+ if (node.type !== "ExportNamedDeclaration") continue;
1002
+ const decl = node.declaration;
1003
+ if (!decl) continue;
1004
+ if (decl.type === "FunctionDeclaration" && decl.id?.name === "run") return decl;
1005
+ if (decl.type === "VariableDeclaration") {
1006
+ for (const d of decl.declarations ?? []) if (d.id?.name === "run" && (d.init?.type === "ArrowFunctionExpression" || d.init?.type === "FunctionExpression")) return d.init;
1007
+ }
1008
+ }
1009
+ };
1010
+ /** JSDoc 辅路径:@param {type} input.name - desc(点形式)与 @param {{...}} input(对象形式) */
1011
+ function inferFromJsDoc(jsdoc, mapper, reparse) {
1012
+ const properties = {};
1013
+ const required = [];
1014
+ const objectForm = /@param\s+\{\{([\s\S]+?)\}\}\s+input(?:\s+-\s+(.*))?/.exec(jsdoc);
1015
+ if (objectForm) {
1016
+ const alias = reparse(`{${objectForm[1]}}`);
1017
+ if (!alias) return void 0;
1018
+ const schema = mapper.map(alias, /* @__PURE__ */ new Set());
1019
+ return schema?.type === "object" ? schema : void 0;
1020
+ }
1021
+ const dotForm = /@param\s+\{([^}]+)\}\s+input\.(\w+)(?:\s+-\s+([^\n*]+))?/g;
1022
+ let matched = false;
1023
+ for (const m of jsdoc.matchAll(dotForm)) {
1024
+ const [, typeText, name, desc] = m;
1025
+ if (!typeText || !name) continue;
1026
+ matched = true;
1027
+ const alias = reparse(typeText.trim());
1028
+ const schema = alias ? mapper.map(alias, /* @__PURE__ */ new Set()) ?? {} : { type: "string" };
1029
+ if (desc?.trim()) schema.description = desc.trim();
1030
+ properties[name] = schema;
1031
+ required.push(name);
1032
+ }
1033
+ if (!matched) return void 0;
1034
+ return {
1035
+ type: "object",
1036
+ properties,
1037
+ required
1038
+ };
1039
+ }
1040
+ /**
1041
+ * D2 Schema 推导(OXC 静态文本分析,宿主侧执行,不进沙箱)。
1042
+ * TS 类型标注为主路径,JSDoc @param 为辅路径;不支持类型降级 string 并标 'x-inferred'。
1043
+ */
1044
+ var OxcSchemaInferer = class {
1045
+ inferSchemaFromSource(source, options) {
1046
+ let program;
1047
+ let comments;
1048
+ try {
1049
+ const result = loadParseSync()(options?.fileName ?? "script.ts", source);
1050
+ if (result.errors?.length > 0) return void 0;
1051
+ program = result.program;
1052
+ comments = result.comments ?? [];
1053
+ } catch {
1054
+ return;
1055
+ }
1056
+ const runFn = findRunFunction(program.body ?? []);
1057
+ if (!runFn) return void 0;
1058
+ const annotation = (runFn.params?.[0] ?? runFn.params?.items?.[0])?.typeAnnotation?.typeAnnotation;
1059
+ const mapper = new TypeMapper(program.body ?? [], source);
1060
+ if (annotation) return mapper.objectSchema(annotation, /* @__PURE__ */ new Set());
1061
+ const runStart = runFn.start ?? Number.MAX_SAFE_INTEGER;
1062
+ const jsdoc = comments.filter((c) => c.type === "Block" && typeof c.value === "string" && c.value.includes("@param") && typeof c.end === "number" && c.end <= runStart).sort((a, b) => b.end - a.end)[0];
1063
+ if (!jsdoc) return void 0;
1064
+ const reparse = (typeText) => {
1065
+ const r = loadParseSync()("__t.ts", `type __T = ${typeText};`);
1066
+ if (r.errors?.length > 0) return void 0;
1067
+ return r.program.body.find((n) => n.type === "TSTypeAliasDeclaration")?.typeAnnotation;
1068
+ };
1069
+ return inferFromJsDoc(jsdoc.value, mapper, reparse);
1070
+ }
1071
+ };
1072
+ /**
1073
+ * FileArtifactStore:兼容别名,语义同阶段 2-4。
1074
+ * 实现已上移到 runtime 的 FsArtifactStore;node 侧保留 NodeFS 默认值。
1075
+ */
1076
+ var FileArtifactStore = class extends FsArtifactStore {
1077
+ constructor(deps) {
1078
+ super({
1079
+ root: deps.root,
1080
+ fs: deps.fs ?? new NodeFS()
1081
+ });
1082
+ }
1083
+ };
1084
+ /**
1085
+ * FileMemoryStore:兼容别名,语义同阶段 3。
1086
+ * 实现已上移到 runtime 的 FsMemoryStore;node 侧保留 NodeFS 默认值。
1087
+ */
1088
+ var FileMemoryStore = class extends FsMemoryStore {
1089
+ constructor(deps) {
1090
+ super({
1091
+ root: deps.root,
1092
+ fs: deps.fs ?? new NodeFS()
1093
+ });
1094
+ }
1095
+ };
1096
+ /**
1097
+ * 命令行 UiBridge:ask→问答,confirm→y/n(带默认值),
1098
+ * form→逐字段提示(显示默认值与必填标记),select→编号列表,
1099
+ * authorize→授权询问(默认拒绝,仅显式 y/yes 批准)。
1100
+ * 输入/输出流可注入(测试用 PassThrough)。
1101
+ */
1102
+ var CliUiBridge = class {
1103
+ #input;
1104
+ #output;
1105
+ constructor(deps = {}) {
1106
+ this.#input = deps.input ?? process.stdin;
1107
+ this.#output = deps.output ?? process.stdout;
1108
+ }
1109
+ async request(input) {
1110
+ switch (input.type) {
1111
+ case "ask": return {
1112
+ id: input.id,
1113
+ value: await this.#question(`${input.message}\n> `)
1114
+ };
1115
+ case "confirm": {
1116
+ const hint = input.defaultValue === false ? "y/N" : "Y/n";
1117
+ const answer = (await this.#question(`${input.message} [${hint}] `)).trim().toLowerCase();
1118
+ const value = answer === "" ? input.defaultValue ?? true : answer === "y" || answer === "yes";
1119
+ return {
1120
+ id: input.id,
1121
+ value
1122
+ };
1123
+ }
1124
+ case "form": {
1125
+ const value = {};
1126
+ if (input.title) this.#write(`${input.title}\n`);
1127
+ for (const field of input.fields) value[field.name] = await this.#askField(field);
1128
+ return {
1129
+ id: input.id,
1130
+ value
1131
+ };
1132
+ }
1133
+ case "select": {
1134
+ const lines = input.options.map((o, i) => ` ${i + 1}) ${o.label}`).join("\n");
1135
+ const answer = (await this.#question(`${input.message}\n${lines}\n> `)).trim();
1136
+ const index = Number.parseInt(answer, 10) - 1;
1137
+ const option = input.options[index];
1138
+ return {
1139
+ id: input.id,
1140
+ value: option ? option.value : void 0
1141
+ };
1142
+ }
1143
+ case "authorize": {
1144
+ this.#write(`[authorization required: ${input.capability}]\n`);
1145
+ const answer = (await this.#question(`${input.message} [y/N] `)).trim().toLowerCase();
1146
+ return {
1147
+ id: input.id,
1148
+ value: answer === "y" || answer === "yes"
1149
+ };
1150
+ }
1151
+ }
1152
+ }
1153
+ async progress(input) {
1154
+ const pct = input.value !== void 0 ? ` (${Math.round(input.value * 100)}%)` : "";
1155
+ this.#write(`[progress] ${input.message}${pct}\n`);
1156
+ }
1157
+ async renderResult(input) {
1158
+ const lines = [`\n[result] ${input.summary ?? ""}`];
1159
+ for (const block of input.blocks) if (block.type === "markdown") lines.push(block.text);
1160
+ else if (block.type === "json") lines.push(JSON.stringify(block.data, null, 2));
1161
+ else if (block.type === "file") lines.push(`[file] ${block.path}`);
1162
+ this.#write(`${lines.join("\n")}\n`);
1163
+ }
1164
+ #write(text) {
1165
+ this.#output.write(text);
1166
+ }
1167
+ async #askField(field) {
1168
+ const requiredMark = field.required ? " (required)" : "";
1169
+ const defaultMark = field.defaultValue !== void 0 ? ` [${JSON.stringify(field.defaultValue)}]` : "";
1170
+ if (field.type === "select" && field.options) {
1171
+ const lines = field.options.map((o, i) => ` ${i + 1}) ${o.label}`).join("\n");
1172
+ const answer = (await this.#question(`${field.label}${requiredMark}${defaultMark}:\n${lines}\n> `)).trim();
1173
+ if (answer === "") return field.defaultValue;
1174
+ const option = field.options[Number.parseInt(answer, 10) - 1];
1175
+ return option ? option.value : field.defaultValue;
1176
+ }
1177
+ const answer = (await this.#question(`${field.label}${requiredMark}${defaultMark}: `)).trim();
1178
+ if (answer === "") return field.defaultValue;
1179
+ switch (field.type) {
1180
+ case "number": {
1181
+ const n = Number(answer);
1182
+ return Number.isNaN(n) ? field.defaultValue : n;
1183
+ }
1184
+ case "boolean": return answer === "y" || answer === "yes" || answer === "true";
1185
+ case "textarea": try {
1186
+ return JSON.parse(answer);
1187
+ } catch {
1188
+ return answer;
1189
+ }
1190
+ default: return answer;
1191
+ }
1192
+ }
1193
+ #rl;
1194
+ #lines = [];
1195
+ #waiters = [];
1196
+ /** 常驻 line 监听进入内部队列:预写入的多行输入不会在两次提问之间丢失 */
1197
+ #ensureInterface() {
1198
+ if (this.#rl) return;
1199
+ this.#rl = createInterface({
1200
+ input: this.#input,
1201
+ output: this.#output,
1202
+ terminal: false
1203
+ });
1204
+ this.#rl.on("line", (line) => {
1205
+ const waiter = this.#waiters.shift();
1206
+ if (waiter) waiter(line);
1207
+ else this.#lines.push(line);
1208
+ });
1209
+ }
1210
+ async #question(prompt) {
1211
+ this.#ensureInterface();
1212
+ this.#output.write(prompt);
1213
+ const queued = this.#lines.shift();
1214
+ if (queued !== void 0) return queued;
1215
+ return new Promise((resolve) => this.#waiters.push(resolve));
1216
+ }
1217
+ };
1218
+ const toPlatform$1 = (p) => p.split("/").join(path.sep);
1219
+ const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
1220
+ let tarModule$1;
1221
+ async function loadTar$1() {
1222
+ tarModule$1 ??= await import("tar").catch((e) => {
1223
+ throw new WebSkillError("TOOL_UNSUPPORTED", "The \"tar\" package is required for tar/tar.gz archives; install it first (npm i tar)", e);
1224
+ });
1225
+ return tarModule$1;
1226
+ }
1227
+ /**
1228
+ * zip 解包(fflate 流式,单条目/总解压体积上限)。每个条目路径过 resolveInsideRoot:
1229
+ * 拒绝 `..` 段、绝对路径、反斜杠穿越(zip-slip 防护)。
1230
+ */
1231
+ async function extractZipData(fs, data, destRoot, limits) {
1232
+ for (const [entryPath, content] of await unzipWithLimits(data, limits)) {
1233
+ const target = resolveInsideRoot(destRoot, entryPath);
1234
+ if (entryPath.endsWith("/")) await fs.mkdir(target);
1235
+ else await fs.writeBinary(target, content);
1236
+ }
1237
+ }
1238
+ /**
1239
+ * tar/tar.gz 解包(tar 包)。先列条目逐一过 resolveInsideRoot 校验并累计
1240
+ * 单条目/总体积上限(声明 size 预检),再解包;符号链接/硬链接条目不跟随(跳过)。
1241
+ */
1242
+ async function extractTarFile(archivePath, destRoot, limits) {
1243
+ const { maxEntryBytes, maxTotalBytes } = resolveArchiveLimits(limits);
1244
+ try {
1245
+ const entryPaths = [];
1246
+ let total = 0;
1247
+ const tarMod = await loadTar$1();
1248
+ await tarMod.t({
1249
+ file: toPlatform$1(archivePath),
1250
+ onentry: (entry) => {
1251
+ entryPaths.push(entry.path);
1252
+ const size = typeof entry.size === "number" ? entry.size : 0;
1253
+ if (size > maxEntryBytes) throw new WebSkillError("INSTALL_FAILED", `Archive entry exceeds the ${maxEntryBytes}-byte limit: ${entry.path}`);
1254
+ total += size;
1255
+ if (total > maxTotalBytes) throw new WebSkillError("INSTALL_FAILED", `Archive contents exceed the ${maxTotalBytes}-byte total limit`);
1256
+ }
1257
+ });
1258
+ for (const p of entryPaths) if (!p.replace(/\\/g, "/").split("/").every((s) => s === "" || s === ".")) resolveInsideRoot(destRoot, p);
1259
+ await tarMod.x({
1260
+ file: toPlatform$1(archivePath),
1261
+ cwd: toPlatform$1(destRoot),
1262
+ filter: (_p, entry) => !("type" in entry) || entry.type !== "SymbolicLink" && entry.type !== "Link"
1263
+ });
1264
+ } catch (e) {
1265
+ if (e instanceof WebSkillError) throw e;
1266
+ throw new WebSkillError("INSTALL_FAILED", `Failed to extract tar archive: ${messageOf(e)}`, e);
1267
+ }
1268
+ }
1269
+ /** 递归列出目录内全部文件(相对路径,posix 分隔,不含目录条目) */
1270
+ async function listFiles(fs, root, prefix = "") {
1271
+ const out = [];
1272
+ for (const entry of await fs.list(root)) {
1273
+ const name = entry.path.split("/").pop() ?? "";
1274
+ const rel = prefix === "" ? name : `${prefix}/${name}`;
1275
+ if (entry.type === "directory") out.push(...await listFiles(fs, entry.path, rel));
1276
+ else out.push(rel);
1277
+ }
1278
+ return out;
1279
+ }
1280
+ const toPlatform = (p) => p.split("/").join(path.sep);
1281
+ /** lstat 扫描拒绝符号链接条目(与 tar 解包 filter 策略一致;虚拟 fs 路径不存在于真实 fs 时跳过) */
1282
+ async function assertNoSymlinks(platformRoot) {
1283
+ let entries;
1284
+ try {
1285
+ entries = await promises.readdir(platformRoot, { withFileTypes: true });
1286
+ } catch (error) {
1287
+ const code = error.code;
1288
+ if (code === "ENOENT") return;
1289
+ throw new WebSkillError("INSTALL_FAILED", `Cannot verify symbolic links under ${platformRoot} (${code ?? "unknown error"}); refusing to copy`, { cause: error });
1290
+ }
1291
+ for (const entry of entries) {
1292
+ if (entry.isSymbolicLink()) throw new WebSkillError("INSTALL_FAILED", `Refusing to copy symbolic link entry: ${platformRoot}${path.sep}${entry.name}`);
1293
+ if (entry.isDirectory()) await assertNoSymlinks(`${platformRoot}${path.sep}${entry.name}`);
1294
+ }
1295
+ }
1296
+ /** 递归拷贝目录(含子目录与二进制文件);真实 fs 上先做符号链接扫描 */
1297
+ async function copyDir(fs, from, to) {
1298
+ await assertNoSymlinks(toPlatform(from));
1299
+ await fs.mkdir(to);
1300
+ for (const entry of await fs.list(from)) {
1301
+ const name = entry.path.split("/").pop() ?? "";
1302
+ if (entry.type === "directory") await copyDir(fs, entry.path, `${to}/${name}`);
1303
+ else await fs.writeBinary(`${to}/${name}`, await fs.readBinary(entry.path));
1304
+ }
1305
+ }
1306
+ /** 删除目录(不存在时静默) */
1307
+ async function removeDirQuiet(fs, dir) {
1308
+ try {
1309
+ if (await fs.exists(dir)) await fs.remove(dir, { recursive: true });
1310
+ } catch {}
1311
+ }
1312
+ let tarModule;
1313
+ async function loadTar() {
1314
+ tarModule ??= await import("tar").catch((e) => {
1315
+ throw new WebSkillError("TOOL_UNSUPPORTED", "The \"tar\" package is required for tar/tar.gz archives; install it first (npm i tar)", e);
1316
+ });
1317
+ return tarModule;
1318
+ }
1319
+ /** 导出技能目录全部文件(含 manifest)为 zip/tar 归档,返回 outPath */
1320
+ async function exportArchive(fs, skillRoot, options) {
1321
+ try {
1322
+ if (options.format === "zip") {
1323
+ const files = {};
1324
+ for (const rel of await listFiles(fs, skillRoot)) files[rel] = await fs.readBinary(`${skillRoot}/${rel}`);
1325
+ await fs.writeBinary(options.outPath, zipSync(files, { level: 6 }));
1326
+ } else await (await loadTar()).c({
1327
+ file: toPlatform$1(options.outPath),
1328
+ cwd: toPlatform$1(skillRoot)
1329
+ }, ["."]);
1330
+ return options.outPath;
1331
+ } catch (e) {
1332
+ if (e instanceof WebSkillError) throw e;
1333
+ throw new WebSkillError("EXPORT_FAILED", `Failed to export ${options.format} archive: ${messageOf(e)}`, e);
1334
+ }
1335
+ }
1336
+ /** 只解出归档中的 webskill.skill-manifest.json 条目(安装前预览) */
1337
+ async function readArchiveManifest(fs, archivePath) {
1338
+ const notFound = () => new WebSkillError("EXPORT_FAILED", `Archive ${archivePath} does not contain ${SKILL_MANIFEST_FILE}`);
1339
+ const data = await fs.readBinary(archivePath);
1340
+ if (isZipArchive(data)) {
1341
+ const entries = unzipSync(data);
1342
+ const key = Object.keys(entries).find((k) => k === "webskill.skill-manifest.json" || k.endsWith(`/${"webskill.skill-manifest.json"}`));
1343
+ if (!key) throw notFound();
1344
+ return JSON.parse(new TextDecoder().decode(entries[key]));
1345
+ }
1346
+ let manifestText;
1347
+ try {
1348
+ await (await loadTar()).t({
1349
+ file: toPlatform$1(archivePath),
1350
+ onentry: (entry) => {
1351
+ if (entry.path === "webskill.skill-manifest.json" || entry.path.endsWith(`/${"webskill.skill-manifest.json"}`)) {
1352
+ const chunks = [];
1353
+ entry.on("data", (chunk) => chunks.push(chunk));
1354
+ entry.on("end", () => {
1355
+ manifestText = Buffer.concat(chunks).toString("utf8");
1356
+ });
1357
+ }
1358
+ }
1359
+ });
1360
+ } catch (e) {
1361
+ throw new WebSkillError("EXPORT_FAILED", `Failed to read tar archive: ${messageOf(e)}`, e);
1362
+ }
1363
+ if (manifestText === void 0) throw notFound();
1364
+ return JSON.parse(manifestText);
1365
+ }
1366
+ const _execFileP = promisify(execFile);
1367
+ /**
1368
+ * 跨平台执行命令:Windows 下对 npm 等 cmd 包装的命令通过 cmd.exe 代理执行,
1369
+ * git 等原生 exe 不受影响。默认 120s 超时(防挂死安装管线)。
1370
+ */
1371
+ async function execFileP(command, args, options = {}) {
1372
+ const timeoutMs = options.timeoutMs ?? 12e4;
1373
+ if (process.platform === "win32" && command === "npm") return _execFileP("cmd", [
1374
+ "/c",
1375
+ command,
1376
+ ...args
1377
+ ], { timeout: timeoutMs });
1378
+ return _execFileP(command, args, { timeout: timeoutMs });
1379
+ }
1380
+ /** 命令缺失/失败 → INSTALL_FAILED 结构化诊断 */
1381
+ function commandFailed(command, e) {
1382
+ return new WebSkillError("INSTALL_FAILED", `Failed to run ${command}: ${e?.code === "ENOENT" ? `command "${command}" not found on this system` : messageOf(e)}`, e);
1383
+ }
1384
+ /** git url 协议白名单:https:// 与 git@ SCP 形式;无 scheme 的本地路径放行(dev 工作流);
1385
+ * 其余显式协议(ext::/file:// 等)一律拒绝 */
1386
+ function assertGitUrlSafe(url) {
1387
+ if (url.startsWith("--")) throw new WebSkillError("INSTALL_FAILED", `Git url must not start with "--": ${JSON.stringify(url)}`);
1388
+ if (url.startsWith("https://") || url.startsWith("git@")) return;
1389
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(url)) throw new WebSkillError("INSTALL_FAILED", `Git url protocol is not allowed (only https://, git@ SCP form, or plain local paths): ${JSON.stringify(url)}`);
1390
+ }
1391
+ /** git 源:clone --depth 1 + rev-parse HEAD 记录 commit;url/ref 前插 `--` 防选项注入 */
1392
+ async function stageGit(source, ctx) {
1393
+ assertGitUrlSafe(source.url);
1394
+ if (source.ref?.startsWith("--")) throw new WebSkillError("INSTALL_FAILED", `Git ref must not start with "--": ${JSON.stringify(source.ref)}`);
1395
+ const skillDir = `${ctx.stagingRoot}/repo`;
1396
+ const args = [
1397
+ "clone",
1398
+ "--depth",
1399
+ "1",
1400
+ ...source.ref ? ["--branch", source.ref] : [],
1401
+ "--",
1402
+ source.url,
1403
+ skillDir
1404
+ ];
1405
+ try {
1406
+ await execFileP("git", args);
1407
+ } catch (e) {
1408
+ throw commandFailed("git", e);
1409
+ }
1410
+ let commit;
1411
+ try {
1412
+ const { stdout } = await execFileP("git", [
1413
+ "-C",
1414
+ skillDir,
1415
+ "rev-parse",
1416
+ "HEAD"
1417
+ ]);
1418
+ commit = stdout.trim() || void 0;
1419
+ } catch (e) {
1420
+ console.warn(`[webskill] git rev-parse failed for ${source.url}; install provenance will omit the commit: ${e instanceof Error ? e.message : String(e)}`);
1421
+ }
1422
+ return {
1423
+ skillDir,
1424
+ source: {
1425
+ ...source,
1426
+ ...commit ? { commit } : {}
1427
+ }
1428
+ };
1429
+ }
1430
+ function sha256Hex(data) {
1431
+ return createHash("sha256").update(data).digest("hex");
1432
+ }
1433
+ /** 重定向跳数上限(SSRF 防护:逐跳重新校验目标 URL) */
1434
+ const MAX_REDIRECT_HOPS = 3;
1435
+ /**
1436
+ * 带 SSRF 防护的下载:手动跟随重定向(默认 ≤3 跳),初始 URL 与每一跳目标
1437
+ * 都过 assertRemoteUrlAllowed(https 默认;私有/环回/链路本地默认拒绝)。
1438
+ */
1439
+ async function fetchWithSsrfGuard(source, fetchImpl) {
1440
+ const policy = {
1441
+ allowHttp: source.allowHttp ?? false,
1442
+ allowPrivateHosts: source.allowPrivateHosts ?? false
1443
+ };
1444
+ let url = assertRemoteUrlAllowed(source.url, policy);
1445
+ for (let hop = 0;; hop++) {
1446
+ const res = await fetchImpl(url.href, { redirect: "manual" });
1447
+ const location = res.headers.get("location");
1448
+ if (res.status >= 300 && res.status < 400 && location) {
1449
+ if (hop >= MAX_REDIRECT_HOPS) throw new WebSkillError("INSTALL_FAILED", `Download exceeded the redirect limit of ${MAX_REDIRECT_HOPS} hops`);
1450
+ url = assertRemoteUrlAllowed(new URL(location, url).href, policy);
1451
+ continue;
1452
+ }
1453
+ return res;
1454
+ }
1455
+ }
1456
+ /** http 源:下载归档(SSRF 防护 + Content-Length/流式上限;可选 expectedSha256 校验包体)→ 解包 → 定位技能根/包集 */
1457
+ async function stageHttp(source, ctx, expectedSha256) {
1458
+ const fetchImpl = ctx.fetchImpl ?? fetch;
1459
+ let res;
1460
+ try {
1461
+ res = await fetchWithSsrfGuard(source, fetchImpl);
1462
+ } catch (e) {
1463
+ if (e instanceof WebSkillError) throw e;
1464
+ throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf(e)}`, e);
1465
+ }
1466
+ if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
1467
+ const data = await readResponseWithLimit(res, ctx.archiveLimits);
1468
+ if (expectedSha256 !== void 0) {
1469
+ const actual = sha256Hex(data);
1470
+ if (actual !== expectedSha256) throw new WebSkillError("INSTALL_FAILED", `Archive checksum mismatch: expected ${expectedSha256}, got ${actual}`);
1471
+ }
1472
+ const contentDir = `${ctx.stagingRoot}/content`;
1473
+ await ctx.fs.mkdir(contentDir);
1474
+ if (isZipArchive(data)) await extractZipData(ctx.fs, data, contentDir, ctx.archiveLimits);
1475
+ else {
1476
+ const archivePath = `${ctx.stagingRoot}/archive.tar`;
1477
+ await ctx.fs.writeBinary(archivePath, data);
1478
+ await extractTarFile(toPlatform$1(archivePath), toPlatform$1(contentDir), ctx.archiveLimits);
1479
+ }
1480
+ const packFilePath = `${contentDir}/${SKILL_PACK_FILE}`;
1481
+ if (await ctx.fs.exists(packFilePath)) return {
1482
+ skillDir: "",
1483
+ source,
1484
+ pack: {
1485
+ contentDir,
1486
+ manifest: parseSkillPackManifest(await ctx.fs.readText(packFilePath))
1487
+ }
1488
+ };
1489
+ if (await ctx.fs.exists(`${contentDir}/SKILL.md`)) return {
1490
+ skillDir: contentDir,
1491
+ source
1492
+ };
1493
+ const dirs = (await ctx.fs.list(contentDir)).filter((e) => e.type === "directory");
1494
+ if (dirs.length === 1 && await ctx.fs.exists(`${dirs[0]?.path}/SKILL.md`)) return {
1495
+ skillDir: dirs[0].path,
1496
+ source
1497
+ };
1498
+ throw new WebSkillError("INSTALL_FAILED", "Archive does not contain a SKILL.md at its root or in a single top-level directory");
1499
+ }
1500
+ /** local 源:校验源目录存在,拷贝到 staging */
1501
+ async function stageLocal(source, ctx) {
1502
+ if (!await ctx.fs.exists(source.path)) throw new WebSkillError("INSTALL_FAILED", `Local skill directory not found: ${source.path}`);
1503
+ const skillDir = `${ctx.stagingRoot}/skill`;
1504
+ await copyDir(ctx.fs, source.path, skillDir);
1505
+ return {
1506
+ skillDir,
1507
+ source
1508
+ };
1509
+ }
1510
+ /** npm 包名(@scope/name 或 name,小写字母数字 . _ - /)与版本(semver 或 dist-tag)正则 */
1511
+ const PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
1512
+ const VERSION_RE = /^[a-z0-9._~^>=<x*+-]+$/i;
1513
+ /**
1514
+ * 本地路径字符白名单(Windows 下 npm 经 cmd /c 代理二次解析,
1515
+ * `& | < > ^ " ' % ! ;` 与空格一律拒绝;允许 Windows 盘符 `C:\` 与正/反斜杠)
1516
+ */
1517
+ const LOCAL_PATH_RE = /^[A-Za-z0-9._~+@/\\:-]+$/;
1518
+ function assertNpmSpecSafe(source) {
1519
+ const name = source.packageName;
1520
+ const isLocalPath = name.startsWith("/") || name.startsWith("./") || name.startsWith("../");
1521
+ if (name.startsWith("--") || !PACKAGE_NAME_RE.test(name) && !isLocalPath) throw new WebSkillError("INSTALL_FAILED", `Invalid npm package name: ${JSON.stringify(name)}`);
1522
+ if (isLocalPath && !LOCAL_PATH_RE.test(name)) throw new WebSkillError("INSTALL_FAILED", `Local npm path contains characters outside the safe whitelist: ${JSON.stringify(name)}`);
1523
+ if (source.version !== void 0 && (!VERSION_RE.test(source.version) || source.version.startsWith("--"))) throw new WebSkillError("INSTALL_FAILED", `Invalid npm package version: ${JSON.stringify(source.version)}`);
1524
+ }
1525
+ /** npm 源:npm pack --ignore-scripts → 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根;spec 前插 `--` 防选项注入 */
1526
+ async function stageNpm(source, ctx) {
1527
+ assertNpmSpecSafe(source);
1528
+ const spec = source.version ? `${source.packageName}@${source.version}` : source.packageName;
1529
+ let stdout;
1530
+ try {
1531
+ ({stdout} = await execFileP("npm", [
1532
+ "pack",
1533
+ "--ignore-scripts",
1534
+ "--pack-destination",
1535
+ ctx.stagingRoot,
1536
+ "--",
1537
+ spec
1538
+ ]));
1539
+ } catch (e) {
1540
+ throw commandFailed("npm", e);
1541
+ }
1542
+ const tarball = stdout.trim().split("\n").filter(Boolean).pop();
1543
+ const tgzPath = tarball ? `${ctx.stagingRoot}/${tarball}` : "";
1544
+ if (!tarball || !await ctx.fs.exists(tgzPath)) throw new WebSkillError("INSTALL_FAILED", `npm pack output not found in staging directory: ${tarball ?? "(empty output)"}`);
1545
+ const pkgDir = `${ctx.stagingRoot}/pkg`;
1546
+ await ctx.fs.mkdir(pkgDir);
1547
+ await extractTarFile(toPlatform$1(tgzPath), toPlatform$1(pkgDir));
1548
+ const pkgRoot = `${pkgDir}/package`;
1549
+ const skillDir = await ctx.fs.exists(`${pkgRoot}/skill/SKILL.md`) ? `${pkgRoot}/skill` : pkgRoot;
1550
+ let packageName = source.packageName;
1551
+ let version = source.version;
1552
+ try {
1553
+ const pkg = JSON.parse(await ctx.fs.readText(`${pkgRoot}/package.json`));
1554
+ packageName = pkg.name ?? packageName;
1555
+ version = pkg.version ?? version;
1556
+ } catch (e) {
1557
+ console.warn(`[webskill] Cannot read package.json of npm skill "${source.packageName}"; recorded version may not match the installed artifact: ${e instanceof Error ? e.message : String(e)}`);
1558
+ }
1559
+ return {
1560
+ skillDir,
1561
+ source: {
1562
+ type: "npm",
1563
+ packageName,
1564
+ ...version ? { version } : {}
1565
+ }
1566
+ };
1567
+ }
1568
+ const emptyLockfile = () => ({
1569
+ schemaVersion: 1,
1570
+ skills: {}
1571
+ });
1572
+ /** 读取 lockfile;不存在返回空结构;损坏 JSON → INTEGRITY_FAILED */
1573
+ async function readLockfile(fs, managedRoot) {
1574
+ const path = `${managedRoot}/${SKILLS_LOCKFILE}`;
1575
+ if (!await fs.exists(path)) return emptyLockfile();
1576
+ try {
1577
+ const parsed = JSON.parse(await fs.readText(path));
1578
+ if (typeof parsed !== "object" || parsed === null || typeof parsed.skills !== "object") throw new Error("unexpected lockfile shape");
1579
+ return parsed;
1580
+ } catch (e) {
1581
+ throw new WebSkillError("INTEGRITY_FAILED", `Skills lockfile at ${path} is corrupted: ${e instanceof Error ? e.message : String(e)}`, e);
1582
+ }
1583
+ }
1584
+ async function upsertLockEntry(fs, managedRoot, name, entry) {
1585
+ const lockfile = await readLockfile(fs, managedRoot);
1586
+ lockfile.skills[name] = entry;
1587
+ await writeLockfileAtomic(fs, managedRoot, lockfile);
1588
+ return lockfile;
1589
+ }
1590
+ async function removeLockEntry(fs, managedRoot, name) {
1591
+ const lockfile = await readLockfile(fs, managedRoot);
1592
+ delete lockfile.skills[name];
1593
+ await writeLockfileAtomic(fs, managedRoot, lockfile);
1594
+ return lockfile;
1595
+ }
1596
+ /** 原子写:先写临时文件再 rename(进程崩溃不留下半写 lockfile) */
1597
+ async function writeLockfileAtomic(fs, managedRoot, lockfile) {
1598
+ await atomicWriteText(fs, `${managedRoot}/${SKILLS_LOCKFILE}`, JSON.stringify(lockfile, null, 2));
1599
+ }
1600
+ const EXCLUDED_FILES = /* @__PURE__ */ new Set([SKILL_MANIFEST_FILE, SKILLS_LOCKFILE]);
1601
+ /** 递归收集技能目录内的文件(相对路径,posix 分隔) */
1602
+ async function walkFiles(fs, root, prefix = "") {
1603
+ const out = [];
1604
+ for (const entry of await fs.list(root)) {
1605
+ const name = entry.path.split("/").pop() ?? "";
1606
+ const rel = prefix === "" ? name : `${prefix}/${name}`;
1607
+ if (entry.type === "directory") out.push(...await walkFiles(fs, entry.path, rel));
1608
+ else if (!EXCLUDED_FILES.has(name)) out.push(rel);
1609
+ }
1610
+ return out;
1611
+ }
1612
+ /** 生成并写入 webskill.skill-manifest.json */
1613
+ async function createManifest(fs, skillRoot, input) {
1614
+ const files = [];
1615
+ for (const rel of (await walkFiles(fs, skillRoot)).sort()) {
1616
+ const content = await fs.readBinary(`${skillRoot}/${rel}`);
1617
+ files.push({
1618
+ path: rel,
1619
+ size: content.length,
1620
+ sha256: sha256Hex(content)
1621
+ });
1622
+ }
1623
+ const manifest = await buildManifest({
1624
+ name: input.name,
1625
+ ...input.version !== void 0 ? { version: input.version } : {},
1626
+ source: input.source,
1627
+ installedAt: input.installedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1628
+ files,
1629
+ sha256: sha256Hex
1630
+ });
1631
+ await fs.writeText(`${skillRoot}/${SKILL_MANIFEST_FILE}`, JSON.stringify(manifest, null, 2));
1632
+ return manifest;
1633
+ }
1634
+ /** 读取技能目录内的 manifest;缺失/损坏 → INTEGRITY_FAILED */
1635
+ async function readManifest(fs, skillRoot) {
1636
+ const path = `${skillRoot}/${SKILL_MANIFEST_FILE}`;
1637
+ if (!await fs.exists(path)) throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest not found at ${path} (was the skill installed via SkillManager?)`);
1638
+ try {
1639
+ return JSON.parse(await fs.readText(path));
1640
+ } catch (e) {
1641
+ throw new WebSkillError("INTEGRITY_FAILED", `Skill manifest at ${path} is corrupted: ${e instanceof Error ? e.message : String(e)}`, e);
1642
+ }
1643
+ }
1644
+ /** walk 实际目录与 manifest 比对:hash 不一致 / 清单外新增 / 清单内缺失三类全空才 ok */
1645
+ async function verifyIntegrity(fs, skillRoot) {
1646
+ const manifest = await readManifest(fs, skillRoot);
1647
+ const actualHashes = /* @__PURE__ */ new Map();
1648
+ const actualFiles = await walkFiles(fs, skillRoot);
1649
+ for (const rel of actualFiles) actualHashes.set(rel, sha256Hex(await fs.readBinary(`${skillRoot}/${rel}`)));
1650
+ return verifyManifest(manifest, actualHashes, actualFiles);
1651
+ }
1652
+ const asInstallFailed = (e) => e instanceof WebSkillError && e.code === "INSTALL_FAILED" ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
1653
+ /**
1654
+ * 技能管理门面:统一安装管线(staging → 解析 name → 校验 → 拷贝 → manifest → lockfile),
1655
+ * 任何失败清理现场抛 INSTALL_FAILED。
1656
+ * @stable
1657
+ */
1658
+ var SkillManager = class {
1659
+ #managedRoot;
1660
+ #fs;
1661
+ #fetchImpl;
1662
+ #schemaInference;
1663
+ #archiveLimits;
1664
+ #onChanged;
1665
+ #schemaInferer = new OxcSchemaInferer();
1666
+ constructor(deps) {
1667
+ this.#managedRoot = deps.managedRoot.replace(/\/+$/, "");
1668
+ this.#fs = deps.fs ?? new NodeFS();
1669
+ this.#fetchImpl = deps.fetchImpl;
1670
+ this.#schemaInference = deps.schemaInference ?? true;
1671
+ this.#archiveLimits = deps.archiveLimits;
1672
+ this.#onChanged = deps.onChanged;
1673
+ }
1674
+ /** 托管根目录(治理发布归档捕获等只读场景) */
1675
+ get managedRoot() {
1676
+ return this.#managedRoot;
1677
+ }
1678
+ async install(source, options) {
1679
+ const fs = this.#fs;
1680
+ const stagingRoot = (await mkdtemp(path.join(tmpdir(), "webskill-install-"))).split(path.sep).join("/");
1681
+ /** 已写入目标目录时用于回滚 */
1682
+ let targetDir;
1683
+ try {
1684
+ const ctx = {
1685
+ fs,
1686
+ stagingRoot,
1687
+ ...this.#fetchImpl ? { fetchImpl: this.#fetchImpl } : {},
1688
+ ...this.#archiveLimits ? { archiveLimits: this.#archiveLimits } : {}
1689
+ };
1690
+ let staged;
1691
+ switch (source.type) {
1692
+ case "local":
1693
+ staged = await stageLocal(source, ctx);
1694
+ break;
1695
+ case "http":
1696
+ staged = await stageHttp(source, ctx, options?.expectedSha256);
1697
+ break;
1698
+ case "git":
1699
+ staged = await stageGit(source, ctx);
1700
+ break;
1701
+ case "npm":
1702
+ staged = await stageNpm(source, ctx);
1703
+ break;
1704
+ case "archive": throw new WebSkillError("TOOL_UNSUPPORTED", "The \"archive\" install source is only supported by the browser skill manager");
1705
+ }
1706
+ if (staged.pack) return (await this.#installPack(staged.pack, staged.source, stagingRoot))[0];
1707
+ let name;
1708
+ let version;
1709
+ try {
1710
+ const { metadata } = parseSkillMarkdown(await fs.readText(`${staged.skillDir}/SKILL.md`));
1711
+ name = metadata.name;
1712
+ version = typeof metadata["version"] === "string" ? metadata["version"] : void 0;
1713
+ } catch (e) {
1714
+ throw new WebSkillError("INSTALL_FAILED", `Failed to read skill name from SKILL.md: ${messageOf(e)}`, e);
1715
+ }
1716
+ if (!isValidSkillName(name)) throw new WebSkillError("INSTALL_FAILED", `Invalid skill name in SKILL.md (path traversal rejected): ${JSON.stringify(name)}`);
1717
+ const finalRoot = `${stagingRoot}/final`;
1718
+ const finalDir = `${finalRoot}/${name}`;
1719
+ await copyDir(fs, staged.skillDir, finalDir);
1720
+ const report = await validateSkills(fs, [finalRoot]);
1721
+ if (!report.ok) {
1722
+ const errors = report.issues.filter((i) => i.severity === "error");
1723
+ throw new WebSkillError("INSTALL_FAILED", `Skill "${name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
1724
+ }
1725
+ await this.#inferSkillSchemas(finalDir);
1726
+ const manifest = await createManifest(fs, finalDir, {
1727
+ name,
1728
+ ...version ? { version } : {},
1729
+ source: staged.source
1730
+ });
1731
+ targetDir = `${this.#managedRoot}/${name}`;
1732
+ const backupDir = `${stagingRoot}/backup`;
1733
+ const hadPrevious = await fs.exists(targetDir);
1734
+ if (hadPrevious) await copyDir(fs, targetDir, backupDir);
1735
+ let committed = false;
1736
+ try {
1737
+ if (hadPrevious) await fs.remove(targetDir, { recursive: true });
1738
+ await copyDir(fs, finalDir, targetDir);
1739
+ committed = true;
1740
+ await upsertLockEntry(fs, this.#managedRoot, name, {
1741
+ digest: manifest.integrity.digest,
1742
+ installedAt: manifest.installedAt,
1743
+ source: staged.source
1744
+ });
1745
+ } catch (e) {
1746
+ if (committed) await removeDirQuiet(fs, targetDir);
1747
+ if (hadPrevious) await copyDir(fs, backupDir, targetDir);
1748
+ targetDir = void 0;
1749
+ throw e;
1750
+ }
1751
+ targetDir = void 0;
1752
+ this.#onChanged?.();
1753
+ return manifest;
1754
+ } catch (e) {
1755
+ if (targetDir) await removeDirQuiet(fs, targetDir);
1756
+ throw asInstallFailed(e);
1757
+ } finally {
1758
+ await removeDirQuiet(fs, stagingRoot);
1759
+ }
1760
+ }
1761
+ /**
1762
+ * 包集安装:逐技能走单技能管线(归一 → 汇总校验 → 落 managed root → manifest → digest 比对)。
1763
+ * 任一失败整体回滚(删除本次已落目录;lockfile 在全部成功后才写入,天然零残留)。
1764
+ * 包是封闭产物:跳过安装期 schema 推导(sidecar 已随包携带,保证 digest 逐字节一致)。
1765
+ */
1766
+ async #installPack(pack, source, stagingRoot) {
1767
+ const fs = this.#fs;
1768
+ const finalRoot = `${stagingRoot}/final`;
1769
+ const installed = [];
1770
+ try {
1771
+ const versions = /* @__PURE__ */ new Map();
1772
+ for (const entry of pack.manifest.skills) {
1773
+ if (!isValidSkillName(entry.name)) throw new WebSkillError("INSTALL_FAILED", `Invalid skill name in skill pack manifest (path traversal rejected): ${JSON.stringify(entry.name)}`);
1774
+ const skillDir = `${pack.contentDir}/${entry.name}`;
1775
+ if (!await fs.exists(`${skillDir}/SKILL.md`)) throw new WebSkillError("INSTALL_FAILED", `Skill pack is missing a directory for skill "${entry.name}"`);
1776
+ let name;
1777
+ try {
1778
+ const { metadata } = parseSkillMarkdown(await fs.readText(`${skillDir}/SKILL.md`));
1779
+ name = metadata.name;
1780
+ versions.set(name, typeof metadata["version"] === "string" ? metadata["version"] : void 0);
1781
+ } catch (e) {
1782
+ throw new WebSkillError("INSTALL_FAILED", `Failed to read skill name from SKILL.md of pack entry "${entry.name}": ${messageOf(e)}`, e);
1783
+ }
1784
+ if (name !== entry.name) throw new WebSkillError("INSTALL_FAILED", `Skill pack entry "${entry.name}" does not match the SKILL.md name "${name}"`);
1785
+ await copyDir(fs, skillDir, `${finalRoot}/${name}`);
1786
+ }
1787
+ const report = await validateSkills(fs, [finalRoot]);
1788
+ if (!report.ok) {
1789
+ const errors = report.issues.filter((i) => i.severity === "error");
1790
+ throw new WebSkillError("INSTALL_FAILED", `Skill pack failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
1791
+ }
1792
+ const manifests = [];
1793
+ for (const entry of pack.manifest.skills) {
1794
+ const manifest = await createManifest(fs, `${finalRoot}/${entry.name}`, {
1795
+ name: entry.name,
1796
+ ...versions.get(entry.name) ? { version: versions.get(entry.name) } : {},
1797
+ source
1798
+ });
1799
+ if (manifest.integrity.digest !== entry.digest) throw new WebSkillError("INSTALL_FAILED", `Skill "${entry.name}" digest mismatch: expected ${entry.digest}, got ${manifest.integrity.digest}`);
1800
+ manifests.push(manifest);
1801
+ }
1802
+ const swapped = [];
1803
+ try {
1804
+ for (const manifest of manifests) {
1805
+ const targetDir = `${this.#managedRoot}/${manifest.name}`;
1806
+ const backupDir = `${stagingRoot}/backup/${manifest.name}`;
1807
+ if (await fs.exists(targetDir)) {
1808
+ await copyDir(fs, targetDir, backupDir);
1809
+ await fs.remove(targetDir, { recursive: true });
1810
+ }
1811
+ await copyDir(fs, `${finalRoot}/${manifest.name}`, targetDir);
1812
+ installed.push(targetDir);
1813
+ swapped.push({
1814
+ targetDir,
1815
+ backupDir
1816
+ });
1817
+ }
1818
+ for (const manifest of manifests) await upsertLockEntry(fs, this.#managedRoot, manifest.name, {
1819
+ digest: manifest.integrity.digest,
1820
+ installedAt: manifest.installedAt,
1821
+ source
1822
+ });
1823
+ } catch (e) {
1824
+ for (const { targetDir, backupDir } of swapped) {
1825
+ await removeDirQuiet(fs, targetDir);
1826
+ if (await fs.exists(backupDir)) await copyDir(fs, backupDir, targetDir);
1827
+ }
1828
+ installed.length = 0;
1829
+ throw e;
1830
+ }
1831
+ this.#onChanged?.();
1832
+ return manifests;
1833
+ } catch (e) {
1834
+ for (const targetDir of installed) await removeDirQuiet(fs, targetDir);
1835
+ throw asInstallFailed(e);
1836
+ }
1837
+ }
1838
+ /**
1839
+ * D2 安装期预推导:scripts/ 下无显式 inputSchema 导出且无 sidecar 的脚本,
1840
+ * 经 OXC 推导写 sidecar(计入 manifest.files;重装重新生成)
1841
+ */
1842
+ async #inferSkillSchemas(skillRoot) {
1843
+ if (!this.#schemaInference) return;
1844
+ const fs = this.#fs;
1845
+ const scriptsDir = `${skillRoot}/scripts`;
1846
+ if (!await fs.exists(scriptsDir)) return;
1847
+ for (const entry of await fs.list(scriptsDir)) {
1848
+ if (entry.type !== "file") continue;
1849
+ const match = /^(.*)\.(ts|js)$/.exec(entry.path.split("/").pop() ?? "");
1850
+ if (!match?.[1]) continue;
1851
+ const scriptName = match[1];
1852
+ const sidecar = `${scriptsDir}/${scriptName}.schema.json`;
1853
+ if (await fs.exists(sidecar)) continue;
1854
+ const source = await fs.readText(entry.path);
1855
+ if (/export\s+(?:const|let|var)\s+inputSchema/.test(source)) continue;
1856
+ const inferred = this.#schemaInferer.inferSchemaFromSource(source, { fileName: `${scriptName}.${match[2]}` });
1857
+ if (inferred) await fs.writeText(sidecar, JSON.stringify(inferred, null, 2));
1858
+ }
1859
+ }
1860
+ async uninstall(name) {
1861
+ if (!isValidSkillName(name)) throw new WebSkillError("UNINSTALL_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
1862
+ const targetDir = `${this.#managedRoot}/${name}`;
1863
+ if (!await this.#fs.exists(targetDir)) throw new WebSkillError("UNINSTALL_FAILED", `Skill "${name}" is not installed`);
1864
+ try {
1865
+ await this.#fs.remove(targetDir, { recursive: true });
1866
+ await removeLockEntry(this.#fs, this.#managedRoot, name);
1867
+ this.#onChanged?.();
1868
+ } catch (e) {
1869
+ throw new WebSkillError("UNINSTALL_FAILED", `Failed to uninstall "${name}": ${messageOf(e)}`, e);
1870
+ }
1871
+ }
1872
+ async verifyIntegrity(name) {
1873
+ if (!isValidSkillName(name)) throw new WebSkillError("INTEGRITY_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
1874
+ return verifyIntegrity(this.#fs, `${this.#managedRoot}/${name}`);
1875
+ }
1876
+ async listInstalled() {
1877
+ return readLockfile(this.#fs, this.#managedRoot);
1878
+ }
1879
+ async exportArchive(name, options) {
1880
+ if (!isValidSkillName(name)) throw new WebSkillError("EXPORT_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
1881
+ const skillRoot = `${this.#managedRoot}/${name}`;
1882
+ if (!await this.#fs.exists(skillRoot)) throw new WebSkillError("EXPORT_FAILED", `Skill "${name}" is not installed`);
1883
+ return exportArchive(this.#fs, skillRoot, options);
1884
+ }
1885
+ /** 多技能包集导出(webskill.skill-pack.json + 各技能目录含 manifest),写 outPath 并返回 */
1886
+ async exportPack(names, options) {
1887
+ if (names.length === 0) throw new WebSkillError("EXPORT_FAILED", "exportPack requires at least one skill name");
1888
+ const roots = names.map((name) => {
1889
+ if (!isValidSkillName(name)) throw new WebSkillError("EXPORT_FAILED", `Invalid skill name (path traversal rejected): ${JSON.stringify(name)}`);
1890
+ return `${this.#managedRoot}/${name}`;
1891
+ });
1892
+ for (const [index, root] of roots.entries()) if (!await this.#fs.exists(root)) throw new WebSkillError("EXPORT_FAILED", `Skill "${names[index]}" is not installed`);
1893
+ const bytes = await exportSkills(this.#fs, {
1894
+ roots,
1895
+ manifestBuilder: (root) => readManifest(this.#fs, root)
1896
+ });
1897
+ await this.#fs.writeBinary(options.outPath, bytes);
1898
+ return options.outPath;
1899
+ }
1900
+ };
1901
+
1902
+ //#endregion
1903
+ //#region ../governance/dist/node.js
1904
+ /** 审批工作流:review(UiBridge confirm 真实接线)/ publish(校验→安装→版本→审计) */
1905
+ var ApprovalWorkflow = class {
1906
+ #policy;
1907
+ #audit;
1908
+ #store;
1909
+ #skillManager;
1910
+ #versions;
1911
+ #fs;
1912
+ constructor(deps) {
1913
+ this.#policy = deps.policy;
1914
+ this.#audit = deps.audit;
1915
+ this.#store = deps.store;
1916
+ this.#skillManager = deps.skillManager;
1917
+ this.#versions = deps.versions;
1918
+ this.#fs = deps.fs ?? new NodeFS();
1919
+ }
1920
+ /** 策略评估;needs-human 时经 UiBridge confirm 真实询问,按应答迁移状态 */
1921
+ async review(candidateId, input) {
1922
+ const candidate = await this.#store.get(candidateId);
1923
+ if (candidate.status !== "draft" && candidate.status !== "pending-review") throw new WebSkillError("GOVERNANCE_FAILED", `Candidate "${candidateId}" cannot be reviewed from status "${candidate.status}"`);
1924
+ const decision = this.#policy.evaluate(candidate);
1925
+ let approved;
1926
+ if (decision.needsHuman) {
1927
+ if (!input.uiBridge) {
1928
+ await this.#store.updateStatus(candidateId, "pending-review");
1929
+ throw new WebSkillError("APPROVAL_REQUIRED", `Candidate "${candidate.name}" requires human approval: ${decision.reason}`);
1930
+ }
1931
+ await this.#store.updateStatus(candidateId, "pending-review");
1932
+ const response = await input.uiBridge.request({
1933
+ type: "confirm",
1934
+ id: `approval-${candidateId}`,
1935
+ message: `Approve candidate "${candidate.name}" (risk: ${candidate.risk})? ${decision.reason}`,
1936
+ defaultValue: false
1937
+ });
1938
+ approved = response.cancelled !== true && response.value === true;
1939
+ } else approved = true;
1940
+ const updated = await this.#store.updateStatus(candidateId, approved ? "approved" : "rejected");
1941
+ await this.#audit.append({
1942
+ type: "candidate.reviewed",
1943
+ target: candidateId,
1944
+ actor: input.actor,
1945
+ data: {
1946
+ approved,
1947
+ reason: decision.reason
1948
+ }
1949
+ });
1950
+ return updated;
1951
+ }
1952
+ /** publish 全链路:approved 前置 → 写出 staging → validateSkills → install → 版本 → 审计 */
1953
+ async publish(candidateId, input) {
1954
+ const candidate = await this.#store.get(candidateId);
1955
+ if (candidate.status !== "approved") throw new WebSkillError("APPROVAL_REQUIRED", `Candidate "${candidate.name}" must be approved before publishing (status: ${candidate.status})`);
1956
+ const stagingRoot = (await mkdtemp(path.join(tmpdir(), "webskill-candidate-"))).split(path.sep).join("/");
1957
+ try {
1958
+ const skillDir = `${stagingRoot}/${candidate.name}`;
1959
+ for (const file of candidate.files) await this.#fs.writeText(resolveInsideRoot(skillDir, file.path), file.content);
1960
+ const report = await validateSkills(this.#fs, [stagingRoot]);
1961
+ if (!report.ok) {
1962
+ const errors = report.issues.filter((i) => i.severity === "error");
1963
+ throw new WebSkillError("GOVERNANCE_FAILED", `Candidate "${candidate.name}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
1964
+ }
1965
+ const manifest = await this.#skillManager.install({
1966
+ type: "local",
1967
+ path: skillDir
1968
+ });
1969
+ const archiveOut = `${stagingRoot}/version-archive.zip`;
1970
+ await exportArchive(this.#fs, `${this.#skillManager.managedRoot}/${candidate.name}`, {
1971
+ format: "zip",
1972
+ outPath: archiveOut
1973
+ });
1974
+ await this.#versions.add(candidate.name, {
1975
+ reason: `Publish candidate ${candidateId}`,
1976
+ manifest,
1977
+ archive: await this.#fs.readBinary(archiveOut)
1978
+ });
1979
+ await this.#store.updateStatus(candidateId, "published");
1980
+ await this.#audit.append({
1981
+ type: "skill.published",
1982
+ target: candidate.name,
1983
+ actor: input.actor,
1984
+ data: {
1985
+ candidateId,
1986
+ digest: manifest.integrity.digest
1987
+ }
1988
+ });
1989
+ return manifest;
1990
+ } catch (e) {
1991
+ if (e instanceof WebSkillError) throw e;
1992
+ throw new WebSkillError("GOVERNANCE_FAILED", `Failed to publish candidate "${candidateId}": ${messageOf(e)}`, e);
1993
+ } finally {
1994
+ try {
1995
+ await this.#fs.remove(stagingRoot, { recursive: true });
1996
+ } catch {}
1997
+ }
1998
+ }
1999
+ /**
2000
+ * 真实回滚(受审批保护:仅经显式 actor 调用并全程审计):
2001
+ * 版本归档解包 → staging 校验 → 原子安装(复用安装管线 swap)→ 追加新版本 + 审计。
2002
+ * RepairPlanner 的 rollback 选项(targetVersionId)经本方法执行。
2003
+ */
2004
+ async applyRollback(skillName, versionId, input) {
2005
+ assertSafePathSegment(skillName, "skill name");
2006
+ assertSafePathSegment(versionId, "version id");
2007
+ const version = await this.#versions.get(skillName, versionId);
2008
+ const archive = await this.#versions.readArchive(skillName, versionId);
2009
+ const stagingRoot = (await mkdtemp(path.join(tmpdir(), "webskill-rollback-"))).split(path.sep).join("/");
2010
+ try {
2011
+ const skillDir = `${stagingRoot}/${skillName}`;
2012
+ for (const [rel, content] of await unzipWithLimits(archive)) {
2013
+ if (rel.endsWith("/")) continue;
2014
+ await this.#fs.writeBinary(resolveInsideRoot(skillDir, rel), content);
2015
+ }
2016
+ const report = await validateSkills(this.#fs, [stagingRoot]);
2017
+ if (!report.ok) {
2018
+ const errors = report.issues.filter((i) => i.severity === "error");
2019
+ throw new WebSkillError("GOVERNANCE_FAILED", `Rollback archive of "${skillName}" failed validation: ${errors.map((i) => i.message).join("; ")}`, errors);
2020
+ }
2021
+ const manifest = await this.#skillManager.install({
2022
+ type: "local",
2023
+ path: skillDir
2024
+ });
2025
+ if (manifest.integrity.digest !== version.manifest.integrity.digest) throw new WebSkillError("GOVERNANCE_FAILED", `Rollback of "${skillName}" to version "${versionId}" produced a digest mismatch: expected ${version.manifest.integrity.digest}, got ${manifest.integrity.digest}`);
2026
+ await this.#versions.add(skillName, {
2027
+ reason: input.reason ?? `Rollback to version ${versionId}`,
2028
+ manifest,
2029
+ archive
2030
+ });
2031
+ await this.#audit.append({
2032
+ type: "skill.rolled_back",
2033
+ target: skillName,
2034
+ actor: input.actor,
2035
+ data: {
2036
+ targetVersionId: versionId,
2037
+ reason: input.reason
2038
+ }
2039
+ });
2040
+ return manifest;
2041
+ } catch (e) {
2042
+ if (e instanceof WebSkillError) throw e;
2043
+ throw new WebSkillError("GOVERNANCE_FAILED", `Failed to roll back "${skillName}" to version "${versionId}": ${messageOf(e)}`, e);
2044
+ } finally {
2045
+ try {
2046
+ await this.#fs.remove(stagingRoot, { recursive: true });
2047
+ } catch {}
2048
+ }
2049
+ }
2050
+ };
2051
+ /**
2052
+ * 治理评估专用 runtime 装配(不可信技能试用路径):
2053
+ * 默认注入 ProcessSandboxExecutor(fork + --permission 真实进程隔离;子进程
2054
+ * env 默认清空防密钥泄露,需透传时经 ProcessSandboxOptions.envWhitelist 显式放行)。
2055
+ * 可配置 executor 切回 SandboxedScriptExecutor(worker_threads 能力面收敛形态,
2056
+ * 非安全边界;envWhitelist 同样适用于该执行器)。
2057
+ */
2058
+ function createEvaluationRuntime(deps) {
2059
+ return new WebSkillRuntime({
2060
+ ...deps,
2061
+ executor: deps.executor ?? new ProcessSandboxExecutor(deps.fs)
2062
+ });
2063
+ }
2064
+
2065
+ //#endregion
2066
+ export { ApprovalWorkflow, CliUiBridge, FileArtifactStore, FileMemoryStore, NodeFS, NodeScriptExecutor, OxcSchemaInferer, ProcessSandboxExecutor, SKILLS_LOCKFILE, SKILL_MANIFEST_FILE, SandboxedScriptExecutor, SkillManager, createEvaluationRuntime, createScriptContext, exportArchive, probeLlmCapabilities, readArchiveManifest };