@webskill/sdk 0.2.4 → 0.2.5
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/browser.d.ts +2 -2
- package/dist/browser.js +17 -13
- package/dist/{dist-CV64gN62.js → dist-B77plHjw.js} +147 -94
- package/dist/{dist-D7MsoMPx.js → dist-BQzncxXg.js} +70 -3
- package/dist/{dist-Chgf2tcy.js → dist-Chk8iB-E.js} +76 -31
- package/dist/{dist-CeNAzFYi.js → dist-DNSG9FqC.js} +108 -69
- package/dist/governance.d.ts +7 -4
- package/dist/governance.js +20 -8
- package/dist/{index-DrHelz72.d.ts → index-CsDJvYGV.d.ts} +5 -3
- package/dist/{index-DZShzhon.d.ts → index-DfINBEOy.d.ts} +11 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/mcp.d.ts +6 -2
- package/dist/mcp.js +14 -14
- package/dist/node.d.ts +3 -3
- package/dist/node.js +3 -3
- package/dist/{testing-BN18eqbD.js → testing-BUoXvm1u.js} +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{types-CKm5G_eQ-BqyXnvoR.d.ts → types-CKm5G_eQ-krKWW8WV.d.ts} +32 -2
- package/dist/ui-react.d.ts +3 -1
- package/dist/ui-react.js +8 -1
- package/dist/ui-vue.d.ts +3 -1
- package/dist/ui-vue.js +8 -1
- package/dist/ui.d.ts +20 -6
- package/dist/ui.js +3 -3
- package/package.json +1 -1
|
@@ -16,6 +16,10 @@ var WebSkillError = class extends Error {
|
|
|
16
16
|
this.details = details;
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
|
+
/** unknown 异常取消息文本(全仓单一来源,勿再本地重复定义) */
|
|
20
|
+
function messageOf(e) {
|
|
21
|
+
return e instanceof Error ? e.message : String(e);
|
|
22
|
+
}
|
|
19
23
|
/** 技能目录内的安装清单文件名(不参与 files 列表与 digest) */
|
|
20
24
|
const SKILL_MANIFEST_FILE = "webskill.skill-manifest.json";
|
|
21
25
|
/** managed root 下的锁定文件名(不参与 files 列表与 digest) */
|
|
@@ -200,7 +204,12 @@ var MemoryFS = class {
|
|
|
200
204
|
this.#entries.delete(src);
|
|
201
205
|
}
|
|
202
206
|
};
|
|
203
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* 原子写文本:先写临时文件再 rename(进程崩溃不留下半写文件;lockfile 等账本场景)。
|
|
209
|
+
* 原子性强弱取决于 provider 的 rename 实现:NodeFS/MemoryFS 为真 rename;
|
|
210
|
+
* OPFS 无原生 move,rename 是 copy+delete(非原子,崩溃可能残留双份),
|
|
211
|
+
* 对崩溃一致性要求极高的场景在浏览器侧需知悉此前提。
|
|
212
|
+
*/
|
|
204
213
|
async function atomicWriteText(fs, path, content) {
|
|
205
214
|
const tmp = `${path}.tmp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
206
215
|
await fs.writeText(tmp, content);
|
|
@@ -243,6 +252,65 @@ function resolveInsideRoot(root, relativePath) {
|
|
|
243
252
|
const joined = normalizedRoot === "" ? clean.join("/") : `${normalizedRoot}/${clean.join("/")}`;
|
|
244
253
|
return root.startsWith("/") || root.startsWith("\\") ? `/${joined}` : joined;
|
|
245
254
|
}
|
|
255
|
+
const LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost."]);
|
|
256
|
+
function ipv4Octets(host) {
|
|
257
|
+
const parts = host.split(".");
|
|
258
|
+
if (parts.length !== 4) return void 0;
|
|
259
|
+
const octets = parts.map((p) => /^\d{1,3}$/.test(p) ? Number(p) : NaN);
|
|
260
|
+
return octets.every((n) => n >= 0 && n <= 255) ? octets : void 0;
|
|
261
|
+
}
|
|
262
|
+
function isPrivateIpv4(octets) {
|
|
263
|
+
const [a, b] = octets;
|
|
264
|
+
return a === 0 || a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
|
|
265
|
+
}
|
|
266
|
+
function isPrivateIpv6(host) {
|
|
267
|
+
const h = host.toLowerCase();
|
|
268
|
+
if (h === "::1" || h === "::") return true;
|
|
269
|
+
const mapped = h.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
270
|
+
if (mapped) {
|
|
271
|
+
const octets = ipv4Octets(mapped[1]);
|
|
272
|
+
return octets !== void 0 && isPrivateIpv4(octets);
|
|
273
|
+
}
|
|
274
|
+
const mappedHex = h.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
275
|
+
if (mappedHex) {
|
|
276
|
+
const hi = parseInt(mappedHex[1], 16);
|
|
277
|
+
const lo = parseInt(mappedHex[2], 16);
|
|
278
|
+
return isPrivateIpv4([
|
|
279
|
+
hi >> 8,
|
|
280
|
+
hi & 255,
|
|
281
|
+
lo >> 8,
|
|
282
|
+
lo & 255
|
|
283
|
+
]);
|
|
284
|
+
}
|
|
285
|
+
const first = h.split(":")[0];
|
|
286
|
+
if (/^[0-9a-f]{1,4}$/.test(first)) {
|
|
287
|
+
const n = parseInt(first, 16);
|
|
288
|
+
if ((n & 65472) === 65152) return true;
|
|
289
|
+
if ((n & 65024) === 64512) return true;
|
|
290
|
+
}
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
function isPrivateHost(hostname) {
|
|
294
|
+
const host = hostname.toLowerCase();
|
|
295
|
+
if (LOOPBACK_HOSTNAMES.has(host) || host.endsWith(".localhost")) return true;
|
|
296
|
+
const v4 = ipv4Octets(host);
|
|
297
|
+
if (v4) return isPrivateIpv4(v4);
|
|
298
|
+
if (host.startsWith("[") && host.endsWith("]")) return isPrivateIpv6(host.slice(1, -1));
|
|
299
|
+
if (host.includes(":")) return isPrivateIpv6(host);
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* 校验远程 URL 是否允许连接;违规抛 NETWORK_BLOCKED。
|
|
304
|
+
* rawUrl 非合法 URL 时抛 TypeError(由调用方按自身错误码包装)。
|
|
305
|
+
*/
|
|
306
|
+
function assertRemoteUrlAllowed(rawUrl, policy = {}) {
|
|
307
|
+
const url = new URL(rawUrl);
|
|
308
|
+
if (url.protocol === "http:") {
|
|
309
|
+
if (!policy.allowHttp) throw new WebSkillError("NETWORK_BLOCKED", `Insecure URL scheme "http:" is not allowed (opt in via allowHttp): ${url.href}`);
|
|
310
|
+
} else if (url.protocol !== "https:") throw new WebSkillError("NETWORK_BLOCKED", `URL scheme "${url.protocol}" is not allowed (https only): ${url.href}`);
|
|
311
|
+
if (!policy.allowPrivateHosts && isPrivateHost(url.hostname)) throw new WebSkillError("NETWORK_BLOCKED", `Refusing to connect to a private/loopback/link-local address: ${url.hostname}`);
|
|
312
|
+
return url;
|
|
313
|
+
}
|
|
246
314
|
const FRONTMATTER_RE = /^---[^\S\r\n]*\r?\n([\s\S]*?)\r?\n---[^\S\r\n]*(?:\r?\n|$)([\s\S]*)$/;
|
|
247
315
|
const invalid = (message, details) => {
|
|
248
316
|
throw new WebSkillError("SKILL_INVALID_METADATA", message, details);
|
|
@@ -383,7 +451,6 @@ function checkDependencyCycles(adjacency) {
|
|
|
383
451
|
*/
|
|
384
452
|
/** 包级清单文件名(位于 zip 根,不参与各技能 digest) */
|
|
385
453
|
const SKILL_PACK_FILE = "webskill.skill-pack.json";
|
|
386
|
-
const messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
387
454
|
const baseName$1 = (p) => p.split("/").pop() ?? p;
|
|
388
455
|
async function listFilesRecursive(fs, root, prefix = "") {
|
|
389
456
|
const out = [];
|
|
@@ -769,4 +836,4 @@ const xmlRenderer = {
|
|
|
769
836
|
};
|
|
770
837
|
|
|
771
838
|
//#endregion
|
|
772
|
-
export {
|
|
839
|
+
export { resolveArchiveLimits as A, messageOf as C, readResponseWithLimit as D, parseSkillPackManifest as E, xmlRenderer as F, unzipWithLimits as M, validateSkills as N, renderAvailableSkillsXml as O, verifyManifest as P, jsonRenderer as S, parseSkillMarkdown as T, checkSkillRules as _, SKILL_NAME_MAX_LENGTH as a, exportSkills as b, SkillDiscovery as c, assertRemoteUrlAllowed as d, assertSafePathSegment as f, checkDependencyCycles as g, buildManifest as h, SKILL_MANIFEST_FILE as i, resolveInsideRoot as j, renderCatalogJson as k, SkillReader as l, buildCatalog as m, MemoryFS as n, SKILL_NAME_PATTERN as o, atomicWriteText as p, SKILLS_LOCKFILE as r, SKILL_PACK_FILE as s, DEFAULT_ARCHIVE_LIMITS as t, WebSkillError as u, computeDigest as v, normalizePath as w, isValidSkillName as x, escapeXml as y };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { A as isNetworkAllowed, C as bridgeError, F as normalizeToolError, I as parseBridgeRequest, M as networkUrlHost, P as normalizeToolContent, c as FsArtifactStore, l as FsMemoryStore, o as CapabilityApproval } from "./dist-
|
|
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, 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 { A as isNetworkAllowed, C as bridgeError, F as normalizeToolError, I as parseBridgeRequest, M as networkUrlHost, P as normalizeToolContent, c as FsArtifactStore, l as FsMemoryStore, o as CapabilityApproval } from "./dist-B77plHjw.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { unzipSync, zipSync } from "fflate";
|
|
5
5
|
import { existsSync, promises, realpathSync } from "node:fs";
|
|
@@ -295,7 +295,6 @@ var NodeScriptExecutor = class {
|
|
|
295
295
|
};
|
|
296
296
|
const baseName$1 = (p) => p.split("/").pop() ?? p;
|
|
297
297
|
const toPlatform$3 = (p) => p.split("/").join(path.sep);
|
|
298
|
-
const messageOf$6 = (e) => e instanceof Error ? e.message : String(e);
|
|
299
298
|
/**
|
|
300
299
|
* 网络策略判定函数源码(注入 Worker;匹配逻辑单一来源在
|
|
301
300
|
* runtime/sandbox/networkPolicy.ts,Worker 线程不走 vitest 别名故注入而非 import)
|
|
@@ -381,7 +380,7 @@ var SandboxedScriptExecutor = class {
|
|
|
381
380
|
content: [],
|
|
382
381
|
error: {
|
|
383
382
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
384
|
-
message: messageOf
|
|
383
|
+
message: messageOf(e)
|
|
385
384
|
}
|
|
386
385
|
};
|
|
387
386
|
}
|
|
@@ -422,7 +421,7 @@ var SandboxedScriptExecutor = class {
|
|
|
422
421
|
content: [],
|
|
423
422
|
error: {
|
|
424
423
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
425
|
-
message: messageOf
|
|
424
|
+
message: messageOf(e)
|
|
426
425
|
}
|
|
427
426
|
};
|
|
428
427
|
}
|
|
@@ -473,7 +472,7 @@ var SandboxedScriptExecutor = class {
|
|
|
473
472
|
respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
|
|
474
473
|
return;
|
|
475
474
|
}
|
|
476
|
-
onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf
|
|
475
|
+
onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf(e))));
|
|
477
476
|
return;
|
|
478
477
|
}
|
|
479
478
|
if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
|
|
@@ -534,13 +533,12 @@ var SandboxedScriptExecutor = class {
|
|
|
534
533
|
}
|
|
535
534
|
}
|
|
536
535
|
} catch (e) {
|
|
537
|
-
return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf
|
|
536
|
+
return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
|
|
538
537
|
}
|
|
539
538
|
}
|
|
540
539
|
};
|
|
541
540
|
const baseName = (p) => p.split("/").pop() ?? p;
|
|
542
541
|
const toPlatform$2 = (p) => p.split("/").join(path.sep);
|
|
543
|
-
const messageOf$5 = (e) => e instanceof Error ? e.message : String(e);
|
|
544
542
|
const NETWORK_POLICY_LIB = `${isNetworkAllowed.toString()}\n${networkUrlHost.toString()}`;
|
|
545
543
|
const readAllow = (p) => [`--allow-fs-read=${p}`, `--allow-fs-read=${realpathSync(p)}`];
|
|
546
544
|
const writeAllow = (p) => [`--allow-fs-write=${p}`, `--allow-fs-write=${realpathSync(p)}`];
|
|
@@ -572,6 +570,7 @@ var ProcessSandboxExecutor = class {
|
|
|
572
570
|
#approval;
|
|
573
571
|
#slots = [];
|
|
574
572
|
#waiters = [];
|
|
573
|
+
#disposed = false;
|
|
575
574
|
constructor(fs, options = {}) {
|
|
576
575
|
this.#fs = fs;
|
|
577
576
|
this.#options = options;
|
|
@@ -589,8 +588,9 @@ var ProcessSandboxExecutor = class {
|
|
|
589
588
|
get poolSize() {
|
|
590
589
|
return this.#options.poolSize ?? 2;
|
|
591
590
|
}
|
|
592
|
-
/**
|
|
591
|
+
/** 池全部子进程销毁(测试收尾/进程退出前调用);排队中的 acquire 一律 reject(不悬挂) */
|
|
593
592
|
async dispose() {
|
|
593
|
+
this.#disposed = true;
|
|
594
594
|
for (const slot of this.#slots) {
|
|
595
595
|
slot.child.kill();
|
|
596
596
|
await rm(slot.artifactDir, {
|
|
@@ -599,7 +599,8 @@ var ProcessSandboxExecutor = class {
|
|
|
599
599
|
}).catch(() => void 0);
|
|
600
600
|
}
|
|
601
601
|
this.#slots = [];
|
|
602
|
-
|
|
602
|
+
const waiters = this.#waiters.splice(0);
|
|
603
|
+
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"));
|
|
603
604
|
}
|
|
604
605
|
async #locateScript(skillRoot, scriptName) {
|
|
605
606
|
const tsPath = `${skillRoot}/scripts/${scriptName}.ts`;
|
|
@@ -612,6 +613,7 @@ var ProcessSandboxExecutor = class {
|
|
|
612
613
|
/** 取一个可用子进程(同 key 复用 / 淘汰 idle 重生 / 扩容 / 排队) */
|
|
613
614
|
async #acquire(key) {
|
|
614
615
|
for (;;) {
|
|
616
|
+
if (this.#disposed) throw new WebSkillError("TOOL_EXECUTION_FAILED", "Process sandbox executor is disposed");
|
|
615
617
|
const idle = this.#slots.filter((s) => !s.busy);
|
|
616
618
|
const match = idle.find((s) => s.key === key);
|
|
617
619
|
if (match) {
|
|
@@ -633,7 +635,10 @@ var ProcessSandboxExecutor = class {
|
|
|
633
635
|
this.#slots.push(slot);
|
|
634
636
|
return slot;
|
|
635
637
|
}
|
|
636
|
-
await new Promise((resolve) => this.#waiters.push(
|
|
638
|
+
await new Promise((resolve, reject) => this.#waiters.push({
|
|
639
|
+
resolve,
|
|
640
|
+
reject
|
|
641
|
+
}));
|
|
637
642
|
}
|
|
638
643
|
}
|
|
639
644
|
/** 执行后回收:kill 旧子进程,补位重生同 key 新子进程(温池保持),唤醒排队 */
|
|
@@ -645,13 +650,27 @@ var ProcessSandboxExecutor = class {
|
|
|
645
650
|
recursive: true,
|
|
646
651
|
force: true
|
|
647
652
|
}).catch(() => void 0);
|
|
648
|
-
this.#spawnSlot(slot.key).then((fresh) => {
|
|
653
|
+
this.#spawnSlot(slot.key).then(async (fresh) => {
|
|
654
|
+
if (this.#disposed) {
|
|
655
|
+
fresh.child.kill();
|
|
656
|
+
await rm(fresh.artifactDir, {
|
|
657
|
+
recursive: true,
|
|
658
|
+
force: true
|
|
659
|
+
}).catch(() => void 0);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
649
662
|
this.#slots.push(fresh);
|
|
650
|
-
}).catch(() =>
|
|
663
|
+
}).catch((e) => {
|
|
664
|
+
this.#warn(`Failed to respawn sandbox child for pool maintenance: ${messageOf(e)}`);
|
|
665
|
+
}).finally(() => {
|
|
651
666
|
const waiter = this.#waiters.shift();
|
|
652
|
-
if (waiter) waiter();
|
|
667
|
+
if (waiter) waiter.resolve();
|
|
653
668
|
});
|
|
654
669
|
}
|
|
670
|
+
#warn(message) {
|
|
671
|
+
if (this.#options.onWarning) this.#options.onWarning(message);
|
|
672
|
+
else console.warn(message);
|
|
673
|
+
}
|
|
655
674
|
async #spawnSlot(key) {
|
|
656
675
|
const artifactDir = await mkdtemp(path.join(tmpdir(), "webskill-psbx-out-")).then((d) => d.split(path.sep).join("/"));
|
|
657
676
|
const entry = processEntryPath();
|
|
@@ -712,7 +731,7 @@ var ProcessSandboxExecutor = class {
|
|
|
712
731
|
content: [],
|
|
713
732
|
error: {
|
|
714
733
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
715
|
-
message: messageOf
|
|
734
|
+
message: messageOf(e)
|
|
716
735
|
}
|
|
717
736
|
};
|
|
718
737
|
}
|
|
@@ -755,7 +774,7 @@ var ProcessSandboxExecutor = class {
|
|
|
755
774
|
content: [],
|
|
756
775
|
error: {
|
|
757
776
|
code: e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED",
|
|
758
|
-
message: messageOf
|
|
777
|
+
message: messageOf(e)
|
|
759
778
|
}
|
|
760
779
|
};
|
|
761
780
|
} finally {
|
|
@@ -794,7 +813,7 @@ var ProcessSandboxExecutor = class {
|
|
|
794
813
|
respond(bridgeError("unknown", "TOOL_EXECUTION_FAILED", "invalid bridge request"));
|
|
795
814
|
return;
|
|
796
815
|
}
|
|
797
|
-
onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf
|
|
816
|
+
onBridge(request).then(respond, (e) => respond(bridgeError(request.id, "TOOL_EXECUTION_FAILED", messageOf(e))));
|
|
798
817
|
return;
|
|
799
818
|
}
|
|
800
819
|
if (msg?.type === "load-result" || msg?.type === "execute-result") done(() => resolve(msg));
|
|
@@ -863,7 +882,7 @@ var ProcessSandboxExecutor = class {
|
|
|
863
882
|
}
|
|
864
883
|
}
|
|
865
884
|
} catch (e) {
|
|
866
|
-
return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf
|
|
885
|
+
return bridgeError(request.id, e instanceof WebSkillError ? e.code : "TOOL_EXECUTION_FAILED", messageOf(e));
|
|
867
886
|
}
|
|
868
887
|
}
|
|
869
888
|
};
|
|
@@ -1195,7 +1214,6 @@ var CliUiBridge = class {
|
|
|
1195
1214
|
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
1196
1215
|
}
|
|
1197
1216
|
};
|
|
1198
|
-
const messageOf$4 = (e) => e instanceof Error ? e.message : String(e);
|
|
1199
1217
|
const toPlatform$1 = (p) => p.split("/").join(path.sep);
|
|
1200
1218
|
const isZipArchive = (data) => data.length > 1 && data[0] === 80 && data[1] === 75;
|
|
1201
1219
|
let tarModule$1;
|
|
@@ -1244,7 +1262,7 @@ async function extractTarFile(archivePath, destRoot, limits) {
|
|
|
1244
1262
|
});
|
|
1245
1263
|
} catch (e) {
|
|
1246
1264
|
if (e instanceof WebSkillError) throw e;
|
|
1247
|
-
throw new WebSkillError("INSTALL_FAILED", `Failed to extract tar archive: ${messageOf
|
|
1265
|
+
throw new WebSkillError("INSTALL_FAILED", `Failed to extract tar archive: ${messageOf(e)}`, e);
|
|
1248
1266
|
}
|
|
1249
1267
|
}
|
|
1250
1268
|
/** 递归列出目录内全部文件(相对路径,posix 分隔,不含目录条目) */
|
|
@@ -1288,7 +1306,6 @@ async function removeDirQuiet(fs, dir) {
|
|
|
1288
1306
|
if (await fs.exists(dir)) await fs.remove(dir, { recursive: true });
|
|
1289
1307
|
} catch {}
|
|
1290
1308
|
}
|
|
1291
|
-
const messageOf$3 = (e) => e instanceof Error ? e.message : String(e);
|
|
1292
1309
|
let tarModule;
|
|
1293
1310
|
async function loadTar() {
|
|
1294
1311
|
tarModule ??= await import("tar").catch((e) => {
|
|
@@ -1310,7 +1327,7 @@ async function exportArchive(fs, skillRoot, options) {
|
|
|
1310
1327
|
return options.outPath;
|
|
1311
1328
|
} catch (e) {
|
|
1312
1329
|
if (e instanceof WebSkillError) throw e;
|
|
1313
|
-
throw new WebSkillError("EXPORT_FAILED", `Failed to export ${options.format} archive: ${messageOf
|
|
1330
|
+
throw new WebSkillError("EXPORT_FAILED", `Failed to export ${options.format} archive: ${messageOf(e)}`, e);
|
|
1314
1331
|
}
|
|
1315
1332
|
}
|
|
1316
1333
|
/** 只解出归档中的 webskill.skill-manifest.json 条目(安装前预览) */
|
|
@@ -1338,13 +1355,12 @@ async function readArchiveManifest(fs, archivePath) {
|
|
|
1338
1355
|
}
|
|
1339
1356
|
});
|
|
1340
1357
|
} catch (e) {
|
|
1341
|
-
throw new WebSkillError("EXPORT_FAILED", `Failed to read tar archive: ${messageOf
|
|
1358
|
+
throw new WebSkillError("EXPORT_FAILED", `Failed to read tar archive: ${messageOf(e)}`, e);
|
|
1342
1359
|
}
|
|
1343
1360
|
if (manifestText === void 0) throw notFound();
|
|
1344
1361
|
return JSON.parse(manifestText);
|
|
1345
1362
|
}
|
|
1346
1363
|
const _execFileP = promisify(execFile);
|
|
1347
|
-
const messageOf$2 = (e) => e instanceof Error ? e.message : String(e);
|
|
1348
1364
|
/**
|
|
1349
1365
|
* 跨平台执行命令:Windows 下对 npm 等 cmd 包装的命令通过 cmd.exe 代理执行,
|
|
1350
1366
|
* git 等原生 exe 不受影响。默认 120s 超时(防挂死安装管线)。
|
|
@@ -1360,7 +1376,7 @@ async function execFileP(command, args, options = {}) {
|
|
|
1360
1376
|
}
|
|
1361
1377
|
/** 命令缺失/失败 → INSTALL_FAILED 结构化诊断 */
|
|
1362
1378
|
function commandFailed(command, e) {
|
|
1363
|
-
return new WebSkillError("INSTALL_FAILED", `Failed to run ${command}: ${e?.code === "ENOENT" ? `command "${command}" not found on this system` : messageOf
|
|
1379
|
+
return new WebSkillError("INSTALL_FAILED", `Failed to run ${command}: ${e?.code === "ENOENT" ? `command "${command}" not found on this system` : messageOf(e)}`, e);
|
|
1364
1380
|
}
|
|
1365
1381
|
/** git url 协议白名单:https:// 与 git@ SCP 形式;无 scheme 的本地路径放行(dev 工作流);
|
|
1366
1382
|
* 其余显式协议(ext::/file:// 等)一律拒绝 */
|
|
@@ -1409,15 +1425,38 @@ async function stageGit(source, ctx) {
|
|
|
1409
1425
|
function sha256Hex(data) {
|
|
1410
1426
|
return createHash("sha256").update(data).digest("hex");
|
|
1411
1427
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1428
|
+
/** 重定向跳数上限(SSRF 防护:逐跳重新校验目标 URL) */
|
|
1429
|
+
const MAX_REDIRECT_HOPS = 3;
|
|
1430
|
+
/**
|
|
1431
|
+
* 带 SSRF 防护的下载:手动跟随重定向(默认 ≤3 跳),初始 URL 与每一跳目标
|
|
1432
|
+
* 都过 assertRemoteUrlAllowed(https 默认;私有/环回/链路本地默认拒绝)。
|
|
1433
|
+
*/
|
|
1434
|
+
async function fetchWithSsrfGuard(source, fetchImpl) {
|
|
1435
|
+
const policy = {
|
|
1436
|
+
allowHttp: source.allowHttp ?? false,
|
|
1437
|
+
allowPrivateHosts: source.allowPrivateHosts ?? false
|
|
1438
|
+
};
|
|
1439
|
+
let url = assertRemoteUrlAllowed(source.url, policy);
|
|
1440
|
+
for (let hop = 0;; hop++) {
|
|
1441
|
+
const res = await fetchImpl(url.href, { redirect: "manual" });
|
|
1442
|
+
const location = res.headers.get("location");
|
|
1443
|
+
if (res.status >= 300 && res.status < 400 && location) {
|
|
1444
|
+
if (hop >= MAX_REDIRECT_HOPS) throw new WebSkillError("INSTALL_FAILED", `Download exceeded the redirect limit of ${MAX_REDIRECT_HOPS} hops`);
|
|
1445
|
+
url = assertRemoteUrlAllowed(new URL(location, url).href, policy);
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
return res;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
/** http 源:下载归档(SSRF 防护 + Content-Length/流式上限;可选 expectedSha256 校验包体)→ 解包 → 定位技能根/包集 */
|
|
1414
1452
|
async function stageHttp(source, ctx, expectedSha256) {
|
|
1415
1453
|
const fetchImpl = ctx.fetchImpl ?? fetch;
|
|
1416
1454
|
let res;
|
|
1417
1455
|
try {
|
|
1418
|
-
res = await
|
|
1456
|
+
res = await fetchWithSsrfGuard(source, fetchImpl);
|
|
1419
1457
|
} catch (e) {
|
|
1420
|
-
|
|
1458
|
+
if (e instanceof WebSkillError) throw e;
|
|
1459
|
+
throw new WebSkillError("INSTALL_FAILED", `Download failed: ${messageOf(e)}`, e);
|
|
1421
1460
|
}
|
|
1422
1461
|
if (!res.ok) throw new WebSkillError("INSTALL_FAILED", `Download failed with HTTP ${res.status}`);
|
|
1423
1462
|
const data = await readResponseWithLimit(res, ctx.archiveLimits);
|
|
@@ -1466,13 +1505,19 @@ async function stageLocal(source, ctx) {
|
|
|
1466
1505
|
/** npm 包名(@scope/name 或 name,小写字母数字 . _ - /)与版本(semver 或 dist-tag)正则 */
|
|
1467
1506
|
const PACKAGE_NAME_RE = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
|
|
1468
1507
|
const VERSION_RE = /^[a-z0-9._~^>=<x*+-]+$/i;
|
|
1508
|
+
/**
|
|
1509
|
+
* 本地路径字符白名单(Windows 下 npm 经 cmd /c 代理二次解析,
|
|
1510
|
+
* `& | < > ^ " ' % ! ;` 与空格一律拒绝;允许 Windows 盘符 `C:\` 与正/反斜杠)
|
|
1511
|
+
*/
|
|
1512
|
+
const LOCAL_PATH_RE = /^[A-Za-z0-9._~+@/\\:-]+$/;
|
|
1469
1513
|
function assertNpmSpecSafe(source) {
|
|
1470
1514
|
const name = source.packageName;
|
|
1471
1515
|
const isLocalPath = name.startsWith("/") || name.startsWith("./") || name.startsWith("../");
|
|
1472
1516
|
if (name.startsWith("--") || !PACKAGE_NAME_RE.test(name) && !isLocalPath) throw new WebSkillError("INSTALL_FAILED", `Invalid npm package name: ${JSON.stringify(name)}`);
|
|
1517
|
+
if (isLocalPath && !LOCAL_PATH_RE.test(name)) throw new WebSkillError("INSTALL_FAILED", `Local npm path contains characters outside the safe whitelist: ${JSON.stringify(name)}`);
|
|
1473
1518
|
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)}`);
|
|
1474
1519
|
}
|
|
1475
|
-
/** npm 源:npm pack → 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根;spec 前插 `--` 防选项注入 */
|
|
1520
|
+
/** npm 源:npm pack --ignore-scripts → 解 tar.gz;技能根取包的 skill/ 子目录(存在时)否则包根;spec 前插 `--` 防选项注入 */
|
|
1476
1521
|
async function stageNpm(source, ctx) {
|
|
1477
1522
|
assertNpmSpecSafe(source);
|
|
1478
1523
|
const spec = source.version ? `${source.packageName}@${source.version}` : source.packageName;
|
|
@@ -1480,6 +1525,7 @@ async function stageNpm(source, ctx) {
|
|
|
1480
1525
|
try {
|
|
1481
1526
|
({stdout} = await execFileP("npm", [
|
|
1482
1527
|
"pack",
|
|
1528
|
+
"--ignore-scripts",
|
|
1483
1529
|
"--pack-destination",
|
|
1484
1530
|
ctx.stagingRoot,
|
|
1485
1531
|
"--",
|
|
@@ -1596,7 +1642,6 @@ async function verifyIntegrity(fs, skillRoot) {
|
|
|
1596
1642
|
for (const rel of actualFiles) actualHashes.set(rel, sha256Hex(await fs.readBinary(`${skillRoot}/${rel}`)));
|
|
1597
1643
|
return verifyManifest(manifest, actualHashes, actualFiles);
|
|
1598
1644
|
}
|
|
1599
|
-
const messageOf = (e) => e instanceof Error ? e.message : String(e);
|
|
1600
1645
|
const asInstallFailed = (e) => e instanceof WebSkillError && e.code === "INSTALL_FAILED" ? e : new WebSkillError("INSTALL_FAILED", `Install failed: ${messageOf(e)}`, e);
|
|
1601
1646
|
/**
|
|
1602
1647
|
* 技能管理门面:统一安装管线(staging → 解析 name → 校验 → 拷贝 → manifest → lockfile),
|